Skip to content

Commit a3b2d80

Browse files
authored
Added ability to pass a console object to Cmd.print_to(). (#1592)
1 parent 6c48851 commit a3b2d80

File tree

4 files changed

+173
-66
lines changed

4 files changed

+173
-66
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
# 3.3.0 (TBD)
2+
3+
- Enhancements
4+
- Added ability to pass a console object to `Cmd.print_to()`. This provides support for things
5+
like wrapping a `print_to()` call in a `console.status()` or `console.capture()` context
6+
manager.
7+
8+
- Breaking Changes
9+
- Renamed the `file` parameter of `Cmd.print_to()` to `destination` to support file-like objects
10+
and console objects.
11+
- `Cmd2BaseConsole(file)` argument is now a keyword-only argument to be consistent with the
12+
`rich.console.Console` class.
13+
114
## 3.2.2 (February 21, 2026)
215

316
- Bug Fixes

cmd2/cmd2.py

Lines changed: 108 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@
6565
)
6666

6767
import rich.box
68-
from rich.console import Group, RenderableType
68+
from rich.console import (
69+
Console,
70+
Group,
71+
RenderableType,
72+
)
6973
from rich.highlighter import ReprHighlighter
7074
from rich.rule import Rule
7175
from rich.style import Style, StyleType
@@ -133,6 +137,7 @@
133137
shlex_split,
134138
)
135139
from .rich_utils import (
140+
Cmd2BaseConsole,
136141
Cmd2ExceptionConsole,
137142
Cmd2GeneralConsole,
138143
RichPrintKwargs,
@@ -165,7 +170,7 @@
165170

166171
# Set up readline
167172
if rl_type == RlType.NONE: # pragma: no cover
168-
Cmd2GeneralConsole(sys.stderr).print(rl_warning, style=Cmd2Style.WARNING)
173+
Cmd2GeneralConsole(file=sys.stderr).print(rl_warning, style=Cmd2Style.WARNING)
169174
else:
170175
from .rl_utils import (
171176
readline,
@@ -1247,30 +1252,66 @@ def visible_prompt(self) -> str:
12471252

12481253
def print_to(
12491254
self,
1250-
file: IO[str],
1255+
destination: IO[str] | Cmd2BaseConsole,
12511256
*objects: Any,
12521257
sep: str = " ",
12531258
end: str = "\n",
12541259
style: StyleType | None = None,
1255-
soft_wrap: bool = True,
1256-
emoji: bool = False,
1257-
markup: bool = False,
1258-
highlight: bool = False,
1260+
soft_wrap: bool | None = None,
1261+
emoji: bool | None = None,
1262+
markup: bool | None = None,
1263+
highlight: bool | None = None,
12591264
rich_print_kwargs: RichPrintKwargs | None = None,
12601265
**kwargs: Any, # noqa: ARG002
12611266
) -> None:
1262-
"""Print objects to a given file stream.
1267+
"""Print objects to a given destination (file stream or cmd2 console).
1268+
1269+
If ``destination`` is a file-like object, it is wrapped in a ``Cmd2GeneralConsole``
1270+
which is configured for general-purpose printing. By default, it enables soft wrap and
1271+
disables Rich's automatic detection for markup, emoji, and highlighting. These defaults
1272+
can be overridden by passing explicit keyword arguments.
1273+
1274+
If ``destination`` is a ``Cmd2BaseConsole``, the console's default settings for
1275+
soft wrap, markup, emoji, and highlighting are used unless overridden by passing
1276+
explicit keyword arguments.
1277+
1278+
See the Rich documentation for more details on emoji codes, markup tags, and highlighting.
1279+
1280+
**Why use this method instead of console.print()?**
1281+
1282+
This method calls ``cmd2.rich_utils.prepare_objects_for_rendering()`` on the objects
1283+
being printed. This ensures that strings containing ANSI style sequences are converted
1284+
to Rich Text objects, so that Rich can correctly calculate their display width when
1285+
printing.
1286+
1287+
Example:
1288+
```py
1289+
with console.capture() as capture:
1290+
self.print_to(console, some_ansi_styled_string)
1291+
```
1292+
1293+
!!! note
12631294
1264-
This method is configured for general-purpose printing. By default, it enables
1265-
soft wrap and disables Rich's automatic detection for markup, emoji, and highlighting.
1266-
These defaults can be overridden by passing explicit keyword arguments.
1295+
To ensure consistent behavior, this method requires a file-like object or
1296+
an instance of ``Cmd2BaseConsole``.
1297+
Consoles not derived from ``Cmd2BaseConsole`` are disallowed because:
12671298
1268-
:param file: file stream being written to
1299+
1. **Style Control**: They ignore the global ``ALLOW_STYLE`` setting.
1300+
2. **Theming**: They do not respect the application-wide ``APP_THEME``.
1301+
3. **Error Handling**: They trigger a ``SystemExit`` on broken pipes.
1302+
``Cmd2BaseConsole`` instead raises a catchable ``BrokenPipeError``,
1303+
ensuring the CLI application remains alive if a pipe is closed.
1304+
1305+
:param destination: The output target. File-like objects are automatically
1306+
wrapped in a ``Cmd2GeneralConsole`` to ensure they respect
1307+
cmd2 global settings; otherwise, this must be an
1308+
instance of ``Cmd2BaseConsole``.
12691309
:param objects: objects to print
12701310
:param sep: string to write between printed text. Defaults to " ".
12711311
:param end: string to write at end of printed text. Defaults to a newline.
12721312
:param style: optional style to apply to output
1273-
:param soft_wrap: Enable soft wrap mode. Defaults to True.
1313+
:param soft_wrap: Enable soft wrap mode. Defaults to None.
1314+
If None, the destination console's default behavior is used.
12741315
If True, text that doesn't fit will run on to the following line,
12751316
just like with print(). This is useful for raw text and logs.
12761317
If False, Rich wraps text to fit the terminal width.
@@ -1279,24 +1320,43 @@ def print_to(
12791320
For example, when soft_wrap is True Panels truncate text
12801321
which is wider than the terminal.
12811322
:param emoji: If True, Rich will replace emoji codes (e.g., :smiley:) with their
1282-
corresponding Unicode characters. Defaults to False.
1323+
corresponding Unicode characters. Defaults to None.
1324+
If None, the destination console's default behavior is used.
12831325
:param markup: If True, Rich will interpret strings with tags (e.g., [bold]hello[/bold])
1284-
as styled output. Defaults to False.
1326+
as styled output. Defaults to None.
1327+
If None, the destination console's default behavior is used.
12851328
:param highlight: If True, Rich will automatically apply highlighting to elements within
12861329
strings, such as common Python data types like numbers, booleans, or None.
12871330
This is particularly useful when pretty printing objects like lists and
1288-
dictionaries to display them in color. Defaults to False.
1331+
dictionaries to display them in color. Defaults to None.
1332+
If None, the destination console's default behavior is used.
12891333
:param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print().
12901334
:param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this
12911335
method and still call `super()` without encountering unexpected keyword argument errors.
12921336
These arguments are not passed to Rich's Console.print().
1337+
:raises TypeError: If ``destination`` is a non-cmd2 ``Console`` instance that
1338+
does not derive from ``Cmd2BaseConsole``.
12931339
1294-
See the Rich documentation for more details on emoji codes, markup tags, and highlighting.
12951340
"""
1341+
if isinstance(destination, Console):
1342+
if not isinstance(destination, Cmd2BaseConsole):
1343+
# Explicitly reject non-cmd2 consoles to ensure safe behavior
1344+
raise TypeError(
1345+
f"destination must be a 'Cmd2BaseConsole' or a file-like object, "
1346+
f"not a non-cmd2 '{type(destination).__name__}'. "
1347+
"Consoles not derived from 'Cmd2BaseConsole' bypass cmd2's "
1348+
"'ALLOW_STYLE' logic, 'APP_THEME' settings, and trigger 'SystemExit' "
1349+
"on broken pipes."
1350+
)
1351+
console = destination
1352+
else:
1353+
# It's a file-like object (e.g., sys.stdout, StringIO)
1354+
console = Cmd2GeneralConsole(file=destination)
1355+
12961356
prepared_objects = ru.prepare_objects_for_rendering(*objects)
12971357

12981358
try:
1299-
Cmd2GeneralConsole(file).print(
1359+
console.print(
13001360
*prepared_objects,
13011361
sep=sep,
13021362
end=end,
@@ -1313,19 +1373,19 @@ def print_to(
13131373
# writing. If you would like your application to print a
13141374
# warning message, then set the broken_pipe_warning attribute
13151375
# to the message you want printed.
1316-
if self.broken_pipe_warning and file != sys.stderr:
1317-
Cmd2GeneralConsole(sys.stderr).print(self.broken_pipe_warning)
1376+
if self.broken_pipe_warning and console.file != sys.stderr:
1377+
Cmd2GeneralConsole(file=sys.stderr).print(self.broken_pipe_warning)
13181378

13191379
def poutput(
13201380
self,
13211381
*objects: Any,
13221382
sep: str = " ",
13231383
end: str = "\n",
13241384
style: StyleType | None = None,
1325-
soft_wrap: bool = True,
1326-
emoji: bool = False,
1327-
markup: bool = False,
1328-
highlight: bool = False,
1385+
soft_wrap: bool | None = None,
1386+
emoji: bool | None = None,
1387+
markup: bool | None = None,
1388+
highlight: bool | None = None,
13291389
rich_print_kwargs: RichPrintKwargs | None = None,
13301390
**kwargs: Any, # noqa: ARG002
13311391
) -> None:
@@ -1352,10 +1412,10 @@ def perror(
13521412
sep: str = " ",
13531413
end: str = "\n",
13541414
style: StyleType | None = Cmd2Style.ERROR,
1355-
soft_wrap: bool = True,
1356-
emoji: bool = False,
1357-
markup: bool = False,
1358-
highlight: bool = False,
1415+
soft_wrap: bool | None = None,
1416+
emoji: bool | None = None,
1417+
markup: bool | None = None,
1418+
highlight: bool | None = None,
13591419
rich_print_kwargs: RichPrintKwargs | None = None,
13601420
**kwargs: Any, # noqa: ARG002
13611421
) -> None:
@@ -1383,10 +1443,10 @@ def psuccess(
13831443
*objects: Any,
13841444
sep: str = " ",
13851445
end: str = "\n",
1386-
soft_wrap: bool = True,
1387-
emoji: bool = False,
1388-
markup: bool = False,
1389-
highlight: bool = False,
1446+
soft_wrap: bool | None = None,
1447+
emoji: bool | None = None,
1448+
markup: bool | None = None,
1449+
highlight: bool | None = None,
13901450
rich_print_kwargs: RichPrintKwargs | None = None,
13911451
**kwargs: Any, # noqa: ARG002
13921452
) -> None:
@@ -1411,10 +1471,10 @@ def pwarning(
14111471
*objects: Any,
14121472
sep: str = " ",
14131473
end: str = "\n",
1414-
soft_wrap: bool = True,
1415-
emoji: bool = False,
1416-
markup: bool = False,
1417-
highlight: bool = False,
1474+
soft_wrap: bool | None = None,
1475+
emoji: bool | None = None,
1476+
markup: bool | None = None,
1477+
highlight: bool | None = None,
14181478
rich_print_kwargs: RichPrintKwargs | None = None,
14191479
**kwargs: Any, # noqa: ARG002
14201480
) -> None:
@@ -1447,7 +1507,7 @@ def pexcept(
14471507
:param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this
14481508
method and still call `super()` without encountering unexpected keyword argument errors.
14491509
"""
1450-
console = Cmd2ExceptionConsole(sys.stderr)
1510+
console = Cmd2ExceptionConsole(file=sys.stderr)
14511511

14521512
# Only print a traceback if we're in debug mode and one exists.
14531513
if self.debug and sys.exc_info() != (None, None, None):
@@ -1494,10 +1554,10 @@ def pfeedback(
14941554
sep: str = " ",
14951555
end: str = "\n",
14961556
style: StyleType | None = None,
1497-
soft_wrap: bool = True,
1498-
emoji: bool = False,
1499-
markup: bool = False,
1500-
highlight: bool = False,
1557+
soft_wrap: bool | None = None,
1558+
emoji: bool | None = None,
1559+
markup: bool | None = None,
1560+
highlight: bool | None = None,
15011561
rich_print_kwargs: RichPrintKwargs | None = None,
15021562
**kwargs: Any, # noqa: ARG002
15031563
) -> None:
@@ -1542,9 +1602,9 @@ def ppaged(
15421602
style: StyleType | None = None,
15431603
chop: bool = False,
15441604
soft_wrap: bool = True,
1545-
emoji: bool = False,
1546-
markup: bool = False,
1547-
highlight: bool = False,
1605+
emoji: bool | None = None,
1606+
markup: bool | None = None,
1607+
highlight: bool | None = None,
15481608
rich_print_kwargs: RichPrintKwargs | None = None,
15491609
**kwargs: Any, # noqa: ARG002
15501610
) -> None:
@@ -1581,17 +1641,16 @@ def ppaged(
15811641

15821642
# Check if we are outputting to a pager.
15831643
if functional_terminal and can_block:
1584-
prepared_objects = ru.prepare_objects_for_rendering(*objects)
1585-
15861644
# Chopping overrides soft_wrap
15871645
if chop:
15881646
soft_wrap = True
15891647

15901648
# Generate the bytes to send to the pager
1591-
console = Cmd2GeneralConsole(self.stdout)
1649+
console = Cmd2GeneralConsole(file=self.stdout)
15921650
with console.capture() as capture:
1593-
console.print(
1594-
*prepared_objects,
1651+
self.print_to(
1652+
console,
1653+
*objects,
15951654
sep=sep,
15961655
end=end,
15971656
style=style,

cmd2/rich_utils.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class Cmd2BaseConsole(Console):
124124

125125
def __init__(
126126
self,
127+
*,
127128
file: IO[str] | None = None,
128129
**kwargs: Any,
129130
) -> None:
@@ -180,17 +181,19 @@ def on_broken_pipe(self) -> None:
180181

181182

182183
class Cmd2GeneralConsole(Cmd2BaseConsole):
183-
"""Rich console for general-purpose printing."""
184+
"""Rich console for general-purpose printing.
184185
185-
def __init__(self, file: IO[str] | None = None) -> None:
186+
It enables soft wrap and disables Rich's automatic detection for markup,
187+
emoji, and highlighting. These defaults can be overridden in calls to the
188+
console's or cmd2's print methods.
189+
"""
190+
191+
def __init__(self, *, file: IO[str] | None = None) -> None:
186192
"""Cmd2GeneralConsole initializer.
187193
188194
:param file: optional file object where the console should write to.
189195
Defaults to sys.stdout.
190196
"""
191-
# This console is configured for general-purpose printing. It enables soft wrap
192-
# and disables Rich's automatic detection for markup, emoji, and highlighting.
193-
# These defaults can be overridden in calls to the console's or cmd2's print methods.
194197
super().__init__(
195198
file=file,
196199
soft_wrap=True,
@@ -203,35 +206,53 @@ def __init__(self, file: IO[str] | None = None) -> None:
203206
class Cmd2RichArgparseConsole(Cmd2BaseConsole):
204207
"""Rich console for rich-argparse output.
205208
206-
This class ensures long lines in help text are not truncated by avoiding soft_wrap,
209+
Ensures long lines in help text are not truncated by disabling soft_wrap,
207210
which conflicts with rich-argparse's explicit no_wrap and overflow settings.
211+
212+
Since this console is used to print error messages which may not be intended
213+
for Rich formatting, it disables Rich's automatic detection for markup, emoji,
214+
and highlighting. Because rich-argparse does markup and highlighting without
215+
involving the console, disabling these settings does not affect the library's
216+
internal functionality.
208217
"""
209218

210-
def __init__(self, file: IO[str] | None = None) -> None:
219+
def __init__(self, *, file: IO[str] | None = None) -> None:
211220
"""Cmd2RichArgparseConsole initializer.
212221
213222
:param file: optional file object where the console should write to.
214223
Defaults to sys.stdout.
215224
"""
216-
# Since this console is used to print error messages which may not have
217-
# been pre-formatted by rich-argparse, disable Rich's automatic detection
218-
# for markup, emoji, and highlighting. rich-argparse does markup and
219-
# highlighting without involving the console so these won't affect its
220-
# internal functionality.
221225
super().__init__(
222226
file=file,
227+
soft_wrap=False,
223228
markup=False,
224229
emoji=False,
225230
highlight=False,
226231
)
227232

228233

229234
class Cmd2ExceptionConsole(Cmd2BaseConsole):
230-
"""Rich console for printing exceptions.
235+
"""Rich console for printing exceptions and Rich Tracebacks.
231236
232-
Ensures that long exception messages word wrap for readability by keeping soft_wrap disabled.
237+
Ensures that output is always word-wrapped for readability and disables
238+
Rich's automatic detection for markup, emoji, and highlighting to prevent
239+
interference with raw error data.
233240
"""
234241

242+
def __init__(self, *, file: IO[str] | None = None) -> None:
243+
"""Cmd2ExceptionConsole initializer.
244+
245+
:param file: optional file object where the console should write to.
246+
Defaults to sys.stdout.
247+
"""
248+
super().__init__(
249+
file=file,
250+
soft_wrap=False,
251+
markup=False,
252+
emoji=False,
253+
highlight=False,
254+
)
255+
235256

236257
def console_width() -> int:
237258
"""Return the width of the console."""

0 commit comments

Comments
 (0)