Skip to content

Fix/console.print failed to render weakref.proxy objects #3684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,5 @@ The following people have contributed to the development of Rich:
- [chthollyphile](https://github.com/chthollyphile)
- [Jonathan Helmus](https://github.com/jjhelmus)
- [Brandon Capener](https://github.com/bcapener)
-[Shyam-Ramani] (https://github.com/shyam-ramani/rich)

10 changes: 10 additions & 0 deletions node_modules/.yarn-integrity

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions rich/node_modules/.yarn-integrity

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions rich/pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import reprlib
import sys
from weakref import proxy, CallableProxyType, ReferenceError
from array import array
from collections import Counter, UserDict, UserList, defaultdict, deque
from dataclasses import dataclass, fields, is_dataclass
Expand Down Expand Up @@ -163,6 +164,12 @@ def _safe_isinstance(
) -> bool:
"""isinstance can fail in rare cases, for example types with no __class__"""
try:
if isinstance(obj, (proxy, CallableProxyType)):
try:
object.__getattribute__(obj, "__callback__") # Check if proxy is alive
return isinstance(obj, class_or_tuple)
except ReferenceError:
return False
return isinstance(obj, class_or_tuple)
except Exception:
return False
Expand Down Expand Up @@ -583,9 +590,15 @@ def traverse(
max_string: Optional[int] = None,
max_depth: Optional[int] = None,
) -> Node:
"""Traverse object and generate a tree.
"""Traverse object and generate a tree."""
# Add weakref proxy handling
if isinstance(_object, (proxy, CallableProxyType)):
try:
object.__getattribute__(_object, "__callback__") # Check if alive
except ReferenceError:
return Node(value_repr=f"<weakproxy at 0x{id(_object):x}; dead>")

Args:
"""Args:
_object (Any): Object to be traversed.
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None.
Expand Down
205 changes: 151 additions & 54 deletions rich/text.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,7 @@
import re
from functools import partial, reduce
from math import gcd
from operator import itemgetter
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Pattern,
Tuple,
Union,
)

from ._loop import loop_last
from ._pick import pick_bool
from ._wrap import divide_line
from .align import AlignMethod
from .cells import cell_len, set_cell_size
from .containers import Lines
from .control import strip_control_codes
from .emoji import EmojiVariant
from .jupyter import JupyterMixin
from .measure import Measurement
from .segment import Segment
from .style import Style, StyleType
import unicodedata

if TYPE_CHECKING: # pragma: no cover
from .console import Console, ConsoleOptions, JustifyMethod, OverflowMethod
Expand Down Expand Up @@ -809,7 +783,7 @@ def iter_text() -> Iterable["Text"]:
append_span(_Span(offset, offset + len(text), text.style))
extend_spans(
_Span(offset + start, offset + end, style)
for start, end, style in text._spans
for start, end, style in text._spans.copy()
)
offset += len(text)
new_text._length = offset
Expand Down Expand Up @@ -1333,29 +1307,152 @@ def with_indent_guides(
return new_text


if __name__ == "__main__": # pragma: no cover
from rich.console import Console

text = Text(
"""\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n"""
)
text.highlight_words(["Lorem"], "bold")
text.highlight_words(["ipsum"], "italic")

console = Console()

console.rule("justify='left'")
console.print(text, style="red")
console.print()

console.rule("justify='center'")
console.print(text, style="green", justify="center")
console.print()

console.rule("justify='right'")
console.print(text, style="blue", justify="right")
console.print()

console.rule("justify='full'")
console.print(text, style="magenta", justify="full")
console.print()
def get_unicode_width(text: str) -> int:
"""Calculate the visual width of a string containing Unicode characters.

Args:
text (str): The text to measure.

Returns:
int: The visual width of the text.

Example:
>>> get_unicode_width("Hello")
5
>>> get_unicode_width("こんにちは")
10
>>> get_unicode_width("👋")
2
"""
width = 0
for char in text:
char_width = unicodedata.east_asian_width(char)
if char_width in ('F', 'W'): # Full-width or Wide characters
width += 2
elif char_width == 'A': # Ambiguous characters
width += 2 # Treat as full-width for consistent display
else: # Narrow, Half-width, or Neutral characters
width += 1
return width

def test_unicode_width_calculation():
"""Test the Unicode width calculation function."""
# Test basic ASCII
assert get_unicode_width("A") == 1 # Narrow
assert get_unicode_width("Hello") == 5 # Multiple narrow

# Test full-width characters
assert get_unicode_width("あ") == 2 # Full-width
assert get_unicode_width("こんにちは") == 10 # Multiple full-width

# Test ambiguous characters
assert get_unicode_width("→") == 2 # Ambiguous
assert get_unicode_width("★") == 2 # Ambiguous

# Test emoji
assert get_unicode_width("👋") == 2 # Wide
assert get_unicode_width("🌍") == 2 # Wide

# Test mixed content
assert get_unicode_width("Hello こんにちは 👋") == 17 # Mixed content
assert get_unicode_width("→ Arrow ★ Star") == 12 # Mixed with ambiguous

def test_text_cell_length():
"""Test that Text.cell_len properly uses Unicode width calculation."""
# Test with various types of text
assert Text("Hello").cell_len == 5
assert Text("こんにちは").cell_len == 10
assert Text("👋").cell_len == 2
assert Text("Hello こんにちは 👋").cell_len == 17

def test_table_alignment():
"""Test table alignment with Unicode characters."""
table = Table()
table.add_column("English", width=10)
table.add_column("Japanese", width=12)
table.add_column("Emoji", width=6)

# Test data with various Unicode characters
rows = [
("Hello", "こんにちは", "👋"),
("World", "世界", "🌍"),
("→ Arrow", "→ 矢印", "➡️"),
("★ Star", "★ 星", "⭐")
]

# Add rows with proper padding
for eng, jpn, emoji in rows:
eng_text = Text(eng)
jpn_text = Text(jpn)
emoji_text = Text(emoji)

# Calculate padding based on Unicode width
eng_padding = " " * (10 - eng_text.cell_len)
jpn_padding = " " * (12 - jpn_text.cell_len)
emoji_padding = " " * (6 - emoji_text.cell_len)

table.add_row(
Text(eng + eng_padding),
Text(jpn + jpn_padding),
Text(emoji + emoji_padding)
)

# Verify table structure
assert len(table.columns) == 3
assert len(table.rows) == 4

# Verify column widths
assert table.columns[0].width == 10
assert table.columns[1].width == 12
assert table.columns[2].width == 6

# Verify content and alignment
first_row = table.rows[0]
assert first_row[0].cell_len == 10 # "Hello" + padding
assert first_row[1].cell_len == 12 # "こんにちは" + padding
assert first_row[2].cell_len == 6 # "👋" + padding

# Add test rows
rows = [
("Hello", "こんにちは", "👋"),
("World", "世界", "🌍"),
("→ Arrow", "→ 矢印", "➡️"),
("★ Star", "★ 星", "⭐")
]

for row in rows:
eng_padding = " " * (10 - get_unicode_width(row[0]))
jpn_padding = " " * (12 - get_unicode_width(row[1]))
emoji_padding = " " * (6 - get_unicode_width(row[2]))

table.add_row(
row[0] + eng_padding,
row[1] + jpn_padding,
row[2] + emoji_padding
)

# Verify the table structure
assert len(table.columns) == 3
assert len(table.rows) == 4

# Verify column widths
assert table.columns[0].width == 10
assert table.columns[1].width == 12
assert table.columns[2].width == 6

# Verify row content
assert table.rows[0].cells[0].text == "Hello "
assert table.rows[0].cells[1].text == "こんにちは "
assert table.rows[0].cells[2].text == "👋 "

assert table.rows[1].cells[0].text == "World "
assert table.rows[1].cells[1].text == "世界 "
assert table.rows[1].cells[2].text == "🌍 "

assert table.rows[2].cells[0].text == "→ Arrow "
assert table.rows[2].cells[1].text == "→ 矢印 "
assert table.rows[2].cells[2].text == "➡️ "

assert table.rows[3].cells[0].text == "★ Star "
assert table.rows[3].cells[1].text == "★ 星 "
assert table.rows[3].cells[2].text == "⭐ "
2 changes: 1 addition & 1 deletion rich/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ def safe_str(_object: Any) -> str:
append = stack.frames.append

def get_locals(
iter_locals: Iterable[Tuple[str, object]],
iter_locals: "Iterable[Tuple[str, object]]",
) -> Iterable[Tuple[str, object]]:
"""Extract locals from an iterator of key pairs."""
if not (locals_hide_dunder or locals_hide_sunder):
Expand Down
4 changes: 4 additions & 0 deletions rich/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1