Skip to content
Merged
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
61 changes: 0 additions & 61 deletions cmd2/rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,64 +477,3 @@ def prepare_objects_for_rendering(*objects: Any) -> tuple[Any, ...]:
object_list[i] = Text.from_ansi(renderable_as_str)

return tuple(object_list)


###################################################################################
# Rich Library Monkey Patches
#
# These patches fix specific bugs in the Rich library. They are conditional and
# will only be applied if the bug is detected. When the bugs are fixed in a
# future Rich release, these patches and their corresponding tests should be
# removed.
###################################################################################

###################################################################################
# Text.from_ansi() monkey patch
###################################################################################

# Save original Text.from_ansi() so we can call it in our wrapper
_orig_text_from_ansi = Text.from_ansi


@classmethod # type: ignore[misc]
def _from_ansi_wrapper(cls: type[Text], text: str, *args: Any, **kwargs: Any) -> Text: # noqa: ARG001
r"""Wrap Text.from_ansi() to fix its trailing newline bug.

This wrapper handles an issue where Text.from_ansi() removes the
trailing line break from a string (e.g. "Hello\n" becomes "Hello").

There is currently a pull request on Rich to fix this.
https://github.com/Textualize/rich/pull/3793
"""
result = _orig_text_from_ansi(text, *args, **kwargs)

# If the original string ends with a recognized line break character,
# then restore the missing newline. We use "\n" because Text.from_ansi()
# converts all line breaks into newlines.
# Source: https://docs.python.org/3/library/stdtypes.html#str.splitlines
line_break_chars = {
"\n", # Line Feed
"\r", # Carriage Return
"\v", # Vertical Tab
"\f", # Form Feed
"\x1c", # File Separator
"\x1d", # Group Separator
"\x1e", # Record Separator
"\x85", # Next Line (NEL)
"\u2028", # Line Separator
"\u2029", # Paragraph Separator
}
if text and text[-1] in line_break_chars:
result.append("\n")

return result


def _from_ansi_has_newline_bug() -> bool:
"""Check if Test.from_ansi() strips the trailing line break from a string."""
return Text.from_ansi("\n") == Text.from_ansi("")


# Only apply the monkey patch if the bug is present
if _from_ansi_has_newline_bug():
Text.from_ansi = _from_ansi_wrapper # type: ignore[assignment]
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ dependencies = [
"gnureadline>=8; platform_system == 'Darwin'",
"pyperclip>=1.8.2",
"pyreadline3>=3.4; platform_system == 'Windows'",
"rich>=14.3.0",
"rich-argparse>=1.7.1",
"rich>=15.0.0",
"rich-argparse>=1.7.2",
"typing-extensions; python_version == '3.10'",
]

Expand Down
36 changes: 0 additions & 36 deletions tests/test_rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,42 +110,6 @@ def test_set_theme() -> None:
assert ru.APP_THEME.styles[rich_style_key] == theme[rich_style_key]


def test_from_ansi_wrapper() -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 While the monkey patch is being removed, it is beneficial to keep a simplified version of this test as a regression check. This ensures that the required version of the rich library (>= 15.0.0) continues to provide the behavior cmd2 expects regarding trailing newlines.

Suggested change
def test_from_ansi_wrapper() -> None:
def test_text_from_ansi() -> None:
# Line breaks recognized by str.splitlines().
# Source: https://docs.python.org/3/library/stdtypes.html#str.splitlines
line_breaks = {
"\n", # Line Feed
"\r", # Carriage Return
"\r\n", # Carriage Return + Line Feed
"\v", # Vertical Tab
"\f", # Form Feed
"\x1c", # File Separator
"\x1d", # Group Separator
"\x1e", # Record Separator
"\x85", # Next Line (NEL)
"\u2028", # Line Separator
"\u2029", # Paragraph Separator
}
# Test all line breaks
for lb in line_breaks:
input_string = f"Text{lb}"
expected_output = input_string.replace(lb, "\n")
assert Text.from_ansi(input_string).plain == expected_output
# Test string without trailing line break
input_string = "No trailing\nline break"
assert Text.from_ansi(input_string).plain == input_string
# Test empty string
input_string = ""
assert Text.from_ansi(input_string).plain == input_string

# Check if we are still patching Text.from_ansi(). If this check fails, then Rich
# has fixed the bug. Therefore, we can remove this test function and ru._from_ansi_wrapper.
assert Text.from_ansi.__func__ is ru._from_ansi_wrapper.__func__ # type: ignore[attr-defined]

# Line breaks recognized by str.splitlines().
# Source: https://docs.python.org/3/library/stdtypes.html#str.splitlines
line_breaks = {
"\n", # Line Feed
"\r", # Carriage Return
"\r\n", # Carriage Return + Line Feed
"\v", # Vertical Tab
"\f", # Form Feed
"\x1c", # File Separator
"\x1d", # Group Separator
"\x1e", # Record Separator
"\x85", # Next Line (NEL)
"\u2028", # Line Separator
"\u2029", # Paragraph Separator
}

# Test all line breaks
for lb in line_breaks:
input_string = f"Text{lb}"
expected_output = input_string.replace(lb, "\n")
assert Text.from_ansi(input_string).plain == expected_output

# Test string without trailing line break
input_string = "No trailing\nline break"
assert Text.from_ansi(input_string).plain == input_string

# Test empty string
input_string = ""
assert Text.from_ansi(input_string).plain == input_string


@with_ansi_style(ru.AllowStyle.ALWAYS)
def test_cmd2_base_console_print() -> None:
"""Test that Cmd2BaseConsole.print() correctly propagates formatting overrides to structured renderables."""
Expand Down
Loading