diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e24f2006..53590b36f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,8 @@ prompt is displayed. `set_theme()` functions to support lazy initialization and safer in-place updates of the theme. - Renamed `Cmd._command_parsers` to `Cmd.command_parsers`. + - Removed `RichPrintKwargs` `TypedDict` in favor of using `Mapping[str, Any]`, allowing for + greater flexibility in passing keyword arguments to `console.print()` calls. - Enhancements - New `cmd2.Cmd` parameters - **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These @@ -114,8 +116,9 @@ prompt is displayed. - **read_secret**: read secrets like passwords without displaying them to the terminal - **ppretty**: a cmd2-compatible replacement for `rich.pretty.pprint()` - New settables: - - **max_column_completion_results**: (int) the maximum number of completion results to - display in a single column + - **max_column_completion_results**: (int) Maximum number of completion results to display + in a single column + - **traceback_show_locals**: (bool) Display local variables in tracebacks - `cmd2.Cmd.select` has been revamped to use the [choice](https://python-prompt-toolkit.readthedocs.io/en/3.0.52/pages/asking_for_a_choice.html) function from `prompt-toolkit` when both **stdin** and **stdout** are TTYs @@ -131,6 +134,7 @@ prompt is displayed. specific `cmd2.Cmd` subclass (e.g.,`class MyCommandSet(CommandSet[MyApp]):`). This provides full type hints and IDE autocompletion for `self._cmd` without needing to override and cast the property. + - Added `traceback_kwargs` attribute to allow customization of Rich-based tracebacks. ## 3.5.0 (April 13, 2026) diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 5f786414b..9db123b48 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -50,7 +50,6 @@ MetavarTypeCmd2HelpFormatter, RawDescriptionCmd2HelpFormatter, RawTextCmd2HelpFormatter, - RichPrintKwargs, TextGroup, get_theme, set_theme, @@ -105,7 +104,6 @@ "MetavarTypeCmd2HelpFormatter", "RawDescriptionCmd2HelpFormatter", "RawTextCmd2HelpFormatter", - "RichPrintKwargs", "set_theme", "TextGroup", # String Utils diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 325985bbd..f3f4e3da5 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -154,7 +154,6 @@ Cmd2ExceptionConsole, Cmd2GeneralConsole, Cmd2SimpleTable, - RichPrintKwargs, TextGroup, ) from .styles import Cmd2Style @@ -474,6 +473,19 @@ def __init__( self.scripts_add_to_history = True # Scripts and pyscripts add commands to history self.timing = False # Prints elapsed time for each command + # Default settings for Rich tracebacks created by format_exception(). + # This dictionary can contain any parameter accepted by the + # rich.traceback.Traceback class. You can modify it to adjust + # the detail and layout of tracebacks. + self.traceback_kwargs: dict[str, Any] = { + "width": 100, + "code_width": None, # Show all code characters + "show_locals": False, + "max_frames": 100, + "word_wrap": True, # Wrap long lines of code instead of truncate + "indent_guides": True, + } + # Cached Rich consoles used by core print methods. self._console_cache = _ConsoleCache() @@ -1339,12 +1351,11 @@ def allow_style_type(value: str) -> ru.AllowStyle: self.add_settable(Settable("quiet", bool, "Don't print nonessential feedback", self)) self.add_settable(Settable("scripts_add_to_history", bool, "Scripts and pyscripts add commands to history", self)) self.add_settable(Settable("timing", bool, "Report execution times", self)) - - # ----- Methods related to presenting output to the user ----- + self.add_settable(Settable("traceback_show_locals", bool, "Display local variables in tracebacks", self)) @property def allow_style(self) -> ru.AllowStyle: - """Read-only property needed to support do_set when it reads allow_style.""" + """Property needed to support do_set when it reads allow_style.""" return ru.ALLOW_STYLE @allow_style.setter @@ -1352,6 +1363,16 @@ def allow_style(self, new_val: ru.AllowStyle) -> None: """Setter property needed to support do_set when it updates allow_style.""" ru.ALLOW_STYLE = new_val + @property + def traceback_show_locals(self) -> bool: + """Property needed to support do_set when it reads traceback_show_locals.""" + return cast(bool, self.traceback_kwargs.get("show_locals", False)) + + @traceback_show_locals.setter + def traceback_show_locals(self, value: bool) -> None: + """Setter property needed to support do_set when it updates traceback_show_locals.""" + self.traceback_kwargs["show_locals"] = value + @property def visible_prompt(self) -> str: """Read-only property to get the visible prompt with any ANSI style sequences stripped. @@ -1426,7 +1447,7 @@ def print_to( emoji: bool = False, markup: bool = False, highlight: bool = False, - rich_print_kwargs: RichPrintKwargs | None = None, + rich_print_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print objects to a given file stream. @@ -1442,7 +1463,8 @@ def print_to( :param style: optional style to apply to output :param soft_wrap: Enable soft wrap mode. Defaults to True. If True, text that doesn't fit will run on to the following line, - just like with print(). This is useful for raw text and logs. + just like the built-in print() function. This is useful for raw text + and logs. If False, Rich wraps text to fit the terminal width. Set this to False when printing structured Renderables like Tables, Panels, or Columns to ensure they render as expected. @@ -1457,10 +1479,10 @@ def print_to( strings, such as common Python data types like numbers, booleans, or None. This is particularly useful when pretty printing objects like lists and dictionaries to display them in color. Defaults to False. - :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print(). + :param rich_print_kwargs: optional additional keyword arguments to pass to console.print(). :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this method and still call `super()` without encountering unexpected keyword argument errors. - These arguments are not passed to Rich's Console.print(). + These arguments are not passed to console.print(). See the Rich documentation for more details on emoji codes, markup tags, and highlighting. """ @@ -1499,7 +1521,7 @@ def poutput( emoji: bool = False, markup: bool = False, highlight: bool = False, - rich_print_kwargs: RichPrintKwargs | None = None, + rich_print_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print objects to self.stdout. @@ -1531,7 +1553,7 @@ def perror( emoji: bool = False, markup: bool = False, highlight: bool = False, - rich_print_kwargs: RichPrintKwargs | None = None, + rich_print_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print objects to sys.stderr. @@ -1564,7 +1586,7 @@ def psuccess( emoji: bool = False, markup: bool = False, highlight: bool = False, - rich_print_kwargs: RichPrintKwargs | None = None, + rich_print_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Wrap poutput, but apply Cmd2Style.SUCCESS. @@ -1594,7 +1616,7 @@ def pwarning( emoji: bool = False, markup: bool = False, highlight: bool = False, - rich_print_kwargs: RichPrintKwargs | None = None, + rich_print_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Wrap perror, but apply Cmd2Style.WARNING. @@ -1626,13 +1648,7 @@ def format_exception(self, exception: BaseException) -> str: with console.capture() as capture: # Only print a traceback if we're in debug mode and one exists. if self.debug and sys.exc_info() != (None, None, None): - traceback = Traceback( - width=None, # Use all available width - code_width=None, # Use all available width - show_locals=True, - max_frames=0, # 0 means full traceback. - word_wrap=True, # Wrap long lines of code instead of truncate - ) + traceback = Traceback(**self.traceback_kwargs) console.print(traceback, end="") else: @@ -1690,7 +1706,7 @@ def pfeedback( emoji: bool = False, markup: bool = False, highlight: bool = False, - rich_print_kwargs: RichPrintKwargs | None = None, + rich_print_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print nonessential feedback. @@ -1740,7 +1756,7 @@ def ppaged( emoji: bool = False, markup: bool = False, highlight: bool = False, - rich_print_kwargs: RichPrintKwargs | None = None, + rich_print_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print output using a pager. diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index ce632b69c..704e603aa 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -13,7 +13,6 @@ IO, Any, ClassVar, - TypedDict, ) from rich.box import SIMPLE_HEAD @@ -631,27 +630,6 @@ def __init__(self, *, file: IO[str] | None = None) -> None: ) -class RichPrintKwargs(TypedDict, total=False): - """Infrequently used Rich Console.print() keyword arguments. - - These arguments are supported by cmd2's print methods (e.g., poutput()) - via their ``rich_print_kwargs`` parameter. - - See Rich's Console.print() documentation for full details: - https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console.print - - Note: All fields are optional (total=False). If a key is not present, - Rich's default behavior for that argument will apply. - """ - - overflow: OverflowMethod | None - no_wrap: bool | None - width: int | None - height: int | None - crop: bool - new_line_start: bool - - class Cmd2SimpleTable(Table): """A clean, lightweight Rich Table tailored for cmd2's internal use.""" diff --git a/docs/features/builtin_commands.md b/docs/features/builtin_commands.md index d27f8a6a2..ddd633de6 100644 --- a/docs/features/builtin_commands.md +++ b/docs/features/builtin_commands.md @@ -90,6 +90,7 @@ application: quiet False Don't print nonessential feedback scripts_add_to_history True Scripts and pyscripts add commands to history timing False Report execution times + traceback_show_locals False Display local variables in tracebacks ``` Any of these user-settable parameters can be set while running your app with the `set` command like diff --git a/docs/features/generating_output.md b/docs/features/generating_output.md index 8610b30b0..b93b7c2b9 100644 --- a/docs/features/generating_output.md +++ b/docs/features/generating_output.md @@ -40,10 +40,12 @@ complex output: - `end`: string to write at end of printed text. Defaults to a newline - `style`: optional style to apply to output - `soft_wrap`: Enable soft wrap mode. If True, lines of text will not be word-wrapped or cropped to fit the terminal width. Defaults to True + - `justify`: justify method ("left", "center", "right", "full"). Defaults to None. - `emoji`: If True, Rich will replace emoji codes (e.g., 😃) with their corresponding Unicode characters. Defaults to False - `markup`: If True, Rich will interpret strings with tags (e.g., [bold]hello[/bold]) as styled output. Defaults to False - `highlight`: If True, Rich will automatically apply highlighting to elements within strings, such as common Python data types like numbers, booleans, or None. - - `rich_print_kwargs`: optional additional keyword arguments to pass to Rich's `Console.print()` + - `rich_print_kwargs`: optional additional keyword arguments to pass to `console.print()` + - `kwargs`: arbitrary keyword arguments to support extending the print methods. These are not passed to `console.print()`. ## Ordinary Output diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 330ab97b7..d8093eed1 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -27,7 +27,6 @@ Color, CommandSet, Completions, - RichPrintKwargs, clipboard, constants, exceptions, @@ -241,6 +240,25 @@ def test_set_allow_style(base_app, new_val, is_valid, expected) -> None: assert out +def test_set_traceback_show_locals(base_app: cmd2.Cmd) -> None: + # Use the set command to alter traceback_show_locals + + # Clear any existing value + base_app.traceback_kwargs.pop("show_locals", None) + assert "show_locals" not in base_app.traceback_kwargs + + # Test that we receive a default value of False if not present + orig_val = base_app.traceback_show_locals + assert orig_val is False + assert "show_locals" not in base_app.traceback_kwargs + + # Test setting it + new_val = not orig_val + run_cmd(base_app, f"set traceback_show_locals {new_val}") + assert base_app.traceback_show_locals is new_val + assert base_app.traceback_kwargs["show_locals"] is new_val + + def test_set_with_choices(base_app) -> None: """Test choices validation of Settables""" fake_choices = ["valid", "choices"] @@ -2465,7 +2483,7 @@ def test_poutput_emoji(outsim_app): @with_ansi_style(ru.AllowStyle.ALWAYS) def test_poutput_justify_and_width(outsim_app): - rich_print_kwargs = RichPrintKwargs(width=10) + rich_print_kwargs = {"width": 10} # Use a styled-string when justifying to check if its display width is correct. outsim_app.poutput( @@ -2477,9 +2495,8 @@ def test_poutput_justify_and_width(outsim_app): 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) +def test_rich_print_kwargs(outsim_app): + rich_print_kwargs = {"no_wrap": True, "overflow": "ellipsis", "width": 10} outsim_app.poutput( "This is longer than width.", @@ -2500,29 +2517,6 @@ def test_poutput_pretty_print(outsim_app): 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( - overflow="ellipsis", - no_wrap=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 - - @pytest.mark.parametrize( "stream", ["stdout", "stderr"],