Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)

Expand Down
2 changes: 0 additions & 2 deletions cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
MetavarTypeCmd2HelpFormatter,
RawDescriptionCmd2HelpFormatter,
RawTextCmd2HelpFormatter,
RichPrintKwargs,
TextGroup,
get_theme,
set_theme,
Expand Down Expand Up @@ -105,7 +104,6 @@
"MetavarTypeCmd2HelpFormatter",
"RawDescriptionCmd2HelpFormatter",
"RawTextCmd2HelpFormatter",
"RichPrintKwargs",
"set_theme",
"TextGroup",
# String Utils
Expand Down
58 changes: 37 additions & 21 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@
Cmd2ExceptionConsole,
Cmd2GeneralConsole,
Cmd2SimpleTable,
RichPrintKwargs,
TextGroup,
)
from .styles import Cmd2Style
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -1339,19 +1351,28 @@ 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
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
Comment thread
tleonhardt marked this conversation as resolved.

@property
def visible_prompt(self) -> str:
"""Read-only property to get the visible prompt with any ANSI style sequences stripped.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
"""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
22 changes: 0 additions & 22 deletions cmd2/rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
IO,
Any,
ClassVar,
TypedDict,
)

from rich.box import SIMPLE_HEAD
Expand Down Expand Up @@ -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."""

Expand Down
1 change: 1 addition & 0 deletions docs/features/builtin_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/features/generating_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
50 changes: 22 additions & 28 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
Color,
CommandSet,
Completions,
RichPrintKwargs,
clipboard,
constants,
exceptions,
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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(
Expand All @@ -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.",
Expand All @@ -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"],
Expand Down
Loading