From a2e824d023fe31a69b560d29160b3838f8fbb669 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 27 Feb 2026 19:29:21 -0500 Subject: [PATCH 1/7] Added ability to pass a console object to Cmd.print_to(). --- CHANGELOG.md | 7 ++++++ cmd2/cmd2.py | 60 ++++++++++++++++++++++++++++++++++++---------- tests/test_cmd2.py | 20 +++++++++++++--- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b12799b93..721716e4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 3.2.3 (TBD) + + - Enhancements + - Added ability to pass a console object to `Cmd.print_to()`. This provides support for + things like wrapping a `print_to()` call in a `console.status()` or `console.capture()` + context manager. + ## 3.2.2 (February 21, 2026) - Bug Fixes diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 33e6aa7ed..f917fb797 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -65,7 +65,11 @@ ) import rich.box -from rich.console import Group, RenderableType +from rich.console import ( + Console, + Group, + RenderableType, +) from rich.highlighter import ReprHighlighter from rich.rule import Rule from rich.style import Style, StyleType @@ -133,6 +137,7 @@ shlex_split, ) from .rich_utils import ( + Cmd2BaseConsole, Cmd2ExceptionConsole, Cmd2GeneralConsole, RichPrintKwargs, @@ -1247,7 +1252,7 @@ def visible_prompt(self) -> str: def print_to( self, - file: IO[str], + destination: IO[str] | Cmd2BaseConsole, *objects: Any, sep: str = " ", end: str = "\n", @@ -1259,13 +1264,30 @@ def print_to( rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: - """Print objects to a given file stream. + """Print objects to a given destination (file stream or cmd2 console). This method is configured for general-purpose printing. By default, it enables soft wrap and disables Rich's automatic detection for markup, emoji, and highlighting. These defaults can be overridden by passing explicit keyword arguments. - :param file: file stream being written to + See the Rich documentation for more details on emoji codes, markup tags, and highlighting. + + !!! note + + To ensure consistent behavior, this method requires a file-like object or + an instance of ``Cmd2BaseConsole``. + Consoles not derived from ``Cmd2BaseConsole`` are disallowed because: + + 1. **Style Control**: They ignore the global ``ALLOW_STYLE`` setting. + 2. **Theming**: They do not respect the application-wide ``APP_THEME``. + 3. **Error Handling**: They trigger a ``SystemExit`` on broken pipes. + ``Cmd2BaseConsole`` instead raises a catchable ``BrokenPipeError``, + ensuring the CLI application remains alive if a pipe is closed. + + :param destination: The output target. File-like objects are automatically + wrapped in a ``Cmd2GeneralConsole`` to ensure they respect + cmd2 global settings; otherwise, this must be an + instance of ``Cmd2BaseConsole``. :param objects: objects to print :param sep: string to write between printed text. Defaults to " ". :param end: string to write at end of printed text. Defaults to a newline. @@ -1290,13 +1312,28 @@ def print_to( :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(). - - See the Rich documentation for more details on emoji codes, markup tags, and highlighting. + :raises TypeError: If ``destination`` is a non-cmd2 ``Console`` instance that + does not derive from ``Cmd2BaseConsole``. """ + if isinstance(destination, Console): + if not isinstance(destination, Cmd2BaseConsole): + # Explicitly reject non-cmd2 consoles to ensure safe behavior + raise TypeError( + f"destination must be a 'Cmd2BaseConsole' or a file-like object, " + f"not a non-cmd2 '{type(destination).__name__}'. " + "Consoles not derived from 'Cmd2BaseConsole' bypass cmd2's " + "'ALLOW_STYLE' logic, 'APP_THEME' settings, and trigger 'SystemExit' " + "on broken pipes." + ) + console = destination + else: + # It's a file-like object (e.g., sys.stdout, StringIO) + console = Cmd2GeneralConsole(destination) + prepared_objects = ru.prepare_objects_for_rendering(*objects) try: - Cmd2GeneralConsole(file).print( + console.print( *prepared_objects, sep=sep, end=end, @@ -1313,7 +1350,7 @@ def print_to( # writing. If you would like your application to print a # warning message, then set the broken_pipe_warning attribute # to the message you want printed. - if self.broken_pipe_warning and file != sys.stderr: + if self.broken_pipe_warning and console.file != sys.stderr: Cmd2GeneralConsole(sys.stderr).print(self.broken_pipe_warning) def poutput( @@ -1581,8 +1618,6 @@ def ppaged( # Check if we are outputting to a pager. if functional_terminal and can_block: - prepared_objects = ru.prepare_objects_for_rendering(*objects) - # Chopping overrides soft_wrap if chop: soft_wrap = True @@ -1590,8 +1625,9 @@ def ppaged( # Generate the bytes to send to the pager console = Cmd2GeneralConsole(self.stdout) with console.capture() as capture: - console.print( - *prepared_objects, + self.print_to( + console, + *objects, sep=sep, end=end, style=style, diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 4922bd97f..9e7196fed 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -6,9 +6,7 @@ import signal import sys import tempfile -from code import ( - InteractiveConsole, -) +from code import InteractiveConsole from typing import NoReturn from unittest import mock @@ -2130,6 +2128,22 @@ def test_read_command_line_eof(base_app, monkeypatch) -> None: assert line == 'eof' +def test_print_to_custom_console(base_app) -> None: + console = ru.Cmd2GeneralConsole() + with console.capture() as capture: + base_app.print_to(console, "hello") + assert capture.get() == "hello\n" + + +def test_print_to_invalid_console_type(base_app) -> None: + from rich.console import Console + + console = Console() + with pytest.raises(TypeError) as excinfo: + base_app.print_to(console, "hello") + assert "destination must be a 'Cmd2BaseConsole'" in str(excinfo.value) + + def test_poutput_string(outsim_app) -> None: msg = 'This is a test' outsim_app.poutput(msg) From a2b36397bfd73920e8d9ea618231a3eb56d872dc Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 27 Feb 2026 19:36:26 -0500 Subject: [PATCH 2/7] Fixed doc formatting. --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 721716e4f..d9c21d80f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # 3.2.3 (TBD) - - Enhancements - - Added ability to pass a console object to `Cmd.print_to()`. This provides support for - things like wrapping a `print_to()` call in a `console.status()` or `console.capture()` - context manager. +- Enhancements + - Added ability to pass a console object to `Cmd.print_to()`. This provides support for things + like wrapping a `print_to()` call in a `console.status()` or `console.capture()` context + manager. ## 3.2.2 (February 21, 2026) From 322b2e45d175edfaa29d041a38d98611f2df72f4 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 28 Feb 2026 17:38:00 -0500 Subject: [PATCH 3/7] Updated default values for print functions. --- cmd2/cmd2.py | 91 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f917fb797..26edc2d5e 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1257,21 +1257,39 @@ def print_to( sep: str = " ", end: str = "\n", style: StyleType | None = None, - soft_wrap: bool = True, - emoji: bool = False, - markup: bool = False, - highlight: bool = False, + soft_wrap: bool | None = None, + emoji: bool | None = None, + markup: bool | None = None, + highlight: bool | None = None, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print objects to a given destination (file stream or cmd2 console). - This method is configured for general-purpose printing. By default, it enables - soft wrap and disables Rich's automatic detection for markup, emoji, and highlighting. - These defaults can be overridden by passing explicit keyword arguments. + If ``destination`` is a file-like object, it is wrapped in a ``Cmd2GeneralConsole`` + which is configured for general-purpose printing. By default, it enables soft wrap and + disables Rich's automatic detection for markup, emoji, and highlighting. These defaults + can be overridden by passing explicit keyword arguments. + + If ``destination`` is a ``Cmd2BaseConsole``, the console's default settings for + soft wrap, markup, emoji, and highlighting are used unless overridden by passing + explicit keyword arguments. See the Rich documentation for more details on emoji codes, markup tags, and highlighting. + **Why use this method instead of console.print()?** + + This method calls ``cmd2.rich_utils.prepare_objects_for_rendering()`` on the objects + being printed. This ensures that strings containing ANSI style sequences are correctly + converted to Rich Text objects, preserving their style and calculating the correct + display width. This is particularly important when capturing output from a console. + + Example: + ```py + with console.capture() as capture: + self.print_to(console, some_ansi_styled_string) + ``` + !!! note To ensure consistent behavior, this method requires a file-like object or @@ -1292,7 +1310,8 @@ def print_to( :param sep: string to write between printed text. Defaults to " ". :param end: string to write at end of printed text. Defaults to a newline. :param style: optional style to apply to output - :param soft_wrap: Enable soft wrap mode. Defaults to True. + :param soft_wrap: Enable soft wrap mode. Defaults to None. + If None, the destination console's default behavior is used. 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. If False, Rich wraps text to fit the terminal width. @@ -1301,19 +1320,23 @@ def print_to( For example, when soft_wrap is True Panels truncate text which is wider than the terminal. :param emoji: If True, Rich will replace emoji codes (e.g., :smiley:) with their - corresponding Unicode characters. Defaults to False. + corresponding Unicode characters. Defaults to None. + If None, the destination console's default behavior is used. :param markup: If True, Rich will interpret strings with tags (e.g., [bold]hello[/bold]) - as styled output. Defaults to False. + as styled output. Defaults to None. + If None, the destination console's default behavior is used. :param highlight: If True, Rich will automatically apply highlighting to elements within 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. + dictionaries to display them in color. Defaults to None. + If None, the destination console's default behavior is used. :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's 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(). :raises TypeError: If ``destination`` is a non-cmd2 ``Console`` instance that does not derive from ``Cmd2BaseConsole``. + """ if isinstance(destination, Console): if not isinstance(destination, Cmd2BaseConsole): @@ -1359,10 +1382,10 @@ def poutput( sep: str = " ", end: str = "\n", style: StyleType | None = None, - soft_wrap: bool = True, - emoji: bool = False, - markup: bool = False, - highlight: bool = False, + soft_wrap: bool | None = None, + emoji: bool | None = None, + markup: bool | None = None, + highlight: bool | None = None, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1389,10 +1412,10 @@ def perror( sep: str = " ", end: str = "\n", style: StyleType | None = Cmd2Style.ERROR, - soft_wrap: bool = True, - emoji: bool = False, - markup: bool = False, - highlight: bool = False, + soft_wrap: bool | None = None, + emoji: bool | None = None, + markup: bool | None = None, + highlight: bool | None = None, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1420,10 +1443,10 @@ def psuccess( *objects: Any, sep: str = " ", end: str = "\n", - soft_wrap: bool = True, - emoji: bool = False, - markup: bool = False, - highlight: bool = False, + soft_wrap: bool | None = None, + emoji: bool | None = None, + markup: bool | None = None, + highlight: bool | None = None, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1448,10 +1471,10 @@ def pwarning( *objects: Any, sep: str = " ", end: str = "\n", - soft_wrap: bool = True, - emoji: bool = False, - markup: bool = False, - highlight: bool = False, + soft_wrap: bool | None = None, + emoji: bool | None = None, + markup: bool | None = None, + highlight: bool | None = None, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1531,10 +1554,10 @@ def pfeedback( sep: str = " ", end: str = "\n", style: StyleType | None = None, - soft_wrap: bool = True, - emoji: bool = False, - markup: bool = False, - highlight: bool = False, + soft_wrap: bool | None = None, + emoji: bool | None = None, + markup: bool | None = None, + highlight: bool | None = None, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1579,9 +1602,9 @@ def ppaged( style: StyleType | None = None, chop: bool = False, soft_wrap: bool = True, - emoji: bool = False, - markup: bool = False, - highlight: bool = False, + emoji: bool | None = None, + markup: bool | None = None, + highlight: bool | None = None, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: From ac4c90496ea29cb82bd66ec9ce68d0b131a5d730 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 28 Feb 2026 20:37:03 -0500 Subject: [PATCH 4/7] Updated documentation. --- cmd2/cmd2.py | 10 ++++----- cmd2/rich_utils.py | 51 +++++++++++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 26edc2d5e..1c1feeb9a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -170,7 +170,7 @@ # Set up readline if rl_type == RlType.NONE: # pragma: no cover - Cmd2GeneralConsole(sys.stderr).print(rl_warning, style=Cmd2Style.WARNING) + Cmd2GeneralConsole(file=sys.stderr).print(rl_warning, style=Cmd2Style.WARNING) else: from .rl_utils import ( readline, @@ -1351,7 +1351,7 @@ def print_to( console = destination else: # It's a file-like object (e.g., sys.stdout, StringIO) - console = Cmd2GeneralConsole(destination) + console = Cmd2GeneralConsole(file=destination) prepared_objects = ru.prepare_objects_for_rendering(*objects) @@ -1374,7 +1374,7 @@ def print_to( # warning message, then set the broken_pipe_warning attribute # to the message you want printed. if self.broken_pipe_warning and console.file != sys.stderr: - Cmd2GeneralConsole(sys.stderr).print(self.broken_pipe_warning) + Cmd2GeneralConsole(file=sys.stderr).print(self.broken_pipe_warning) def poutput( self, @@ -1507,7 +1507,7 @@ def pexcept( :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. """ - console = Cmd2ExceptionConsole(sys.stderr) + console = Cmd2ExceptionConsole() # Only print a traceback if we're in debug mode and one exists. if self.debug and sys.exc_info() != (None, None, None): @@ -1646,7 +1646,7 @@ def ppaged( soft_wrap = True # Generate the bytes to send to the pager - console = Cmd2GeneralConsole(self.stdout) + console = Cmd2GeneralConsole(file=self.stdout) with console.capture() as capture: self.print_to( console, diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index dcd5d15b7..535135198 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -124,6 +124,7 @@ class Cmd2BaseConsole(Console): def __init__( self, + *, file: IO[str] | None = None, **kwargs: Any, ) -> None: @@ -180,17 +181,19 @@ def on_broken_pipe(self) -> None: class Cmd2GeneralConsole(Cmd2BaseConsole): - """Rich console for general-purpose printing.""" + """Rich console for general-purpose printing. - def __init__(self, file: IO[str] | None = None) -> None: + It enables soft wrap and disables Rich's automatic detection for markup, + emoji, and highlighting. These defaults can be overridden in calls to the + console's or cmd2's print methods. + """ + + def __init__(self, *, file: IO[str] | None = None) -> None: """Cmd2GeneralConsole initializer. :param file: optional file object where the console should write to. Defaults to sys.stdout. """ - # This console is configured for general-purpose printing. It enables soft wrap - # and disables Rich's automatic detection for markup, emoji, and highlighting. - # These defaults can be overridden in calls to the console's or cmd2's print methods. super().__init__( file=file, soft_wrap=True, @@ -203,23 +206,25 @@ def __init__(self, file: IO[str] | None = None) -> None: class Cmd2RichArgparseConsole(Cmd2BaseConsole): """Rich console for rich-argparse output. - This class ensures long lines in help text are not truncated by avoiding soft_wrap, + Ensures long lines in help text are not truncated by disabling soft_wrap, which conflicts with rich-argparse's explicit no_wrap and overflow settings. + + Since this console is used to print error messages which may not be intended + for Rich formatting, it disables Rich's automatic detection for markup, emoji, + and highlighting. Because rich-argparse does markup and highlighting without + involving the console, disabling these settings does not affect the library's + internal functionality. """ - def __init__(self, file: IO[str] | None = None) -> None: + def __init__(self, *, file: IO[str] | None = None) -> None: """Cmd2RichArgparseConsole initializer. :param file: optional file object where the console should write to. Defaults to sys.stdout. """ - # Since this console is used to print error messages which may not have - # been pre-formatted by rich-argparse, disable Rich's automatic detection - # for markup, emoji, and highlighting. rich-argparse does markup and - # highlighting without involving the console so these won't affect its - # internal functionality. super().__init__( file=file, + soft_wrap=False, markup=False, emoji=False, highlight=False, @@ -227,11 +232,29 @@ def __init__(self, file: IO[str] | None = None) -> None: class Cmd2ExceptionConsole(Cmd2BaseConsole): - """Rich console for printing exceptions. + """Rich console for printing exceptions and Rich Tracebacks. - Ensures that long exception messages word wrap for readability by keeping soft_wrap disabled. + Ensures that output is always word-wrapped for readability and disables + Rich's automatic detection for markup, emoji, and highlighting to prevent + interference with raw error data. """ + def __init__(self, *, file: IO[str] | None = None) -> None: + """Cmd2ExceptionConsole initializer. + + :param file: optional file object where the console should write to. + If None, output defaults to sys.stderr. + """ + # Use stderr=True so that Rich defaults to sys.stderr if file is None. + super().__init__( + file=file, + stderr=True, + soft_wrap=False, + markup=False, + emoji=False, + highlight=False, + ) + def console_width() -> int: """Return the width of the console.""" From 56b10c3ec2e851f009b6bfbfb19aa47649b2f69f Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 28 Feb 2026 20:47:37 -0500 Subject: [PATCH 5/7] Updated change log. --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c21d80f..8755ccd76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ -# 3.2.3 (TBD) +# 3.3.0 (TBD) - Enhancements - Added ability to pass a console object to `Cmd.print_to()`. This provides support for things like wrapping a `print_to()` call in a `console.status()` or `console.capture()` context manager. +- Breaking Changes + - Renamed the `file` parameter of `Cmd.print_to()` to `destination` to support file-like objects + and console objects. + - `Cmd2BaseConsole(file)` argument is now a keyword-only argument to be consistent with the + `rich.console.Console` class. + ## 3.2.2 (February 21, 2026) - Bug Fixes From 98e5e90320acf7ed44b2beb848546ab0e1d45a36 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 28 Feb 2026 20:58:45 -0500 Subject: [PATCH 6/7] Updated comment. --- cmd2/cmd2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 1c1feeb9a..697e98eb3 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1280,9 +1280,9 @@ def print_to( **Why use this method instead of console.print()?** This method calls ``cmd2.rich_utils.prepare_objects_for_rendering()`` on the objects - being printed. This ensures that strings containing ANSI style sequences are correctly - converted to Rich Text objects, preserving their style and calculating the correct - display width. This is particularly important when capturing output from a console. + being printed. This ensures that strings containing ANSI style sequences are converted + to Rich Text objects, so that Rich can correctly calculate their display width when + printing. Example: ```py From bfb8be4ec2b4d7419f677e5183886f93ace23f2c Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 28 Feb 2026 21:22:32 -0500 Subject: [PATCH 7/7] Don't default Cmd2ExceptionConsole's file to sys.stderr. --- cmd2/cmd2.py | 2 +- cmd2/rich_utils.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 697e98eb3..8bca1f883 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1507,7 +1507,7 @@ def pexcept( :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. """ - console = Cmd2ExceptionConsole() + console = Cmd2ExceptionConsole(file=sys.stderr) # Only print a traceback if we're in debug mode and one exists. if self.debug and sys.exc_info() != (None, None, None): diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index 535135198..4178158d6 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -243,12 +243,10 @@ def __init__(self, *, file: IO[str] | None = None) -> None: """Cmd2ExceptionConsole initializer. :param file: optional file object where the console should write to. - If None, output defaults to sys.stderr. + Defaults to sys.stdout. """ - # Use stderr=True so that Rich defaults to sys.stderr if file is None. super().__init__( file=file, - stderr=True, soft_wrap=False, markup=False, emoji=False,