Skip to content

Commit 926636a

Browse files
authored
Removed monkeypatch for Rich's Segment.apply_style(). (#1560)
* Removed monkeypatch for Rich's Segment.apply_style() since they fixed the bug in 14.3.0. Updated minimum Rich dependency to this version. * Removed unused Cmd2Style.EXCEPTION_TYPE style.
1 parent 7aa4ded commit 926636a

File tree

5 files changed

+3
-265
lines changed

5 files changed

+3
-265
lines changed

.github/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ See the `dependencies` list under the `[project]` heading in [pyproject.toml](..
6565
| ---------------------------------------------------------- | --------------- | ------------------------------------------------------ |
6666
| [python](https://www.python.org/downloads/) | `3.10` | Python programming language |
6767
| [pyperclip](https://github.com/asweigart/pyperclip) | `1.8` | Cross-platform clipboard functions |
68-
| [rich](https://github.com/Textualize/rich) | `14.1.0` | Add rich text and beautiful formatting in the terminal |
68+
| [rich](https://github.com/Textualize/rich) | `14.3.0` | Add rich text and beautiful formatting in the terminal |
6969
| [rich-argparse](https://github.com/hamdanal/rich-argparse) | `1.7.1` | A rich-enabled help formatter for argparse |
7070

7171
> `macOS` and `Windows` each have an extra dependency to ensure they have a viable alternative to

cmd2/rich_utils.py

Lines changed: 1 addition & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
"""Provides common utilities to support Rich in cmd2-based applications."""
22

33
import re
4-
from collections.abc import (
5-
Iterable,
6-
Mapping,
7-
)
4+
from collections.abc import Mapping
85
from enum import Enum
96
from typing import (
107
IO,
@@ -22,7 +19,6 @@
2219
from rich.padding import Padding
2320
from rich.pretty import is_expandable
2421
from rich.protocol import rich_cast
25-
from rich.segment import Segment
2622
from rich.style import StyleType
2723
from rich.table import (
2824
Column,
@@ -380,72 +376,3 @@ def _from_ansi_has_newline_bug() -> bool:
380376
# Only apply the monkey patch if the bug is present
381377
if _from_ansi_has_newline_bug():
382378
Text.from_ansi = _from_ansi_wrapper # type: ignore[assignment]
383-
384-
385-
###################################################################################
386-
# Segment.apply_style() monkey patch
387-
###################################################################################
388-
389-
# Save original Segment.apply_style() so we can call it in our wrapper
390-
_orig_segment_apply_style = Segment.apply_style
391-
392-
393-
@classmethod # type: ignore[misc]
394-
def _apply_style_wrapper(cls: type[Segment], *args: Any, **kwargs: Any) -> Iterable["Segment"]:
395-
r"""Wrap Segment.apply_style() to fix bug with styling newlines.
396-
397-
This wrapper handles an issue where Segment.apply_style() includes newlines
398-
within styled Segments. As a result, when printing text using a background color
399-
and soft wrapping, the background color incorrectly carries over onto the following line.
400-
401-
You can reproduce this behavior by calling console.print() using a background color
402-
and soft wrapping.
403-
404-
For example:
405-
console.print("line_1", style="blue on white", soft_wrap=True)
406-
407-
When soft wrapping is disabled, console.print() splits Segments into their individual
408-
lines, which separates the newlines from the styled text. Therefore, the background color
409-
issue does not occur in that mode.
410-
411-
This function copies that behavior to fix this the issue even when soft wrapping is enabled.
412-
413-
There is currently a pull request on Rich to fix this.
414-
https://github.com/Textualize/rich/pull/3839
415-
"""
416-
styled_segments = list(_orig_segment_apply_style(*args, **kwargs))
417-
newline_segment = cls.line()
418-
419-
# If the final segment ends in a newline, that newline will be stripped by Segment.split_lines().
420-
# Save an unstyled newline to restore later.
421-
end_segment = newline_segment if styled_segments and styled_segments[-1].text.endswith("\n") else None
422-
423-
# Use Segment.split_lines() to separate the styled text from the newlines.
424-
# This way the ANSI reset code will appear before any newline.
425-
sanitized_segments: list[Segment] = []
426-
427-
lines = list(Segment.split_lines(styled_segments))
428-
for index, line in enumerate(lines):
429-
sanitized_segments.extend(line)
430-
if index < len(lines) - 1:
431-
sanitized_segments.append(newline_segment)
432-
433-
if end_segment is not None:
434-
sanitized_segments.append(end_segment)
435-
436-
return sanitized_segments
437-
438-
439-
def _rich_has_styled_newline_bug() -> bool:
440-
"""Check if newlines are styled when soft wrapping."""
441-
console = Console(force_terminal=True)
442-
with console.capture() as capture:
443-
console.print("line_1", style="blue on white", soft_wrap=True)
444-
445-
# Check if we see a styled newline in the output
446-
return "\x1b[34;47m\n\x1b[0m" in capture.get()
447-
448-
449-
# Only apply the monkey patch if the bug is present
450-
if _rich_has_styled_newline_bug():
451-
Segment.apply_style = _apply_style_wrapper # type: ignore[assignment]

cmd2/styles.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ class Cmd2Style(StrEnum):
5151

5252
COMMAND_LINE = "cmd2.example" # Command line examples in help text
5353
ERROR = "cmd2.error" # Error text (used by perror())
54-
EXCEPTION_TYPE = "cmd2.exception.type" # Used by pexcept to mark an exception type
5554
HELP_HEADER = "cmd2.help.header" # Help table header text
5655
HELP_LEADER = "cmd2.help.leader" # Text right before the help tables are listed
5756
SUCCESS = "cmd2.success" # Success text (used by psuccess())
@@ -63,7 +62,6 @@ class Cmd2Style(StrEnum):
6362
DEFAULT_CMD2_STYLES: dict[str, StyleType] = {
6463
Cmd2Style.COMMAND_LINE: Style(color=Color.CYAN, bold=True),
6564
Cmd2Style.ERROR: Style(color=Color.BRIGHT_RED),
66-
Cmd2Style.EXCEPTION_TYPE: Style(color=Color.DARK_ORANGE, bold=True),
6765
Cmd2Style.HELP_HEADER: Style(color=Color.BRIGHT_GREEN, bold=True),
6866
Cmd2Style.HELP_LEADER: Style(color=Color.CYAN, bold=True),
6967
Cmd2Style.SUCCESS: Style(color=Color.GREEN),

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ dependencies = [
3333
"gnureadline>=8; platform_system == 'Darwin'",
3434
"pyperclip>=1.8.2",
3535
"pyreadline3>=3.4; platform_system == 'Windows'",
36-
"rich>=14.1.0",
36+
"rich>=14.3.0",
3737
"rich-argparse>=1.7.1",
3838
]
3939

tests/test_rich_utils.py

Lines changed: 0 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import pytest
44
import rich.box
55
from rich.console import Console
6-
from rich.segment import Segment
76
from rich.style import Style
87
from rich.table import Table
98
from rich.text import Text
@@ -143,189 +142,3 @@ def test_from_ansi_wrapper() -> None:
143142
# Test empty string
144143
input_string = ""
145144
assert Text.from_ansi(input_string).plain == input_string
146-
147-
148-
@pytest.mark.parametrize(
149-
# Print with style and verify that everything but newline characters have style.
150-
('objects', 'sep', 'end', 'expected'),
151-
[
152-
# Print nothing
153-
((), " ", "\n", "\n"),
154-
# Empty string
155-
(("",), " ", "\n", "\n"),
156-
# Multple empty strings
157-
(("", ""), " ", "\n", "\x1b[34;47m \x1b[0m\n"),
158-
# Basic string
159-
(
160-
("str_1",),
161-
" ",
162-
"\n",
163-
"\x1b[34;47mstr_1\x1b[0m\n",
164-
),
165-
# String which ends with newline
166-
(
167-
("str_1\n",),
168-
" ",
169-
"\n",
170-
"\x1b[34;47mstr_1\x1b[0m\n\n",
171-
),
172-
# String which ends with multiple newlines
173-
(
174-
("str_1\n\n",),
175-
" ",
176-
"\n",
177-
"\x1b[34;47mstr_1\x1b[0m\n\n\n",
178-
),
179-
# Mutiple lines
180-
(
181-
("str_1\nstr_2",),
182-
" ",
183-
"\n",
184-
"\x1b[34;47mstr_1\x1b[0m\n\x1b[34;47mstr_2\x1b[0m\n",
185-
),
186-
# Multiple strings
187-
(
188-
("str_1", "str_2"),
189-
" ",
190-
"\n",
191-
"\x1b[34;47mstr_1 str_2\x1b[0m\n",
192-
),
193-
# Multiple strings with newline between them.
194-
(
195-
("str_1\n", "str_2"),
196-
" ",
197-
"\n",
198-
"\x1b[34;47mstr_1\x1b[0m\n\x1b[34;47m str_2\x1b[0m\n",
199-
),
200-
# Multiple strings and non-space value for sep
201-
(
202-
("str_1", "str_2"),
203-
"(sep)",
204-
"\n",
205-
"\x1b[34;47mstr_1(sep)str_2\x1b[0m\n",
206-
),
207-
# Multiple strings and sep is a newline
208-
(
209-
("str_1", "str_2"),
210-
"\n",
211-
"\n",
212-
"\x1b[34;47mstr_1\x1b[0m\n\x1b[34;47mstr_2\x1b[0m\n",
213-
),
214-
# Multiple strings and sep has newlines
215-
(
216-
("str_1", "str_2"),
217-
"(sep1)\n(sep2)\n",
218-
"\n",
219-
("\x1b[34;47mstr_1(sep1)\x1b[0m\n\x1b[34;47m(sep2)\x1b[0m\n\x1b[34;47mstr_2\x1b[0m\n"),
220-
),
221-
# Non-newline value for end.
222-
(
223-
("str_1", "str_2"),
224-
"(sep1)\n(sep2)",
225-
"(end)",
226-
"\x1b[34;47mstr_1(sep1)\x1b[0m\n\x1b[34;47m(sep2)str_2\x1b[0m\x1b[34;47m(end)\x1b[0m",
227-
),
228-
# end has newlines.
229-
(
230-
("str_1", "str_2"),
231-
"(sep1)\n(sep2)\n",
232-
"(end1)\n(end2)\n",
233-
(
234-
"\x1b[34;47mstr_1(sep1)\x1b[0m\n"
235-
"\x1b[34;47m(sep2)\x1b[0m\n"
236-
"\x1b[34;47mstr_2\x1b[0m\x1b[34;47m(end1)\x1b[0m\n"
237-
"\x1b[34;47m(end2)\x1b[0m\n"
238-
),
239-
),
240-
# Empty sep and end values
241-
(
242-
("str_1", "str_2"),
243-
"",
244-
"",
245-
"\x1b[34;47mstr_1str_2\x1b[0m",
246-
),
247-
],
248-
)
249-
def test_apply_style_wrapper_soft_wrap(objects: tuple[str], sep: str, end: str, expected: str) -> None:
250-
# Check if we are still patching Segment.apply_style(). If this check fails, then Rich
251-
# has fixed the bug. Therefore, we can remove this test function and ru._apply_style_wrapper.
252-
assert Segment.apply_style.__func__ is ru._apply_style_wrapper.__func__ # type: ignore[attr-defined]
253-
254-
console = Console(force_terminal=True)
255-
256-
try:
257-
# Since our patch was meant to fix behavior seen when soft wrapping,
258-
# we will first test in that condition.
259-
with console.capture() as capture:
260-
console.print(*objects, sep=sep, end=end, style="blue on white", soft_wrap=True)
261-
result = capture.get()
262-
assert result == expected
263-
264-
# Now print with soft wrapping disabled. Since none of our input strings are long enough
265-
# to auto wrap, the results should be the same as our soft-wrapping output.
266-
with console.capture() as capture:
267-
console.print(*objects, sep=sep, end=end, style="blue on white", soft_wrap=False)
268-
result = capture.get()
269-
assert result == expected
270-
271-
# Now remove our patch and disable soft wrapping. This will prove that our patch produces
272-
# the same result as unpatched Rich
273-
Segment.apply_style = ru._orig_segment_apply_style # type: ignore[assignment]
274-
275-
with console.capture() as capture:
276-
console.print(*objects, sep=sep, end=end, style="blue on white", soft_wrap=False)
277-
result = capture.get()
278-
assert result == expected
279-
280-
finally:
281-
# Restore the patch
282-
Segment.apply_style = ru._apply_style_wrapper # type: ignore[assignment]
283-
284-
285-
def test_apply_style_wrapper_word_wrap() -> None:
286-
"""
287-
Test that our patch didn't mess up word wrapping.
288-
Make sure it does not insert styled newlines or apply style to existing newlines.
289-
"""
290-
# Check if we are still patching Segment.apply_style(). If this check fails, then Rich
291-
# has fixed the bug. Therefore, we can remove this test function and ru._apply_style_wrapper.
292-
assert Segment.apply_style.__func__ is ru._apply_style_wrapper.__func__ # type: ignore[attr-defined]
293-
294-
str1 = "this\nwill word wrap\n"
295-
str2 = "and\nso will this\n"
296-
sep = "(sep1)\n(sep2)\n"
297-
end = "(end1)\n(end2)\n"
298-
style = "blue on white"
299-
300-
# All newlines should appear outside of ANSI style sequences.
301-
expected = (
302-
"\x1b[34;47mthis\x1b[0m\n"
303-
"\x1b[34;47mwill word \x1b[0m\n"
304-
"\x1b[34;47mwrap\x1b[0m\n"
305-
"\x1b[34;47m(sep1)\x1b[0m\n"
306-
"\x1b[34;47m(sep2)\x1b[0m\n"
307-
"\x1b[34;47mand\x1b[0m\n"
308-
"\x1b[34;47mso will \x1b[0m\n"
309-
"\x1b[34;47mthis\x1b[0m\n"
310-
"\x1b[34;47m(end1)\x1b[0m\n"
311-
"\x1b[34;47m(end2)\x1b[0m\n"
312-
)
313-
314-
# Set a width which will cause word wrapping.
315-
console = Console(force_terminal=True, width=10)
316-
317-
try:
318-
with console.capture() as capture:
319-
console.print(str1, str2, sep=sep, end=end, style=style, soft_wrap=False)
320-
assert capture.get() == expected
321-
322-
# Now remove our patch and make sure it produced the same result as unpatched Rich.
323-
Segment.apply_style = ru._orig_segment_apply_style # type: ignore[assignment]
324-
325-
with console.capture() as capture:
326-
console.print(str1, str2, sep=sep, end=end, style=style, soft_wrap=False)
327-
assert capture.get() == expected
328-
329-
finally:
330-
# Restore the patch
331-
Segment.apply_style = ru._apply_style_wrapper # type: ignore[assignment]

0 commit comments

Comments
 (0)