Skip to content

Commit d5642c3

Browse files
authored
Removed need for Rich patches in Cmd2BaseConsole.print() and Cmd2BaseConsole.log(). (#1609)
1 parent e01abb9 commit d5642c3

File tree

3 files changed

+98
-141
lines changed

3 files changed

+98
-141
lines changed

cmd2/cmd2.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,27 @@ def visible_prompt(self) -> str:
13151315
"""
13161316
return su.strip_style(self.prompt)
13171317

1318+
def _create_base_printing_console(
1319+
self,
1320+
file: IO[str],
1321+
emoji: bool,
1322+
markup: bool,
1323+
highlight: bool,
1324+
) -> Cmd2BaseConsole:
1325+
"""Create a Cmd2BaseConsole with formatting overrides.
1326+
1327+
This works around a bug in Rich where complex renderables (like Table and Rule)
1328+
may not receive formatting settings passed directly to print() or log(). Passing
1329+
them to the constructor instead ensures they are correctly propagated.
1330+
See: https://github.com/Textualize/rich/issues/4028
1331+
"""
1332+
return Cmd2BaseConsole(
1333+
file=file,
1334+
emoji=emoji,
1335+
markup=markup,
1336+
highlight=highlight,
1337+
)
1338+
13181339
def print_to(
13191340
self,
13201341
file: IO[str],
@@ -1364,15 +1385,17 @@ def print_to(
13641385
See the Rich documentation for more details on emoji codes, markup tags, and highlighting.
13651386
"""
13661387
try:
1367-
Cmd2BaseConsole(file=file).print(
1388+
self._create_base_printing_console(
1389+
file=file,
1390+
emoji=emoji,
1391+
markup=markup,
1392+
highlight=highlight,
1393+
).print(
13681394
*objects,
13691395
sep=sep,
13701396
end=end,
13711397
style=style,
13721398
soft_wrap=soft_wrap,
1373-
emoji=emoji,
1374-
markup=markup,
1375-
highlight=highlight,
13761399
**(rich_print_kwargs if rich_print_kwargs is not None else {}),
13771400
)
13781401
except BrokenPipeError:
@@ -1665,17 +1688,19 @@ def ppaged(
16651688
soft_wrap = True
16661689

16671690
# Generate the bytes to send to the pager
1668-
console = Cmd2BaseConsole(file=self.stdout)
1691+
console = self._create_base_printing_console(
1692+
file=self.stdout,
1693+
emoji=emoji,
1694+
markup=markup,
1695+
highlight=highlight,
1696+
)
16691697
with console.capture() as capture:
16701698
console.print(
16711699
*objects,
16721700
sep=sep,
16731701
end=end,
16741702
style=style,
16751703
soft_wrap=soft_wrap,
1676-
emoji=emoji,
1677-
markup=markup,
1678-
highlight=highlight,
16791704
**(rich_print_kwargs if rich_print_kwargs is not None else {}),
16801705
)
16811706
output_bytes = capture.get().encode('utf-8', 'replace')

cmd2/rich_utils.py

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

33
import re
4-
import threading
54
from collections.abc import Mapping
65
from enum import Enum
76
from typing import (
@@ -178,31 +177,12 @@ def __init__(
178177
theme=APP_THEME,
179178
**kwargs,
180179
)
181-
self._thread_local = threading.local()
182180

183181
def on_broken_pipe(self) -> None:
184182
"""Override which raises BrokenPipeError instead of SystemExit."""
185183
self.quiet = True
186184
raise BrokenPipeError
187185

188-
def render_str(
189-
self,
190-
text: str,
191-
highlight: bool | None = None,
192-
markup: bool | None = None,
193-
emoji: bool | None = None,
194-
**kwargs: Any,
195-
) -> Text:
196-
"""Override to ensure formatting overrides passed to print() and log() are respected."""
197-
if emoji is None:
198-
emoji = getattr(self._thread_local, "emoji", None)
199-
if markup is None:
200-
markup = getattr(self._thread_local, "markup", None)
201-
if highlight is None:
202-
highlight = getattr(self._thread_local, "highlight", None)
203-
204-
return super().render_str(text, highlight=highlight, markup=markup, emoji=emoji, **kwargs)
205-
206186
def print(
207187
self,
208188
*objects: Any,
@@ -221,52 +201,32 @@ def print(
221201
soft_wrap: bool | None = None,
222202
new_line_start: bool = False,
223203
) -> None:
224-
"""Override to support ANSI sequences and address a bug in Rich.
204+
"""Override to support ANSI sequences.
225205
226206
This method calls [cmd2.rich_utils.prepare_objects_for_rendering][] on the
227207
objects being printed. This ensures that strings containing ANSI style
228208
sequences are converted to Rich Text objects, so that Rich can correctly
229209
calculate their display width.
230-
231-
Additionally, it works around a bug in Rich where complex renderables
232-
(like Table and Rule) may not receive formatting settings passed to print().
233-
By temporarily injecting these settings into thread-local storage, we ensure
234-
that all internal rendering calls within the print() operation respect the
235-
requested overrides.
236-
237-
There is an issue on Rich to fix the latter:
238-
https://github.com/Textualize/rich/issues/4028
239210
"""
240211
prepared_objects = prepare_objects_for_rendering(*objects)
241212

242-
# Inject overrides into thread-local storage
243-
self._thread_local.emoji = emoji
244-
self._thread_local.markup = markup
245-
self._thread_local.highlight = highlight
246-
247-
try:
248-
super().print(
249-
*prepared_objects,
250-
sep=sep,
251-
end=end,
252-
style=style,
253-
justify=justify,
254-
overflow=overflow,
255-
no_wrap=no_wrap,
256-
emoji=emoji,
257-
markup=markup,
258-
highlight=highlight,
259-
width=width,
260-
height=height,
261-
crop=crop,
262-
soft_wrap=soft_wrap,
263-
new_line_start=new_line_start,
264-
)
265-
finally:
266-
# Clear overrides from thread-local storage
267-
self._thread_local.emoji = None
268-
self._thread_local.markup = None
269-
self._thread_local.highlight = None
213+
super().print(
214+
*prepared_objects,
215+
sep=sep,
216+
end=end,
217+
style=style,
218+
justify=justify,
219+
overflow=overflow,
220+
no_wrap=no_wrap,
221+
emoji=emoji,
222+
markup=markup,
223+
highlight=highlight,
224+
width=width,
225+
height=height,
226+
crop=crop,
227+
soft_wrap=soft_wrap,
228+
new_line_start=new_line_start,
229+
)
270230

271231
def log(
272232
self,
@@ -281,56 +241,35 @@ def log(
281241
log_locals: bool = False,
282242
_stack_offset: int = 1,
283243
) -> None:
284-
"""Override to support ANSI sequences and address a bug in Rich.
244+
"""Override to support ANSI sequences.
285245
286246
This method calls [cmd2.rich_utils.prepare_objects_for_rendering][] on the
287247
objects being logged. This ensures that strings containing ANSI style
288248
sequences are converted to Rich Text objects, so that Rich can correctly
289249
calculate their display width.
290-
291-
Additionally, it works around a bug in Rich where complex renderables
292-
(like Table and Rule) may not receive formatting settings passed to log().
293-
By temporarily injecting these settings into thread-local storage, we ensure
294-
that all internal rendering calls within the log() operation respect the
295-
requested overrides.
296-
297-
There is an issue on Rich to fix the latter:
298-
https://github.com/Textualize/rich/issues/4028
299250
"""
300251
prepared_objects = prepare_objects_for_rendering(*objects)
301252

302-
# Inject overrides into thread-local storage
303-
self._thread_local.emoji = emoji
304-
self._thread_local.markup = markup
305-
self._thread_local.highlight = highlight
306-
307-
try:
308-
# Increment _stack_offset because we added this wrapper frame
309-
super().log(
310-
*prepared_objects,
311-
sep=sep,
312-
end=end,
313-
style=style,
314-
justify=justify,
315-
emoji=emoji,
316-
markup=markup,
317-
highlight=highlight,
318-
log_locals=log_locals,
319-
_stack_offset=_stack_offset + 1,
320-
)
321-
finally:
322-
# Clear overrides from thread-local storage
323-
self._thread_local.emoji = None
324-
self._thread_local.markup = None
325-
self._thread_local.highlight = None
253+
# Increment _stack_offset because we added this wrapper frame
254+
super().log(
255+
*prepared_objects,
256+
sep=sep,
257+
end=end,
258+
style=style,
259+
justify=justify,
260+
emoji=emoji,
261+
markup=markup,
262+
highlight=highlight,
263+
log_locals=log_locals,
264+
_stack_offset=_stack_offset + 1,
265+
)
326266

327267

328268
class Cmd2GeneralConsole(Cmd2BaseConsole):
329269
"""Rich console for general-purpose printing.
330270
331-
It enables soft wrap and disables Rich's automatic detection for markup,
332-
emoji, and highlighting. These defaults can be overridden in calls to the
333-
console's or cmd2's print methods.
271+
It enables soft wrap and disables Rich's automatic detection
272+
for markup, emoji, and highlighting.
334273
"""
335274

336275
def __init__(self, *, file: IO[str] | None = None) -> None:

tests/test_rich_utils.py

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
import rich.box
5+
from pytest_mock import MockerFixture
56
from rich.console import Console
67
from rich.style import Style
78
from rich.table import Table
@@ -13,8 +14,6 @@
1314
)
1415
from cmd2 import rich_utils as ru
1516

16-
from .conftest import with_ansi_style
17-
1817

1918
def test_cmd2_base_console() -> None:
2019
# Test the keyword arguments which are not allowed.
@@ -152,49 +151,43 @@ def test_from_ansi_wrapper() -> None:
152151
assert Text.from_ansi(input_string).plain == input_string
153152

154153

155-
@with_ansi_style(ru.AllowStyle.ALWAYS)
156-
def test_cmd2_base_console_print() -> None:
157-
"""Test that Cmd2BaseConsole.print() correctly propagates formatting overrides to structured renderables."""
158-
from rich.rule import Rule
159-
160-
# Create a console that defaults to no formatting
161-
console = ru.Cmd2BaseConsole(emoji=False, markup=False)
162-
163-
# Use a Rule with emoji and markup in the title
164-
rule = Rule(title="[green]Success :1234:[/green]")
154+
def test_cmd2_base_console_print(mocker: MockerFixture) -> None:
155+
"""Test that Cmd2BaseConsole.print() calls prepare_objects_for_rendering()."""
156+
# Mock prepare_objects_for_rendering to return a specific value
157+
prepared_val = ("prepared",)
158+
mock_prepare = mocker.patch("cmd2.rich_utils.prepare_objects_for_rendering", return_value=prepared_val)
165159

166-
with console.capture() as capture:
167-
# Override settings in the print() call
168-
console.print(rule, emoji=True, markup=True)
169-
170-
result = capture.get()
160+
# Mock the superclass print() method
161+
mock_super_print = mocker.patch("rich.console.Console.print")
171162

172-
# Verify that the overrides were respected by checking for the emoji and the color code
173-
assert "🔢" in result
174-
assert "\x1b[32mSuccess" in result
163+
console = ru.Cmd2BaseConsole()
164+
console.print("hello")
175165

166+
# Verify that prepare_objects_for_rendering() was called with the input objects
167+
mock_prepare.assert_called_once_with("hello")
176168

177-
@with_ansi_style(ru.AllowStyle.ALWAYS)
178-
def test_cmd2_base_console_log() -> None:
179-
"""Test that Cmd2BaseConsole.log() correctly propagates formatting overrides to structured renderables."""
180-
from rich.rule import Rule
169+
# Verify that the superclass print() method was called with the prepared objects
170+
args, _ = mock_super_print.call_args
171+
assert args == prepared_val
181172

182-
# Create a console that defaults to no formatting
183-
console = ru.Cmd2BaseConsole(emoji=False, markup=False)
184173

185-
# Use a Rule with emoji and markup in the title
186-
rule = Rule(title="[green]Success :1234:[/green]")
174+
def test_cmd2_base_console_log(mocker: MockerFixture) -> None:
175+
"""Test that Cmd2BaseConsole.log() calls prepare_objects_for_rendering() and increments _stack_offset."""
176+
# Mock prepare_objects_for_rendering to return a specific value
177+
prepared_val = ("prepared",)
178+
mock_prepare = mocker.patch("cmd2.rich_utils.prepare_objects_for_rendering", return_value=prepared_val)
187179

188-
with console.capture() as capture:
189-
# Override settings in the log() call
190-
console.log(rule, emoji=True, markup=True)
180+
# Mock the superclass log() method
181+
mock_super_log = mocker.patch("rich.console.Console.log")
191182

192-
result = capture.get()
183+
console = ru.Cmd2BaseConsole()
184+
console.log("test", _stack_offset=2)
193185

194-
# Verify that the formatting overrides were respected
195-
assert "🔢" in result
196-
assert "\x1b[32mSuccess" in result
186+
# Verify that prepare_objects_for_rendering() was called with the input objects
187+
mock_prepare.assert_called_once_with("test")
197188

198-
# Verify stack offset: the log line should point to this file, not rich_utils.py
199-
# Rich logs include the filename and line number on the right.
200-
assert "test_rich_utils.py" in result
189+
# Verify that the superclass log() method was called with the prepared objects
190+
# and that the stack offset was correctly incremented.
191+
args, kwargs = mock_super_log.call_args
192+
assert args == prepared_val
193+
assert kwargs["_stack_offset"] == 3

0 commit comments

Comments
 (0)