Skip to content

Commit 98cdaa0

Browse files
committed
Added ability to pass a console object to Cmd.print_to().
1 parent 3802eca commit 98cdaa0

File tree

5 files changed

+174
-68
lines changed

5 files changed

+174
-68
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ prompt is displayed.
7979
- **max_column_completion_results**: (int) the maximum number of completion results to
8080
display in a single column
8181

82+
# 3.3.0 (TBD)
83+
84+
- Enhancements
85+
- Added ability to pass a console object to `Cmd.print_to()`. This provides support for things
86+
like wrapping a `print_to()` call in a `console.status()` or `console.capture()` context
87+
manager.
88+
89+
- Breaking Changes
90+
- Renamed the `file` parameter of `Cmd.print_to()` to `destination` to support file-like objects
91+
and console objects.
92+
- `Cmd2BaseConsole(file)` argument is now a keyword-only argument to be consistent with the
93+
`rich.console.Console` class.
94+
8295
## 3.2.2 (February 21, 2026)
8396

8497
- Bug Fixes

cmd2/argparse_completer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Completion
633633
hint_table.add_row(Text.from_ansi(item.display), *item.table_row)
634634

635635
# Generate the table string
636-
console = Cmd2GeneralConsole()
636+
console = Cmd2GeneralConsole(file=self._cmd2_app.stdout)
637637
with console.capture() as capture:
638638
console.print(hint_table, end="", soft_wrap=False)
639639

cmd2/cmd2.py

Lines changed: 108 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from prompt_toolkit.patch_stdout import patch_stdout
8585
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, set_title
8686
from rich.console import (
87+
Console,
8788
Group,
8889
RenderableType,
8990
)
@@ -157,6 +158,7 @@
157158
shlex_split,
158159
)
159160
from .rich_utils import (
161+
Cmd2BaseConsole,
160162
Cmd2ExceptionConsole,
161163
Cmd2GeneralConsole,
162164
RichPrintKwargs,
@@ -1318,30 +1320,66 @@ def visible_prompt(self) -> str:
13181320

13191321
def print_to(
13201322
self,
1321-
file: IO[str],
1323+
destination: IO[str] | Cmd2BaseConsole,
13221324
*objects: Any,
13231325
sep: str = " ",
13241326
end: str = "\n",
13251327
style: StyleType | None = None,
1326-
soft_wrap: bool = True,
1327-
emoji: bool = False,
1328-
markup: bool = False,
1329-
highlight: bool = False,
1328+
soft_wrap: bool | None = None,
1329+
emoji: bool | None = None,
1330+
markup: bool | None = None,
1331+
highlight: bool | None = None,
13301332
rich_print_kwargs: RichPrintKwargs | None = None,
13311333
**kwargs: Any, # noqa: ARG002
13321334
) -> None:
1333-
"""Print objects to a given file stream.
1335+
"""Print objects to a given destination (file stream or cmd2 console).
1336+
1337+
If ``destination`` is a file-like object, it is wrapped in a ``Cmd2GeneralConsole``
1338+
which is configured for general-purpose printing. By default, it enables soft wrap and
1339+
disables Rich's automatic detection for markup, emoji, and highlighting. These defaults
1340+
can be overridden by passing explicit keyword arguments.
1341+
1342+
If ``destination`` is a ``Cmd2BaseConsole``, the console's default settings for
1343+
soft wrap, markup, emoji, and highlighting are used unless overridden by passing
1344+
explicit keyword arguments.
1345+
1346+
See the Rich documentation for more details on emoji codes, markup tags, and highlighting.
1347+
1348+
**Why use this method instead of console.print()?**
1349+
1350+
This method calls ``cmd2.rich_utils.prepare_objects_for_rendering()`` on the objects
1351+
being printed. This ensures that strings containing ANSI style sequences are converted
1352+
to Rich Text objects, so that Rich can correctly calculate their display width when
1353+
printing.
1354+
1355+
Example:
1356+
```py
1357+
with console.capture() as capture:
1358+
self.print_to(console, some_ansi_styled_string)
1359+
```
1360+
1361+
!!! note
13341362
1335-
This method is configured for general-purpose printing. By default, it enables
1336-
soft wrap and disables Rich's automatic detection for markup, emoji, and highlighting.
1337-
These defaults can be overridden by passing explicit keyword arguments.
1363+
To ensure consistent behavior, this method requires a file-like object or
1364+
an instance of ``Cmd2BaseConsole``.
1365+
Consoles not derived from ``Cmd2BaseConsole`` are disallowed because:
13381366
1339-
:param file: file stream being written to
1367+
1. **Style Control**: They ignore the global ``ALLOW_STYLE`` setting.
1368+
2. **Theming**: They do not respect the application-wide ``APP_THEME``.
1369+
3. **Error Handling**: They trigger a ``SystemExit`` on broken pipes.
1370+
``Cmd2BaseConsole`` instead raises a catchable ``BrokenPipeError``,
1371+
ensuring the CLI application remains alive if a pipe is closed.
1372+
1373+
:param destination: The output target. File-like objects are automatically
1374+
wrapped in a ``Cmd2GeneralConsole`` to ensure they respect
1375+
cmd2 global settings; otherwise, this must be an
1376+
instance of ``Cmd2BaseConsole``.
13401377
:param objects: objects to print
13411378
:param sep: string to write between printed text. Defaults to " ".
13421379
:param end: string to write at end of printed text. Defaults to a newline.
13431380
:param style: optional style to apply to output
1344-
:param soft_wrap: Enable soft wrap mode. Defaults to True.
1381+
:param soft_wrap: Enable soft wrap mode. Defaults to None.
1382+
If None, the destination console's default behavior is used.
13451383
If True, text that doesn't fit will run on to the following line,
13461384
just like with print(). This is useful for raw text and logs.
13471385
If False, Rich wraps text to fit the terminal width.
@@ -1350,24 +1388,43 @@ def print_to(
13501388
For example, when soft_wrap is True Panels truncate text
13511389
which is wider than the terminal.
13521390
:param emoji: If True, Rich will replace emoji codes (e.g., :smiley:) with their
1353-
corresponding Unicode characters. Defaults to False.
1391+
corresponding Unicode characters. Defaults to None.
1392+
If None, the destination console's default behavior is used.
13541393
:param markup: If True, Rich will interpret strings with tags (e.g., [bold]hello[/bold])
1355-
as styled output. Defaults to False.
1394+
as styled output. Defaults to None.
1395+
If None, the destination console's default behavior is used.
13561396
:param highlight: If True, Rich will automatically apply highlighting to elements within
13571397
strings, such as common Python data types like numbers, booleans, or None.
13581398
This is particularly useful when pretty printing objects like lists and
1359-
dictionaries to display them in color. Defaults to False.
1399+
dictionaries to display them in color. Defaults to None.
1400+
If None, the destination console's default behavior is used.
13601401
:param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print().
13611402
:param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this
13621403
method and still call `super()` without encountering unexpected keyword argument errors.
13631404
These arguments are not passed to Rich's Console.print().
1405+
:raises TypeError: If ``destination`` is a non-cmd2 ``Console`` instance that
1406+
does not derive from ``Cmd2BaseConsole``.
13641407
1365-
See the Rich documentation for more details on emoji codes, markup tags, and highlighting.
13661408
"""
1409+
if isinstance(destination, Console):
1410+
if not isinstance(destination, Cmd2BaseConsole):
1411+
# Explicitly reject non-cmd2 consoles to ensure safe behavior
1412+
raise TypeError(
1413+
f"destination must be a 'Cmd2BaseConsole' or a file-like object, "
1414+
f"not a non-cmd2 '{type(destination).__name__}'. "
1415+
"Consoles not derived from 'Cmd2BaseConsole' bypass cmd2's "
1416+
"'ALLOW_STYLE' logic, 'APP_THEME' settings, and trigger 'SystemExit' "
1417+
"on broken pipes."
1418+
)
1419+
console = destination
1420+
else:
1421+
# It's a file-like object (e.g., sys.stdout, StringIO)
1422+
console = Cmd2GeneralConsole(file=destination)
1423+
13671424
prepared_objects = ru.prepare_objects_for_rendering(*objects)
13681425

13691426
try:
1370-
Cmd2GeneralConsole(file).print(
1427+
console.print(
13711428
*prepared_objects,
13721429
sep=sep,
13731430
end=end,
@@ -1384,19 +1441,19 @@ def print_to(
13841441
# writing. If you would like your application to print a
13851442
# warning message, then set the broken_pipe_warning attribute
13861443
# to the message you want printed.
1387-
if self.broken_pipe_warning and file != sys.stderr:
1388-
Cmd2GeneralConsole(sys.stderr).print(self.broken_pipe_warning)
1444+
if self.broken_pipe_warning and console.file != sys.stderr:
1445+
Cmd2GeneralConsole(file=sys.stderr).print(self.broken_pipe_warning)
13891446

13901447
def poutput(
13911448
self,
13921449
*objects: Any,
13931450
sep: str = " ",
13941451
end: str = "\n",
13951452
style: StyleType | None = None,
1396-
soft_wrap: bool = True,
1397-
emoji: bool = False,
1398-
markup: bool = False,
1399-
highlight: bool = False,
1453+
soft_wrap: bool | None = None,
1454+
emoji: bool | None = None,
1455+
markup: bool | None = None,
1456+
highlight: bool | None = None,
14001457
rich_print_kwargs: RichPrintKwargs | None = None,
14011458
**kwargs: Any, # noqa: ARG002
14021459
) -> None:
@@ -1423,10 +1480,10 @@ def perror(
14231480
sep: str = " ",
14241481
end: str = "\n",
14251482
style: StyleType | None = Cmd2Style.ERROR,
1426-
soft_wrap: bool = True,
1427-
emoji: bool = False,
1428-
markup: bool = False,
1429-
highlight: bool = False,
1483+
soft_wrap: bool | None = None,
1484+
emoji: bool | None = None,
1485+
markup: bool | None = None,
1486+
highlight: bool | None = None,
14301487
rich_print_kwargs: RichPrintKwargs | None = None,
14311488
**kwargs: Any, # noqa: ARG002
14321489
) -> None:
@@ -1454,10 +1511,10 @@ def psuccess(
14541511
*objects: Any,
14551512
sep: str = " ",
14561513
end: str = "\n",
1457-
soft_wrap: bool = True,
1458-
emoji: bool = False,
1459-
markup: bool = False,
1460-
highlight: bool = False,
1514+
soft_wrap: bool | None = None,
1515+
emoji: bool | None = None,
1516+
markup: bool | None = None,
1517+
highlight: bool | None = None,
14611518
rich_print_kwargs: RichPrintKwargs | None = None,
14621519
**kwargs: Any, # noqa: ARG002
14631520
) -> None:
@@ -1482,10 +1539,10 @@ def pwarning(
14821539
*objects: Any,
14831540
sep: str = " ",
14841541
end: str = "\n",
1485-
soft_wrap: bool = True,
1486-
emoji: bool = False,
1487-
markup: bool = False,
1488-
highlight: bool = False,
1542+
soft_wrap: bool | None = None,
1543+
emoji: bool | None = None,
1544+
markup: bool | None = None,
1545+
highlight: bool | None = None,
14891546
rich_print_kwargs: RichPrintKwargs | None = None,
14901547
**kwargs: Any, # noqa: ARG002
14911548
) -> None:
@@ -1513,7 +1570,7 @@ def format_exception(self, exception: BaseException) -> str:
15131570
:param exception: the exception to be printed.
15141571
:return: a formatted exception string
15151572
"""
1516-
console = Cmd2ExceptionConsole()
1573+
console = Cmd2ExceptionConsole(file=sys.stderr)
15171574
with console.capture() as capture:
15181575
# Only print a traceback if we're in debug mode and one exists.
15191576
if self.debug and sys.exc_info() != (None, None, None):
@@ -1576,10 +1633,10 @@ def pfeedback(
15761633
sep: str = " ",
15771634
end: str = "\n",
15781635
style: StyleType | None = None,
1579-
soft_wrap: bool = True,
1580-
emoji: bool = False,
1581-
markup: bool = False,
1582-
highlight: bool = False,
1636+
soft_wrap: bool | None = None,
1637+
emoji: bool | None = None,
1638+
markup: bool | None = None,
1639+
highlight: bool | None = None,
15831640
rich_print_kwargs: RichPrintKwargs | None = None,
15841641
**kwargs: Any, # noqa: ARG002
15851642
) -> None:
@@ -1624,9 +1681,9 @@ def ppaged(
16241681
style: StyleType | None = None,
16251682
chop: bool = False,
16261683
soft_wrap: bool = True,
1627-
emoji: bool = False,
1628-
markup: bool = False,
1629-
highlight: bool = False,
1684+
emoji: bool | None = None,
1685+
markup: bool | None = None,
1686+
highlight: bool | None = None,
16301687
rich_print_kwargs: RichPrintKwargs | None = None,
16311688
**kwargs: Any, # noqa: ARG002
16321689
) -> None:
@@ -1663,17 +1720,16 @@ def ppaged(
16631720

16641721
# Check if we are outputting to a pager.
16651722
if functional_terminal and can_block:
1666-
prepared_objects = ru.prepare_objects_for_rendering(*objects)
1667-
16681723
# Chopping overrides soft_wrap
16691724
if chop:
16701725
soft_wrap = True
16711726

16721727
# Generate the bytes to send to the pager
1673-
console = Cmd2GeneralConsole(self.stdout)
1728+
console = Cmd2GeneralConsole(file=self.stdout)
16741729
with console.capture() as capture:
1675-
console.print(
1676-
*prepared_objects,
1730+
self.print_to(
1731+
console,
1732+
*objects,
16771733
sep=sep,
16781734
end=end,
16791735
style=style,
@@ -2477,10 +2533,12 @@ def complete(
24772533
# _NoResultsError completion hints already include a trailing "\n".
24782534
end = "" if isinstance(ex, argparse_completer._NoResultsError) else "\n"
24792535

2480-
console = ru.Cmd2GeneralConsole()
2536+
console = Cmd2GeneralConsole(file=self.stdout)
24812537
with console.capture() as capture:
2482-
console.print(
2483-
Text(err_str, style=Cmd2Style.ERROR if ex.apply_style else ""),
2538+
self.print_to(
2539+
console,
2540+
err_str,
2541+
style=Cmd2Style.ERROR if ex.apply_style else "",
24842542
end=end,
24852543
)
24862544
completion_error = capture.get()

0 commit comments

Comments
 (0)