Skip to content

Commit dea8833

Browse files
authored
Added monkey patch for Rich AnsiDecoder.decode() bug. (#1668)
1 parent bbd689f commit dea8833

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

cmd2/rich_utils.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import argparse
44
import re
55
import sys
6-
from collections.abc import Iterator
6+
from collections.abc import (
7+
Iterable,
8+
Iterator,
9+
)
710
from enum import Enum
811
from typing import (
912
IO,
@@ -14,6 +17,7 @@
1417
runtime_checkable,
1518
)
1619

20+
from rich.ansi import AnsiDecoder
1721
from rich.box import SIMPLE_HEAD
1822
from rich.console import (
1923
Console,
@@ -672,3 +676,38 @@ def prepare_objects_for_rendering(*objects: Any) -> tuple[Any, ...]:
672676
object_list[i] = Text.from_ansi(renderable_as_str)
673677

674678
return tuple(object_list)
679+
680+
681+
###################################################################################
682+
# Rich Library Monkey Patches
683+
#
684+
# These patches fix specific bugs in the Rich library. They are conditional and
685+
# will only be applied if the bug is detected. When the bugs are fixed in a
686+
# future Rich release, these patches and their corresponding tests should be
687+
# removed.
688+
###################################################################################
689+
690+
###################################################################################
691+
# AnsiDecoder.decode() monkey patch
692+
###################################################################################
693+
694+
695+
def _AnsiDecoder_decode(self: AnsiDecoder, terminal_text: str) -> Iterable[Text]: # noqa: N802
696+
"""Patch AnsiDecoder.decode() to properly handle CRLF.
697+
698+
There is currently a pull request on Rich to fix this.
699+
https://github.com/Textualize/rich/pull/4143
700+
"""
701+
for line in re.split(r"(?<=\n)", terminal_text):
702+
# Strip off any remaining line break characters from the end
703+
yield self.decode_line(line.rstrip("\r\n"))
704+
705+
706+
def _decode_has_linebreak_bug() -> bool:
707+
"""Check if AnsiDecoder.decode() properly handles CRLF."""
708+
return Text.from_ansi("hello\r\nworld").plain == "\nworld"
709+
710+
711+
# Only apply the monkey patch if the bug is present
712+
if _decode_has_linebreak_bug():
713+
AnsiDecoder.decode = _AnsiDecoder_decode # type: ignore[assignment]

tests/test_rich_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,28 @@ def side_effect(color: bool, **kwargs: Any) -> None:
317317
assert mock_set_color.call_count == 2
318318
mock_set_color.assert_any_call(True, file=sys.stdout)
319319
mock_set_color.assert_any_call(True)
320+
321+
322+
@pytest.mark.parametrize("line_break", ["\n", "\r\n"])
323+
def test_ansi_decoder_patch(line_break: str) -> None:
324+
# Check if we are still patching AnsiDecoder.decode(). If this check fails, then Rich
325+
# has fixed the bug. Therefore, we can remove this test function and ru._AnsiDecoder_decode.
326+
from rich.ansi import AnsiDecoder
327+
328+
assert AnsiDecoder.decode is ru._AnsiDecoder_decode
329+
330+
assert Text.from_ansi("").plain == ""
331+
assert Text.from_ansi(f"{line_break}").plain == "\n"
332+
assert Text.from_ansi(f"{line_break}{line_break}").plain == "\n\n"
333+
assert Text.from_ansi("Hello").plain == "Hello"
334+
assert Text.from_ansi(f"{line_break}Hello").plain == "\nHello"
335+
assert Text.from_ansi(f"Hello{line_break}").plain == "Hello\n"
336+
assert Text.from_ansi(f"Hello{line_break}{line_break}").plain == "Hello\n\n"
337+
assert Text.from_ansi(f"Hello{line_break}World").plain == "Hello\nWorld"
338+
assert Text.from_ansi(f"Hello{line_break}{line_break}World").plain == "Hello\n\nWorld"
339+
assert Text.from_ansi(f"Hello{line_break}World{line_break}").plain == "Hello\nWorld\n"
340+
assert Text.from_ansi(f"Hello{line_break}World{line_break}{line_break}").plain == "Hello\nWorld\n\n"
341+
assert Text.from_ansi(f"{line_break}Hello{line_break}World{line_break}{line_break}").plain == "\nHello\nWorld\n\n"
342+
343+
# Include a mixture of line break types
344+
assert Text.from_ansi(f"Hello{line_break}\n\r\nWorld").plain == "Hello\n\n\nWorld"

0 commit comments

Comments
 (0)