Skip to content

Commit f0060b7

Browse files
committed
Corrected display width of styled strings in CompletionItem.descriptive_data.
1 parent 3dfe0d9 commit f0060b7

File tree

5 files changed

+63
-33
lines changed

5 files changed

+63
-33
lines changed

cmd2/argparse_custom.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def get_items(self) -> list[CompletionItems]:
194194
truncated with an ellipsis at the end. You can override this and other settings
195195
when you create the ``Column``.
196196
197-
``descriptive_data`` items can include Rich objects, including styled text.
197+
``descriptive_data`` items can include Rich objects, including styled Text and Tables.
198198
199199
To avoid printing a excessive information to the screen at once when a user
200200
presses tab, there is a maximum threshold for the number of CompletionItems
@@ -388,13 +388,18 @@ def __init__(self, value: object, descriptive_data: Sequence[Any], *args: Any) -
388388
"""CompletionItem Initializer.
389389
390390
:param value: the value being tab completed
391-
:param descriptive_data: descriptive data to display
391+
:param descriptive_data: a list of descriptive data to display in the columns that follow
392+
the completion value. The number of items in this list must equal
393+
the number of descriptive headers defined for the argument.
392394
:param args: args for str __init__
393395
"""
394396
super().__init__(*args)
395397

396398
# Make sure all objects are renderable by a Rich table.
397-
self.descriptive_data = [obj if is_renderable(obj) else str(obj) for obj in descriptive_data]
399+
renderable_data = [obj if is_renderable(obj) else str(obj) for obj in descriptive_data]
400+
401+
# Convert objects with ANSI styles to Rich Text for correct display width.
402+
self.descriptive_data = ru.prepare_objects_for_rich_rendering(*renderable_data)
398403

399404
# Save the original value to support CompletionItems as argparse choices.
400405
# cmd2 has patched argparse so input is compared to this value instead of the CompletionItem instance.

cmd2/cmd2.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,7 +1225,7 @@ def print_to(
12251225
method and still call `super()` without encountering unexpected keyword argument errors.
12261226
These arguments are not passed to Rich's Console.print().
12271227
"""
1228-
prepared_objects = ru.prepare_objects_for_rich_print(*objects)
1228+
prepared_objects = ru.prepare_objects_for_rich_rendering(*objects)
12291229

12301230
try:
12311231
Cmd2GeneralConsole(file).print(
@@ -1469,7 +1469,7 @@ def ppaged(
14691469

14701470
# Check if we are outputting to a pager.
14711471
if functional_terminal and can_block:
1472-
prepared_objects = ru.prepare_objects_for_rich_print(*objects)
1472+
prepared_objects = ru.prepare_objects_for_rich_rendering(*objects)
14731473

14741474
# Chopping overrides soft_wrap
14751475
if chop:
@@ -2508,7 +2508,11 @@ def _get_settable_completion_items(self) -> list[CompletionItem]:
25082508
results: list[CompletionItem] = []
25092509

25102510
for cur_key in self.settables:
2511-
descriptive_data = [self.settables[cur_key].get_value(), self.settables[cur_key].description]
2511+
settable = self.settables[cur_key]
2512+
descriptive_data = [
2513+
str(settable.get_value()),
2514+
settable.description,
2515+
]
25122516
results.append(CompletionItem(cur_key, descriptive_data))
25132517

25142518
return results
@@ -4157,8 +4161,8 @@ def _print_documented_command_topics(self, header: str, cmds: list[str], verbose
41574161
Column("Name", no_wrap=True),
41584162
Column("Description", overflow="fold"),
41594163
box=rich.box.SIMPLE_HEAD,
4160-
border_style=Cmd2Style.TABLE_BORDER,
41614164
show_edge=False,
4165+
border_style=Cmd2Style.TABLE_BORDER,
41624166
)
41634167

41644168
# Try to get the documentation string for each command
@@ -4478,16 +4482,20 @@ def do_set(self, args: argparse.Namespace) -> None:
44784482
Column("Value", overflow="fold"),
44794483
Column("Description", overflow="fold"),
44804484
box=rich.box.SIMPLE_HEAD,
4481-
border_style=Cmd2Style.TABLE_BORDER,
44824485
show_edge=False,
4486+
border_style=Cmd2Style.TABLE_BORDER,
44834487
)
44844488

44854489
# Build the table and populate self.last_result
44864490
self.last_result = {} # dict[settable_name, settable_value]
44874491

44884492
for param in sorted(to_show, key=self.default_sort_key):
44894493
settable = self.settables[param]
4490-
settable_table.add_row(param, str(settable.get_value()), settable.description)
4494+
settable_table.add_row(
4495+
param,
4496+
str(settable.get_value()),
4497+
settable.description,
4498+
)
44914499
self.last_result[param] = settable.get_value()
44924500

44934501
self.poutput()

cmd2/rich_utils.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
JustifyMethod,
1515
OverflowMethod,
1616
RenderableType,
17-
RichCast,
1817
)
1918
from rich.padding import Padding
19+
from rich.protocol import rich_cast
2020
from rich.style import StyleType
2121
from rich.table import (
2222
Column,
@@ -40,10 +40,6 @@ def __str__(self) -> str:
4040
"""Return value instead of enum name for printing in cmd2's set command."""
4141
return str(self.value)
4242

43-
def __repr__(self) -> str:
44-
"""Return quoted value instead of enum description for printing in cmd2's set command."""
45-
return repr(self.value)
46-
4743

4844
# Controls when ANSI style sequences are allowed in output
4945
ALLOW_STYLE = AllowStyle.TERMINAL
@@ -303,7 +299,7 @@ def indent(renderable: RenderableType, level: int) -> Padding:
303299
return Padding.indent(renderable, level)
304300

305301

306-
def prepare_objects_for_rich_print(*objects: Any) -> tuple[RenderableType, ...]:
302+
def prepare_objects_for_rich_rendering(*objects: Any) -> tuple[Any, ...]:
307303
"""Prepare a tuple of objects for printing by Rich's Console.print().
308304
309305
This function converts any non-Rich object whose string representation contains
@@ -316,17 +312,22 @@ def prepare_objects_for_rich_print(*objects: Any) -> tuple[RenderableType, ...]:
316312
:return: a tuple containing the processed objects.
317313
"""
318314
object_list = list(objects)
315+
319316
for i, obj in enumerate(object_list):
320-
# If the object is a recognized renderable, we don't need to do anything. Rich will handle it.
321-
if isinstance(obj, (ConsoleRenderable, RichCast)):
317+
# Resolve the object's final renderable form, including those
318+
# with a __rich__ method that might return a string.
319+
renderable = rich_cast(obj)
320+
321+
# This object implements the Rich console protocol, so no preprocessing is needed.
322+
if isinstance(renderable, ConsoleRenderable):
322323
continue
323324

324325
# Check if the object's string representation contains ANSI styles, and if so,
325326
# replace it with a Rich Text object for correct width calculation.
326-
obj_str = str(obj)
327-
obj_text = string_to_rich_text(obj_str)
327+
renderable_as_str = str(renderable)
328+
renderable_as_text = string_to_rich_text(renderable_as_str)
328329

329-
if obj_text.plain != obj_str:
330-
object_list[i] = obj_text
330+
if renderable_as_text.plain != renderable_as_str:
331+
object_list[i] = renderable_as_text
331332

332333
return tuple(object_list)

examples/argparse_completion.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import argparse
55

6-
from rich.box import SIMPLE_HEAD
6+
import rich.box
77
from rich.style import Style
88
from rich.table import Table
99
from rich.text import Text
@@ -49,7 +49,12 @@ def choices_completion_item(self) -> list[CompletionItem]:
4949
Text("styled text!!", style=Style(color=Color.BRIGHT_YELLOW, underline=True)),
5050
)
5151

52-
table_item = Table("Left Column", "Right Column", box=SIMPLE_HEAD, border_style=Cmd2Style.TABLE_BORDER)
52+
table_item = Table(
53+
"Left Column",
54+
"Right Column",
55+
box=rich.box.ROUNDED,
56+
border_style=Cmd2Style.TABLE_BORDER,
57+
)
5358
table_item.add_row("Yes, it's true.", "CompletionItems can")
5459
table_item.add_row("even display description", "data in tables!")
5560

tests/test_cmd2.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2092,21 +2092,32 @@ def test_poutput_none(outsim_app) -> None:
20922092

20932093

20942094
@with_ansi_style(ru.AllowStyle.ALWAYS)
2095-
def test_poutput_ansi_always(outsim_app) -> None:
2096-
msg = 'Hello World'
2097-
colored_msg = Text(msg, style="cyan")
2098-
outsim_app.poutput(colored_msg)
2095+
@pytest.mark.parametrize(
2096+
# Test a Rich Text and a string.
2097+
('styled_msg', 'expected'),
2098+
[
2099+
(Text("A Text object", style="cyan"), "\x1b[36mA Text object\x1b[0m\n"),
2100+
(su.stylize("A str object", style="blue"), "\x1b[34mA str object\x1b[0m\n"),
2101+
],
2102+
)
2103+
def test_poutput_ansi_always(styled_msg, expected, outsim_app) -> None:
2104+
outsim_app.poutput(styled_msg)
20992105
out = outsim_app.stdout.getvalue()
2100-
assert out == "\x1b[36mHello World\x1b[0m\n"
2106+
assert out == expected
21012107

21022108

21032109
@with_ansi_style(ru.AllowStyle.NEVER)
2104-
def test_poutput_ansi_never(outsim_app) -> None:
2105-
msg = 'Hello World'
2106-
colored_msg = Text(msg, style="cyan")
2107-
outsim_app.poutput(colored_msg)
2110+
@pytest.mark.parametrize(
2111+
# Test a Rich Text and a string.
2112+
('styled_msg', 'expected'),
2113+
[
2114+
(Text("A Text object", style="cyan"), "A Text object\n"),
2115+
(su.stylize("A str object", style="blue"), "A str object\n"),
2116+
],
2117+
)
2118+
def test_poutput_ansi_never(styled_msg, expected, outsim_app) -> None:
2119+
outsim_app.poutput(styled_msg)
21082120
out = outsim_app.stdout.getvalue()
2109-
expected = msg + '\n'
21102121
assert out == expected
21112122

21122123

0 commit comments

Comments
 (0)