From 3dfe0d9407a9f54142305e444eee52784e83be6c Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 25 Aug 2025 11:03:14 -0400 Subject: [PATCH 1/5] Updated rich_utils.prepare_objects_for_rich_print() to only convert styled strings to Rich Text objects. --- cmd2/rich_utils.py | 26 ++++++++++++++++++-------- tests/test_argparse.py | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index bdd1dbb96..ab5b19268 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -306,17 +306,27 @@ def indent(renderable: RenderableType, level: int) -> Padding: def prepare_objects_for_rich_print(*objects: Any) -> tuple[RenderableType, ...]: """Prepare a tuple of objects for printing by Rich's Console.print(). - Converts any non-Rich objects (i.e., not ConsoleRenderable or RichCast) - into rich.Text objects by stringifying them and processing them with - from_ansi(). This ensures Rich correctly interprets any embedded ANSI - escape sequences. + This function converts any non-Rich object whose string representation contains + ANSI style codes into a rich.Text object. This ensures correct display width + calculation, as Rich can then properly parse and account for the non-printing + ANSI codes. All other objects are left untouched, allowing Rich's native + renderers to handle them. :param objects: objects to prepare - :return: a tuple containing the processed objects, where non-Rich objects are - converted to rich.Text. + :return: a tuple containing the processed objects. """ object_list = list(objects) for i, obj in enumerate(object_list): - if not isinstance(obj, (ConsoleRenderable, RichCast)): - object_list[i] = string_to_rich_text(str(obj)) + # If the object is a recognized renderable, we don't need to do anything. Rich will handle it. + if isinstance(obj, (ConsoleRenderable, RichCast)): + continue + + # Check if the object's string representation contains ANSI styles, and if so, + # replace it with a Rich Text object for correct width calculation. + obj_str = str(obj) + obj_text = string_to_rich_text(obj_str) + + if obj_text.plain != obj_str: + object_list[i] = obj_text + return tuple(object_list) diff --git a/tests/test_argparse.py b/tests/test_argparse.py index afcae62ec..dd567434f 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -425,7 +425,7 @@ def test_subcmd_decorator(subcommand_app) -> None: # Test subcommand that has no help option out, err = run_cmd(subcommand_app, 'test_subcmd_decorator helpless_subcmd') - assert "'subcommand': 'helpless_subcmd'" in out[0] + assert "'subcommand': 'helpless_subcmd'" in out[1] out, err = run_cmd(subcommand_app, 'help test_subcmd_decorator helpless_subcmd') assert out[0] == 'Usage: test_subcmd_decorator helpless_subcmd' From f0060b742e0538ac4c1469669331fd733d2a303d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 25 Aug 2025 17:09:30 -0400 Subject: [PATCH 2/5] Corrected display width of styled strings in CompletionItem.descriptive_data. --- cmd2/argparse_custom.py | 11 ++++++++--- cmd2/cmd2.py | 20 ++++++++++++++------ cmd2/rich_utils.py | 25 +++++++++++++------------ examples/argparse_completion.py | 9 +++++++-- tests/test_cmd2.py | 31 +++++++++++++++++++++---------- 5 files changed, 63 insertions(+), 33 deletions(-) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index b0461659d..3d700a742 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -194,7 +194,7 @@ def get_items(self) -> list[CompletionItems]: truncated with an ellipsis at the end. You can override this and other settings when you create the ``Column``. -``descriptive_data`` items can include Rich objects, including styled text. +``descriptive_data`` items can include Rich objects, including styled Text and Tables. To avoid printing a excessive information to the screen at once when a user 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) - """CompletionItem Initializer. :param value: the value being tab completed - :param descriptive_data: descriptive data to display + :param descriptive_data: a list of descriptive data to display in the columns that follow + the completion value. The number of items in this list must equal + the number of descriptive headers defined for the argument. :param args: args for str __init__ """ super().__init__(*args) # Make sure all objects are renderable by a Rich table. - self.descriptive_data = [obj if is_renderable(obj) else str(obj) for obj in descriptive_data] + renderable_data = [obj if is_renderable(obj) else str(obj) for obj in descriptive_data] + + # Convert objects with ANSI styles to Rich Text for correct display width. + self.descriptive_data = ru.prepare_objects_for_rich_rendering(*renderable_data) # Save the original value to support CompletionItems as argparse choices. # cmd2 has patched argparse so input is compared to this value instead of the CompletionItem instance. diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index e6550bad8..50dc6580f 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1225,7 +1225,7 @@ def print_to( method and still call `super()` without encountering unexpected keyword argument errors. These arguments are not passed to Rich's Console.print(). """ - prepared_objects = ru.prepare_objects_for_rich_print(*objects) + prepared_objects = ru.prepare_objects_for_rich_rendering(*objects) try: Cmd2GeneralConsole(file).print( @@ -1469,7 +1469,7 @@ def ppaged( # Check if we are outputting to a pager. if functional_terminal and can_block: - prepared_objects = ru.prepare_objects_for_rich_print(*objects) + prepared_objects = ru.prepare_objects_for_rich_rendering(*objects) # Chopping overrides soft_wrap if chop: @@ -2508,7 +2508,11 @@ def _get_settable_completion_items(self) -> list[CompletionItem]: results: list[CompletionItem] = [] for cur_key in self.settables: - descriptive_data = [self.settables[cur_key].get_value(), self.settables[cur_key].description] + settable = self.settables[cur_key] + descriptive_data = [ + str(settable.get_value()), + settable.description, + ] results.append(CompletionItem(cur_key, descriptive_data)) return results @@ -4157,8 +4161,8 @@ def _print_documented_command_topics(self, header: str, cmds: list[str], verbose Column("Name", no_wrap=True), Column("Description", overflow="fold"), box=rich.box.SIMPLE_HEAD, - border_style=Cmd2Style.TABLE_BORDER, show_edge=False, + border_style=Cmd2Style.TABLE_BORDER, ) # Try to get the documentation string for each command @@ -4478,8 +4482,8 @@ def do_set(self, args: argparse.Namespace) -> None: Column("Value", overflow="fold"), Column("Description", overflow="fold"), box=rich.box.SIMPLE_HEAD, - border_style=Cmd2Style.TABLE_BORDER, show_edge=False, + border_style=Cmd2Style.TABLE_BORDER, ) # Build the table and populate self.last_result @@ -4487,7 +4491,11 @@ def do_set(self, args: argparse.Namespace) -> None: for param in sorted(to_show, key=self.default_sort_key): settable = self.settables[param] - settable_table.add_row(param, str(settable.get_value()), settable.description) + settable_table.add_row( + param, + str(settable.get_value()), + settable.description, + ) self.last_result[param] = settable.get_value() self.poutput() diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index ab5b19268..a43aa2555 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -14,9 +14,9 @@ JustifyMethod, OverflowMethod, RenderableType, - RichCast, ) from rich.padding import Padding +from rich.protocol import rich_cast from rich.style import StyleType from rich.table import ( Column, @@ -40,10 +40,6 @@ def __str__(self) -> str: """Return value instead of enum name for printing in cmd2's set command.""" return str(self.value) - def __repr__(self) -> str: - """Return quoted value instead of enum description for printing in cmd2's set command.""" - return repr(self.value) - # Controls when ANSI style sequences are allowed in output ALLOW_STYLE = AllowStyle.TERMINAL @@ -303,7 +299,7 @@ def indent(renderable: RenderableType, level: int) -> Padding: return Padding.indent(renderable, level) -def prepare_objects_for_rich_print(*objects: Any) -> tuple[RenderableType, ...]: +def prepare_objects_for_rich_rendering(*objects: Any) -> tuple[Any, ...]: """Prepare a tuple of objects for printing by Rich's Console.print(). 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, ...]: :return: a tuple containing the processed objects. """ object_list = list(objects) + for i, obj in enumerate(object_list): - # If the object is a recognized renderable, we don't need to do anything. Rich will handle it. - if isinstance(obj, (ConsoleRenderable, RichCast)): + # Resolve the object's final renderable form, including those + # with a __rich__ method that might return a string. + renderable = rich_cast(obj) + + # This object implements the Rich console protocol, so no preprocessing is needed. + if isinstance(renderable, ConsoleRenderable): continue # Check if the object's string representation contains ANSI styles, and if so, # replace it with a Rich Text object for correct width calculation. - obj_str = str(obj) - obj_text = string_to_rich_text(obj_str) + renderable_as_str = str(renderable) + renderable_as_text = string_to_rich_text(renderable_as_str) - if obj_text.plain != obj_str: - object_list[i] = obj_text + if renderable_as_text.plain != renderable_as_str: + object_list[i] = renderable_as_text return tuple(object_list) diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py index 90d2d1041..8d2c3dca1 100755 --- a/examples/argparse_completion.py +++ b/examples/argparse_completion.py @@ -3,7 +3,7 @@ import argparse -from rich.box import SIMPLE_HEAD +import rich.box from rich.style import Style from rich.table import Table from rich.text import Text @@ -49,7 +49,12 @@ def choices_completion_item(self) -> list[CompletionItem]: Text("styled text!!", style=Style(color=Color.BRIGHT_YELLOW, underline=True)), ) - table_item = Table("Left Column", "Right Column", box=SIMPLE_HEAD, border_style=Cmd2Style.TABLE_BORDER) + table_item = Table( + "Left Column", + "Right Column", + box=rich.box.ROUNDED, + border_style=Cmd2Style.TABLE_BORDER, + ) table_item.add_row("Yes, it's true.", "CompletionItems can") table_item.add_row("even display description", "data in tables!") diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index f03d5224a..d505481cb 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -2092,21 +2092,32 @@ def test_poutput_none(outsim_app) -> None: @with_ansi_style(ru.AllowStyle.ALWAYS) -def test_poutput_ansi_always(outsim_app) -> None: - msg = 'Hello World' - colored_msg = Text(msg, style="cyan") - outsim_app.poutput(colored_msg) +@pytest.mark.parametrize( + # Test a Rich Text and a string. + ('styled_msg', 'expected'), + [ + (Text("A Text object", style="cyan"), "\x1b[36mA Text object\x1b[0m\n"), + (su.stylize("A str object", style="blue"), "\x1b[34mA str object\x1b[0m\n"), + ], +) +def test_poutput_ansi_always(styled_msg, expected, outsim_app) -> None: + outsim_app.poutput(styled_msg) out = outsim_app.stdout.getvalue() - assert out == "\x1b[36mHello World\x1b[0m\n" + assert out == expected @with_ansi_style(ru.AllowStyle.NEVER) -def test_poutput_ansi_never(outsim_app) -> None: - msg = 'Hello World' - colored_msg = Text(msg, style="cyan") - outsim_app.poutput(colored_msg) +@pytest.mark.parametrize( + # Test a Rich Text and a string. + ('styled_msg', 'expected'), + [ + (Text("A Text object", style="cyan"), "A Text object\n"), + (su.stylize("A str object", style="blue"), "A str object\n"), + ], +) +def test_poutput_ansi_never(styled_msg, expected, outsim_app) -> None: + outsim_app.poutput(styled_msg) out = outsim_app.stdout.getvalue() - expected = msg + '\n' assert out == expected From 88e517c4f70471bd50486785634401511b96b8e0 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 25 Aug 2025 18:35:51 -0400 Subject: [PATCH 3/5] Added unit tests for printing. --- cmd2/__init__.py | 3 ++ cmd2/argparse_custom.py | 2 +- cmd2/cmd2.py | 4 +- cmd2/rich_utils.py | 2 +- tests/test_cmd2.py | 101 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 4 deletions(-) diff --git a/cmd2/__init__.py b/cmd2/__init__.py index e8aebdaf6..1313bc1a9 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -44,6 +44,7 @@ ) from .parsing import Statement from .py_bridge import CommandResult +from .rich_utils import RichPrintKwargs from .string_utils import stylize from .styles import Cmd2Style from .utils import ( @@ -86,6 +87,8 @@ 'plugin', 'rich_utils', 'string_utils', + # Rich Utils + 'RichPrintKwargs', # String Utils 'stylize', # Styles, diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 3d700a742..c99ca82a2 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -399,7 +399,7 @@ def __init__(self, value: object, descriptive_data: Sequence[Any], *args: Any) - renderable_data = [obj if is_renderable(obj) else str(obj) for obj in descriptive_data] # Convert objects with ANSI styles to Rich Text for correct display width. - self.descriptive_data = ru.prepare_objects_for_rich_rendering(*renderable_data) + self.descriptive_data = ru.prepare_objects_for_rendering(*renderable_data) # Save the original value to support CompletionItems as argparse choices. # cmd2 has patched argparse so input is compared to this value instead of the CompletionItem instance. diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 50dc6580f..48729bf49 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1225,7 +1225,7 @@ def print_to( method and still call `super()` without encountering unexpected keyword argument errors. These arguments are not passed to Rich's Console.print(). """ - prepared_objects = ru.prepare_objects_for_rich_rendering(*objects) + prepared_objects = ru.prepare_objects_for_rendering(*objects) try: Cmd2GeneralConsole(file).print( @@ -1469,7 +1469,7 @@ def ppaged( # Check if we are outputting to a pager. if functional_terminal and can_block: - prepared_objects = ru.prepare_objects_for_rich_rendering(*objects) + prepared_objects = ru.prepare_objects_for_rendering(*objects) # Chopping overrides soft_wrap if chop: diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index a43aa2555..3d3873174 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -299,7 +299,7 @@ def indent(renderable: RenderableType, level: int) -> Padding: return Padding.indent(renderable, level) -def prepare_objects_for_rich_rendering(*objects: Any) -> tuple[Any, ...]: +def prepare_objects_for_rendering(*objects: Any) -> tuple[Any, ...]: """Prepare a tuple of objects for printing by Rich's Console.print(). This function converts any non-Rich object whose string representation contains diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index d505481cb..5716602d3 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -20,6 +20,7 @@ COMMAND_NAME, Cmd2Style, Color, + RichPrintKwargs, clipboard, constants, exceptions, @@ -2133,6 +2134,106 @@ def test_poutput_ansi_terminal(outsim_app) -> None: assert out == expected +@with_ansi_style(ru.AllowStyle.ALWAYS) +def test_poutput_highlight(outsim_app): + rich_print_kwargs = RichPrintKwargs(highlight=True) + outsim_app.poutput( + "My IP Address is 192.168.1.100.", + rich_print_kwargs=rich_print_kwargs, + ) + out = outsim_app.stdout.getvalue() + assert out == "My IP Address is \x1b[1;92m192.168.1.100\x1b[0m.\n" + + +@with_ansi_style(ru.AllowStyle.ALWAYS) +def test_poutput_markup(outsim_app): + rich_print_kwargs = RichPrintKwargs(markup=True) + outsim_app.poutput( + "The leaves are [green]green[/green].", + rich_print_kwargs=rich_print_kwargs, + ) + out = outsim_app.stdout.getvalue() + assert out == "The leaves are \x1b[32mgreen\x1b[0m.\n" + + +@with_ansi_style(ru.AllowStyle.ALWAYS) +def test_poutput_emoji(outsim_app): + rich_print_kwargs = RichPrintKwargs(emoji=True) + outsim_app.poutput( + "Look at the emoji :1234:.", + rich_print_kwargs=rich_print_kwargs, + ) + out = outsim_app.stdout.getvalue() + assert out == "Look at the emoji 🔢.\n" + + +@with_ansi_style(ru.AllowStyle.ALWAYS) +def test_poutput_justify_and_width(outsim_app): + rich_print_kwargs = RichPrintKwargs(justify="right", width=10) + + # Use a styled-string when justifying to check if its display width is correct. + outsim_app.poutput( + su.stylize("Hello", style="blue"), + rich_print_kwargs=rich_print_kwargs, + ) + out = outsim_app.stdout.getvalue() + assert out == " \x1b[34mHello\x1b[0m\n" + + +@with_ansi_style(ru.AllowStyle.ALWAYS) +def test_poutput_no_wrap_and_overflow(outsim_app): + rich_print_kwargs = RichPrintKwargs(no_wrap=True, overflow="ellipsis", width=10) + + outsim_app.poutput( + "This is longer than width.", + soft_wrap=False, + rich_print_kwargs=rich_print_kwargs, + ) + out = outsim_app.stdout.getvalue() + assert out.startswith("This is l…\n") + + +@with_ansi_style(ru.AllowStyle.ALWAYS) +def test_poutput_pretty_print(outsim_app): + """Test that cmd2 passes objects through so they can be pretty-printed when highlighting is enabled.""" + rich_print_kwargs = RichPrintKwargs(highlight=True) + dictionary = {1: 'hello', 2: 'person', 3: 'who', 4: 'codes'} + + outsim_app.poutput( + dictionary, + rich_print_kwargs=rich_print_kwargs, + ) + out = outsim_app.stdout.getvalue() + assert out.startswith("\x1b[1m{\x1b[0m\x1b[1;36m1\x1b[0m: \x1b[32m'hello'\x1b[0m") + + +@with_ansi_style(ru.AllowStyle.ALWAYS) +def test_poutput_all_keyword_args(outsim_app): + """Test that all fields in RichPrintKwargs are recognized by Rich's Console.print().""" + rich_print_kwargs = RichPrintKwargs( + justify="center", + overflow="ellipsis", + no_wrap=True, + markup=True, + emoji=True, + highlight=True, + width=40, + height=50, + crop=False, + new_line_start=True, + ) + + outsim_app.poutput( + "My string", + rich_print_kwargs=rich_print_kwargs, + ) + + # Verify that something printed which means Console.print() didn't + # raise a TypeError for an unexpected keyword argument. + out = outsim_app.stdout.getvalue() + assert "My string" in out + + def test_broken_pipe_error(outsim_app, monkeypatch, capsys): write_mock = mock.MagicMock() write_mock.side_effect = BrokenPipeError From abb2c5fb3c5c047e93187e2b869702f2fada9f07 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 25 Aug 2025 19:20:14 -0400 Subject: [PATCH 4/5] Added unit tests for CompletionItems. --- tests/conftest.py | 28 +++++++++++++---- tests/test_argparse_completer.py | 52 +++++++++++++++++++------------- tests/test_cmd2.py | 20 +----------- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 40ab9abc3..814d9a48c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,12 +10,9 @@ import pytest import cmd2 -from cmd2.rl_utils import ( - readline, -) -from cmd2.utils import ( - StdSim, -) +from cmd2 import rich_utils as ru +from cmd2.rl_utils import readline +from cmd2.utils import StdSim def verify_help_text(cmd2_app: cmd2.Cmd, help_output: str | list[str], verbose_strings: list[str] | None = None) -> None: @@ -88,6 +85,25 @@ def base_app(): return cmd2.Cmd(include_py=True, include_ipy=True) +def with_ansi_style(style): + def arg_decorator(func): + import functools + + @functools.wraps(func) + def cmd_wrapper(*args, **kwargs): + old = ru.ALLOW_STYLE + ru.ALLOW_STYLE = style + try: + retval = func(*args, **kwargs) + finally: + ru.ALLOW_STYLE = old + return retval + + return cmd_wrapper + + return arg_decorator + + # These are odd file names for testing quoting of them odd_file_names = ['nothingweird', 'has spaces', '"is_double_quoted"', "'is_single_quoted'"] diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index 27c965987..38f84a73d 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -7,6 +7,7 @@ import pytest import cmd2 +import cmd2.string_utils as su from cmd2 import ( Cmd2ArgumentParser, CompletionError, @@ -15,11 +16,13 @@ argparse_custom, with_argparser, ) +from cmd2 import rich_utils as ru from .conftest import ( complete_tester, normalize, run_cmd, + with_ansi_style, ) # Data and functions for testing standalone choice_provider and completer @@ -109,12 +112,18 @@ def do_pos_and_flag(self, args: argparse.Namespace) -> None: static_choices_list = ('static', 'choices', 'stop', 'here') choices_from_provider = ('choices', 'provider', 'probably', 'improved') completion_item_choices = ( - CompletionItem('choice_1', ['A description']), - CompletionItem('choice_2', ['Another description']), + CompletionItem('choice_1', ['Description 1']), + # Make this the longest description so we can test display width. + CompletionItem('choice_2', [su.stylize("String with style", style=cmd2.Color.BLUE)]), + CompletionItem('choice_3', [su.stylize("Text with style", style=cmd2.Color.RED)]), ) # This tests that CompletionItems created with numerical values are sorted as numbers. - num_completion_items = (CompletionItem(5, ["Five"]), CompletionItem(1.5, ["One.Five"]), CompletionItem(2, ["Five"])) + num_completion_items = ( + CompletionItem(5, ["Five"]), + CompletionItem(1.5, ["One.Five"]), + CompletionItem(2, ["Five"]), + ) def choices_provider(self) -> tuple[str]: """Method that provides choices""" @@ -704,6 +713,7 @@ def test_autocomp_blank_token(ac_app) -> None: assert sorted(completions) == sorted(ArgparseCompleterTester.completions_for_pos_2) +@with_ansi_style(ru.AllowStyle.ALWAYS) def test_completion_items(ac_app) -> None: # First test CompletionItems created from strings text = '' @@ -716,16 +726,20 @@ def test_completion_items(ac_app) -> None: assert len(ac_app.completion_matches) == len(ac_app.completion_item_choices) assert len(ac_app.display_matches) == len(ac_app.completion_item_choices) - # Look for both the value and description in the hint table - line_found = False - for line in ac_app.formatted_completions.splitlines(): - # Since the CompletionItems were created from strings, the left-most column is left-aligned. - # Therefore choice_1 will begin the line (with 1 space for padding). - if line.startswith(' choice_1') and 'A description' in line: - line_found = True - break + lines = ac_app.formatted_completions.splitlines() + + # Since the CompletionItems were created from strings, the left-most column is left-aligned. + # Therefore choice_1 will begin the line (with 1 space for padding). + assert lines[2].startswith(' choice_1') + assert lines[2].strip().endswith('Description 1') + + # Verify that the styled string was converted to a Rich Text object so that + # Rich could correctly calculate its display width. Since it was the longest + # description in the table, we should only see one space of padding after it. + assert lines[3].endswith("\x1b[34mString with style\x1b[0m ") - assert line_found + # Verify that the styled Rich Text also rendered. + assert lines[4].endswith("\x1b[31mText with style\x1b[0m ") # Now test CompletionItems created from numbers text = '' @@ -738,16 +752,12 @@ def test_completion_items(ac_app) -> None: assert len(ac_app.completion_matches) == len(ac_app.num_completion_items) assert len(ac_app.display_matches) == len(ac_app.num_completion_items) - # Look for both the value and description in the hint table - line_found = False - for line in ac_app.formatted_completions.splitlines(): - # Since the CompletionItems were created from numbers, the left-most column is right-aligned. - # Therefore 1.5 will be right-aligned. - if line.startswith(" 1.5") and "One.Five" in line: - line_found = True - break + lines = ac_app.formatted_completions.splitlines() - assert line_found + # Since the CompletionItems were created from numbers, the left-most column is right-aligned. + # Therefore 1.5 will be right-aligned. + assert lines[2].startswith(" 1.5") + assert lines[2].strip().endswith('One.Five') @pytest.mark.parametrize( diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 5716602d3..295ae7207 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -41,28 +41,10 @@ odd_file_names, run_cmd, verify_help_text, + with_ansi_style, ) -def with_ansi_style(style): - def arg_decorator(func): - import functools - - @functools.wraps(func) - def cmd_wrapper(*args, **kwargs): - old = ru.ALLOW_STYLE - ru.ALLOW_STYLE = style - try: - retval = func(*args, **kwargs) - finally: - ru.ALLOW_STYLE = old - return retval - - return cmd_wrapper - - return arg_decorator - - def create_outsim_app(): c = cmd2.Cmd() c.stdout = utils.StdSim(c.stdout) From 93898d593aa6298de906380c2eab5427347298b4 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 25 Aug 2025 21:55:20 -0400 Subject: [PATCH 5/5] Simplified iterating through some dictionaries. --- cmd2/cmd2.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 48729bf49..c082ac3c6 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2487,9 +2487,9 @@ def _get_alias_completion_items(self) -> list[CompletionItem]: """Return list of alias names and values as CompletionItems.""" results: list[CompletionItem] = [] - for cur_key in self.aliases: - descriptive_data = [self.aliases[cur_key]] - results.append(CompletionItem(cur_key, descriptive_data)) + for name, value in self.aliases.items(): + descriptive_data = [value] + results.append(CompletionItem(name, descriptive_data)) return results @@ -2497,9 +2497,9 @@ def _get_macro_completion_items(self) -> list[CompletionItem]: """Return list of macro names and values as CompletionItems.""" results: list[CompletionItem] = [] - for cur_key in self.macros: - descriptive_data = [self.macros[cur_key].value] - results.append(CompletionItem(cur_key, descriptive_data)) + for name, macro in self.macros.items(): + descriptive_data = [macro.value] + results.append(CompletionItem(name, descriptive_data)) return results @@ -2507,13 +2507,12 @@ def _get_settable_completion_items(self) -> list[CompletionItem]: """Return list of Settable names, values, and descriptions as CompletionItems.""" results: list[CompletionItem] = [] - for cur_key in self.settables: - settable = self.settables[cur_key] + for name, settable in self.settables.items(): descriptive_data = [ str(settable.get_value()), settable.description, ] - results.append(CompletionItem(cur_key, descriptive_data)) + results.append(CompletionItem(name, descriptive_data)) return results