Skip to content
Open
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ prompt is displayed.
- **max_column_completion_results**: (int) the maximum number of completion results to
display in a single column

# 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
Expand Down
2 changes: 1 addition & 1 deletion cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Completion
hint_table.add_row(Text.from_ansi(item.display), *item.table_row)

# Generate the table string
console = Cmd2GeneralConsole()
console = Cmd2GeneralConsole(file=self._cmd2_app.stdout)
with console.capture() as capture:
console.print(hint_table, end="", soft_wrap=False)

Expand Down
158 changes: 108 additions & 50 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, set_title
from rich.console import (
Console,
Group,
RenderableType,
)
Expand Down Expand Up @@ -157,6 +158,7 @@
shlex_split,
)
from .rich_utils import (
Cmd2BaseConsole,
Cmd2ExceptionConsole,
Cmd2GeneralConsole,
RichPrintKwargs,
Expand Down Expand Up @@ -1318,30 +1320,66 @@ def visible_prompt(self) -> str:

def print_to(
self,
file: IO[str],
destination: IO[str] | Cmd2BaseConsole,
*objects: Any,
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 file stream.
"""Print objects to a given destination (file stream or cmd2 console).

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 converted
to Rich Text objects, so that Rich can correctly calculate their display width when
printing.

Example:
```py
with console.capture() as capture:
self.print_to(console, some_ansi_styled_string)
```

!!! note

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.
To ensure consistent behavior, this method requires a file-like object or
an instance of ``Cmd2BaseConsole``.
Consoles not derived from ``Cmd2BaseConsole`` are disallowed because:

:param file: file stream being written to
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.
: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.
Expand All @@ -1350,24 +1388,43 @@ 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``.

See the Rich documentation for more details on emoji codes, markup tags, and highlighting.
"""
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(file=destination)

prepared_objects = ru.prepare_objects_for_rendering(*objects)

try:
Cmd2GeneralConsole(file).print(
console.print(
*prepared_objects,
sep=sep,
end=end,
Expand All @@ -1384,19 +1441,19 @@ 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:
Cmd2GeneralConsole(sys.stderr).print(self.broken_pipe_warning)
if self.broken_pipe_warning and console.file != sys.stderr:
Cmd2GeneralConsole(file=sys.stderr).print(self.broken_pipe_warning)

def poutput(
self,
*objects: Any,
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:
Expand All @@ -1423,10 +1480,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:
Expand Down Expand Up @@ -1454,10 +1511,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:
Expand All @@ -1482,10 +1539,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:
Expand Down Expand Up @@ -1513,7 +1570,7 @@ def format_exception(self, exception: BaseException) -> str:
:param exception: the exception to be printed.
:return: a formatted exception string
"""
console = Cmd2ExceptionConsole()
console = Cmd2ExceptionConsole(file=sys.stderr)
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):
Expand Down Expand Up @@ -1576,10 +1633,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:
Expand Down Expand Up @@ -1624,9 +1681,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:
Expand Down Expand Up @@ -1663,17 +1720,16 @@ 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

# Generate the bytes to send to the pager
console = Cmd2GeneralConsole(self.stdout)
console = Cmd2GeneralConsole(file=self.stdout)
with console.capture() as capture:
console.print(
*prepared_objects,
self.print_to(
console,
*objects,
sep=sep,
end=end,
style=style,
Expand Down Expand Up @@ -2477,10 +2533,12 @@ def complete(
# _NoResultsError completion hints already include a trailing "\n".
end = "" if isinstance(ex, argparse_completer._NoResultsError) else "\n"

console = ru.Cmd2GeneralConsole()
console = Cmd2GeneralConsole(file=self.stdout)
with console.capture() as capture:
console.print(
Text(err_str, style=Cmd2Style.ERROR if ex.apply_style else ""),
self.print_to(
console,
err_str,
style=Cmd2Style.ERROR if ex.apply_style else "",
end=end,
)
completion_error = capture.get()
Expand Down
Loading
Loading