From 1280bb9bf959ee4895a68ef555fcf1c243f892b8 Mon Sep 17 00:00:00 2001 From: neoniobium Date: Tue, 14 Jul 2026 19:05:27 +0200 Subject: [PATCH 1/3] [Settable] description argument can now be rich text --- CHANGELOG.md | 5 +++++ cmd2/cmd2.py | 6 +++--- cmd2/utils.py | 10 +++++++--- tests/test_cmd2.py | 1 - 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6600db7e3..f84f62d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## (TBA) + +- Enhancements + - Added possibility to use rich Text objects to set the description argument of a `Settanble` + ## 4.1.1 (July 9, 2026) - Bug Fixes diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d6adbfbd8..c8653d4e1 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1382,7 +1382,7 @@ def allow_style_type(value: str) -> ru.AllowStyle: Settable( "allow_style", allow_style_type, - ru.rich_text_to_string(settable_description), + settable_description, self, choices_provider=get_allow_style_choices, ) @@ -1399,7 +1399,7 @@ def allow_style_type(value: str) -> ru.AllowStyle: Settable( "editor", str, - ru.rich_text_to_string(editor_description), + editor_description, self, ) ) @@ -1434,7 +1434,7 @@ def allow_style_type(value: str) -> ru.AllowStyle: Settable( "traceback_width", utils.optional_int, - ru.rich_text_to_string(traceback_width_description), + traceback_width_description, self, ) ) diff --git a/cmd2/utils.py b/cmd2/utils.py index 250f353f9..a1a089e93 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -25,7 +25,10 @@ cast, ) +from rich.text import Text + from . import constants +from . import rich_utils as ru from . import string_utils as su from .types import ( CmdOrSet, @@ -88,7 +91,7 @@ def __init__( self, name: str, val_type: type[Any] | Callable[[Any], Any], - description: str, + description: str | Text, settable_object: object, *, settable_attrib_name: str | None = None, @@ -108,7 +111,8 @@ def __init__( input is a valid integer. Specifying bool automatically provides completion for 'true' and 'false' and uses a built-in function for conversion and validation. - :param description: A concise string that describes the purpose of this setting. + :param description: A concise string or rich Text object that describes the purpose of + this setting. :param settable_object: The object that owns the attribute being made settable (e.g. self). :param settable_attrib_name: The name of the attribute on the settable_object that will be modified. This defaults to the value of the name @@ -142,7 +146,7 @@ def get_bool_choices(_cmd2_self: CmdOrSet) -> Choices: self.name = name self.val_type = val_type - self.description = description + self.description = ru.rich_text_to_string(description) if isinstance(description, Text) else description self.settable_obj = settable_object self.settable_attrib_name = settable_attrib_name if settable_attrib_name is not None else name self.onchange_cb = onchange_cb diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index e55197bc3..705fd5983 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -4304,7 +4304,6 @@ def test_completekey_empty_string() -> None: def test_create_main_session_exception(monkeypatch): - # Mock PromptSession to raise ValueError on first call, then succeed valid_session_mock = mock.MagicMock(spec=PromptSession) mock_session = mock.MagicMock(side_effect=[ValueError, valid_session_mock]) From 114d9bf09a9e1508ece896eab7deba6e008f833a Mon Sep 17 00:00:00 2001 From: neoniobium Date: Wed, 15 Jul 2026 07:14:02 +0200 Subject: [PATCH 2/3] [Settable] address typo, docstring format, Added unit test and updated getting_started example --- CHANGELOG.md | 2 +- cmd2/utils.py | 3 +-- examples/getting_started.py | 12 +++++++++++- tests/test_cmd2.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f84f62d47..8f594983f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## (TBA) - Enhancements - - Added possibility to use rich Text objects to set the description argument of a `Settanble` + - Added possibility to use rich Text objects to set the description argument of a `Settable` ## 4.1.1 (July 9, 2026) diff --git a/cmd2/utils.py b/cmd2/utils.py index a1a089e93..a0c8d9067 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -111,8 +111,7 @@ def __init__( input is a valid integer. Specifying bool automatically provides completion for 'true' and 'false' and uses a built-in function for conversion and validation. - :param description: A concise string or rich Text object that describes the purpose of - this setting. + :param description: A concise string or rich Text object that describes the purpose of this setting. :param settable_object: The object that owns the attribute being made settable (e.g. self). :param settable_attrib_name: The name of the attribute on the settable_object that will be modified. This defaults to the value of the name diff --git a/examples/getting_started.py b/examples/getting_started.py index bea956875..5c07208e2 100755 --- a/examples/getting_started.py +++ b/examples/getting_started.py @@ -31,6 +31,7 @@ from prompt_toolkit.application import get_app from prompt_toolkit.formatted_text import AnyFormattedText from rich.style import Style +from rich.text import Text import cmd2 from cmd2 import ( @@ -94,7 +95,16 @@ def __init__(self) -> None: cmd2.Settable( "foreground_color", str, - "Foreground color to use with echo command", + Text.assemble( + "Foreground color to use with echo command ", + "(Options: ", + Text("Green", Style(color=Color.GREEN)), + ", ", + Text("Red", Style(color=Color.RED)), + ", ", + Text("Blue", Style(color=Color.BLUE)), + ", ...)", + ), self, choices=fg_colors, ) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 705fd5983..879304ca9 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -18,6 +18,7 @@ from prompt_toolkit.input import DummyInput, create_pipe_input from prompt_toolkit.output import DummyOutput from prompt_toolkit.shortcuts import PromptSession +from rich.style import Style from rich.text import Text import cmd2 @@ -352,6 +353,31 @@ def test_set_with_choices(base_app) -> None: assert err[0].startswith("Error setting fake: invalid choice") +def test_set_with_rich_description(base_app) -> None: + """Test with rich Text description.""" + description = "Rich text description" + base_app.rich_settable = "old" + rich_settable = cmd2.Settable( + "rich_settable", type(base_app.rich_settable), Text(description, Style(color="red", bold=True)), base_app + ) + base_app.add_settable(rich_settable) + + # Try see list settables info + out, err = run_cmd(base_app, "set rich_settable") + assert not err + name, value, text = out[-1].strip().split(maxsplit=2) + assert name == "rich_settable" + assert value == "old" + assert text == description + + # Try to set a valid value + out, err = run_cmd(base_app, "set rich_settable new") + assert base_app.last_result is True + assert not err + assert out[0].startswith("rich_settable") + assert out[0].endswith("─> 'new'") + + class OnChangeHookApp(cmd2.Cmd): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -4726,3 +4752,7 @@ def do_base(self, _: argparse.Namespace) -> None: root_parser = cast(cmd2.Cmd2ArgumentParser, app.command_parsers.get(app.do_base)) subparsers_action = root_parser.get_subparsers_action() assert not subparsers_action._name_parser_map + subparsers_action = root_parser.get_subparsers_action() + assert not subparsers_action._name_parser_map + subparsers_action = root_parser.get_subparsers_action() + assert not subparsers_action._name_parser_map From 1d8a621b0279105e737cdaa09af8fdf309b93a3f Mon Sep 17 00:00:00 2001 From: neoniobium Date: Wed, 15 Jul 2026 17:16:38 +0200 Subject: [PATCH 3/3] fix --- tests/test_cmd2.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 879304ca9..a1c1eb568 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -4752,7 +4752,3 @@ def do_base(self, _: argparse.Namespace) -> None: root_parser = cast(cmd2.Cmd2ArgumentParser, app.command_parsers.get(app.do_base)) subparsers_action = root_parser.get_subparsers_action() assert not subparsers_action._name_parser_map - subparsers_action = root_parser.get_subparsers_action() - assert not subparsers_action._name_parser_map - subparsers_action = root_parser.get_subparsers_action() - assert not subparsers_action._name_parser_map