Skip to content

Commit 9936bb7

Browse files
Merge branch 'main' into documentation_fixes
2 parents 015e9e2 + d819701 commit 9936bb7

File tree

6 files changed

+163
-52
lines changed

6 files changed

+163
-52
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
uses: astral-sh/setup-uv@v6
2929

3030
- name: Set up Python ${{ matrix.python-version }}
31-
uses: actions/setup-python@v5
31+
uses: actions/setup-python@v6
3232
with:
3333
python-version: ${{ matrix.python-version }}
3434
allow-prereleases: true

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ time reading the [rich documentation](https://rich.readthedocs.io/).
4444
- [argparse_example.py](https://github.com/python-cmd2/cmd2/blob/main/examples/argparse_example.py)
4545
- [command_sets.py](https://github.com/python-cmd2/cmd2/blob/main/examples/command_sets.py)
4646
- [getting_started.py](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py)
47+
- Optimized performance of terminal fixup during command finalization by replacing `stty sane`
48+
with `termios.tcsetattr`
4749

4850
- Bug Fixes
4951
- Fixed a redirection bug where `cmd2` could unintentionally overwrite an application's

cmd2/cmd2.py

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,19 @@ def __init__(
508508
# Commands that will run at the beginning of the command loop
509509
self._startup_commands: list[str] = []
510510

511+
# Store initial termios settings to restore after each command.
512+
# This is a faster way of accomplishing what "stty sane" does.
513+
self._initial_termios_settings = None
514+
if not sys.platform.startswith('win') and self.stdin.isatty():
515+
try:
516+
import io
517+
import termios
518+
519+
self._initial_termios_settings = termios.tcgetattr(self.stdin.fileno())
520+
except (ImportError, io.UnsupportedOperation, termios.error):
521+
# This can happen if termios isn't available or stdin is a pseudo-TTY
522+
self._initial_termios_settings = None
523+
511524
# If a startup script is provided and exists, then execute it in the startup commands
512525
if startup_script:
513526
startup_script = os.path.abspath(os.path.expanduser(startup_script))
@@ -1198,22 +1211,39 @@ def print_to(
11981211
end: str = "\n",
11991212
style: StyleType | None = None,
12001213
soft_wrap: bool = True,
1214+
emoji: bool = False,
1215+
markup: bool = False,
1216+
highlight: bool = False,
12011217
rich_print_kwargs: RichPrintKwargs | None = None,
12021218
**kwargs: Any, # noqa: ARG002
12031219
) -> None:
12041220
"""Print objects to a given file stream.
12051221
1222+
This method is configured for general-purpose printing. By default, it enables
1223+
soft wrap and disables Rich's automatic detection for markup, emoji, and highlighting.
1224+
These defaults can be overridden by passing explicit keyword arguments.
1225+
12061226
:param file: file stream being written to
12071227
:param objects: objects to print
12081228
:param sep: string to write between printed text. Defaults to " ".
12091229
:param end: string to write at end of printed text. Defaults to a newline.
12101230
:param style: optional style to apply to output
1211-
:param soft_wrap: Enable soft wrap mode. If True, lines of text will not be word-wrapped or cropped to
1212-
fit the terminal width. Defaults to True.
1231+
:param soft_wrap: Enable soft wrap mode. If True, lines of text will not be
1232+
word-wrapped or cropped to fit the terminal width. Defaults to True.
1233+
:param emoji: If True, Rich will replace emoji codes (e.g., :smiley:) with their
1234+
corresponding Unicode characters. Defaults to False.
1235+
:param markup: If True, Rich will interpret strings with tags (e.g., [bold]hello[/bold])
1236+
as styled output. Defaults to False.
1237+
:param highlight: If True, Rich will automatically apply highlighting to elements within
1238+
strings, such as common Python data types like numbers, booleans, or None.
1239+
This is particularly useful when pretty printing objects like lists and
1240+
dictionaries to display them in color. Defaults to False.
12131241
:param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print().
12141242
:param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this
12151243
method and still call `super()` without encountering unexpected keyword argument errors.
12161244
These arguments are not passed to Rich's Console.print().
1245+
1246+
See the Rich documentation for more details on emoji codes, markup tags, and highlighting.
12171247
"""
12181248
prepared_objects = ru.prepare_objects_for_rendering(*objects)
12191249

@@ -1224,6 +1254,9 @@ def print_to(
12241254
end=end,
12251255
style=style,
12261256
soft_wrap=soft_wrap,
1257+
emoji=emoji,
1258+
markup=markup,
1259+
highlight=highlight,
12271260
**(rich_print_kwargs if rich_print_kwargs is not None else {}),
12281261
)
12291262
except BrokenPipeError:
@@ -1242,6 +1275,9 @@ def poutput(
12421275
end: str = "\n",
12431276
style: StyleType | None = None,
12441277
soft_wrap: bool = True,
1278+
emoji: bool = False,
1279+
markup: bool = False,
1280+
highlight: bool = False,
12451281
rich_print_kwargs: RichPrintKwargs | None = None,
12461282
**kwargs: Any, # noqa: ARG002
12471283
) -> None:
@@ -1256,6 +1292,9 @@ def poutput(
12561292
end=end,
12571293
style=style,
12581294
soft_wrap=soft_wrap,
1295+
emoji=emoji,
1296+
markup=markup,
1297+
highlight=highlight,
12591298
rich_print_kwargs=rich_print_kwargs,
12601299
)
12611300

@@ -1266,6 +1305,9 @@ def perror(
12661305
end: str = "\n",
12671306
style: StyleType | None = Cmd2Style.ERROR,
12681307
soft_wrap: bool = True,
1308+
emoji: bool = False,
1309+
markup: bool = False,
1310+
highlight: bool = False,
12691311
rich_print_kwargs: RichPrintKwargs | None = None,
12701312
**kwargs: Any, # noqa: ARG002
12711313
) -> None:
@@ -1282,6 +1324,9 @@ def perror(
12821324
end=end,
12831325
style=style,
12841326
soft_wrap=soft_wrap,
1327+
emoji=emoji,
1328+
markup=markup,
1329+
highlight=highlight,
12851330
rich_print_kwargs=rich_print_kwargs,
12861331
)
12871332

@@ -1291,6 +1336,9 @@ def psuccess(
12911336
sep: str = " ",
12921337
end: str = "\n",
12931338
soft_wrap: bool = True,
1339+
emoji: bool = False,
1340+
markup: bool = False,
1341+
highlight: bool = False,
12941342
rich_print_kwargs: RichPrintKwargs | None = None,
12951343
**kwargs: Any, # noqa: ARG002
12961344
) -> None:
@@ -1304,6 +1352,9 @@ def psuccess(
13041352
end=end,
13051353
style=Cmd2Style.SUCCESS,
13061354
soft_wrap=soft_wrap,
1355+
emoji=emoji,
1356+
markup=markup,
1357+
highlight=highlight,
13071358
rich_print_kwargs=rich_print_kwargs,
13081359
)
13091360

@@ -1313,6 +1364,9 @@ def pwarning(
13131364
sep: str = " ",
13141365
end: str = "\n",
13151366
soft_wrap: bool = True,
1367+
emoji: bool = False,
1368+
markup: bool = False,
1369+
highlight: bool = False,
13161370
rich_print_kwargs: RichPrintKwargs | None = None,
13171371
**kwargs: Any, # noqa: ARG002
13181372
) -> None:
@@ -1326,6 +1380,9 @@ def pwarning(
13261380
end=end,
13271381
style=Cmd2Style.WARNING,
13281382
soft_wrap=soft_wrap,
1383+
emoji=emoji,
1384+
markup=markup,
1385+
highlight=highlight,
13291386
rich_print_kwargs=rich_print_kwargs,
13301387
)
13311388

@@ -1390,6 +1447,9 @@ def pfeedback(
13901447
end: str = "\n",
13911448
style: StyleType | None = None,
13921449
soft_wrap: bool = True,
1450+
emoji: bool = False,
1451+
markup: bool = False,
1452+
highlight: bool = False,
13931453
rich_print_kwargs: RichPrintKwargs | None = None,
13941454
**kwargs: Any, # noqa: ARG002
13951455
) -> None:
@@ -1408,6 +1468,9 @@ def pfeedback(
14081468
end=end,
14091469
style=style,
14101470
soft_wrap=soft_wrap,
1471+
emoji=emoji,
1472+
markup=markup,
1473+
highlight=highlight,
14111474
rich_print_kwargs=rich_print_kwargs,
14121475
)
14131476
else:
@@ -1417,6 +1480,9 @@ def pfeedback(
14171480
end=end,
14181481
style=style,
14191482
soft_wrap=soft_wrap,
1483+
emoji=emoji,
1484+
markup=markup,
1485+
highlight=highlight,
14201486
rich_print_kwargs=rich_print_kwargs,
14211487
)
14221488

@@ -1428,6 +1494,9 @@ def ppaged(
14281494
style: StyleType | None = None,
14291495
chop: bool = False,
14301496
soft_wrap: bool = True,
1497+
emoji: bool = False,
1498+
markup: bool = False,
1499+
highlight: bool = False,
14311500
rich_print_kwargs: RichPrintKwargs | None = None,
14321501
**kwargs: Any, # noqa: ARG002
14331502
) -> None:
@@ -1479,6 +1548,9 @@ def ppaged(
14791548
end=end,
14801549
style=style,
14811550
soft_wrap=soft_wrap,
1551+
emoji=emoji,
1552+
markup=markup,
1553+
highlight=highlight,
14821554
**(rich_print_kwargs if rich_print_kwargs is not None else {}),
14831555
)
14841556
output_bytes = capture.get().encode('utf-8', 'replace')
@@ -1503,6 +1575,9 @@ def ppaged(
15031575
end=end,
15041576
style=style,
15051577
soft_wrap=soft_wrap,
1578+
emoji=emoji,
1579+
markup=markup,
1580+
highlight=highlight,
15061581
rich_print_kwargs=rich_print_kwargs,
15071582
)
15081583

@@ -2760,14 +2835,15 @@ def onecmd_plus_hooks(
27602835

27612836
def _run_cmdfinalization_hooks(self, stop: bool, statement: Statement | None) -> bool:
27622837
"""Run the command finalization hooks."""
2763-
with self.sigint_protection:
2764-
if not sys.platform.startswith('win') and self.stdin.isatty():
2765-
# Before the next command runs, fix any terminal problems like those
2766-
# caused by certain binary characters having been printed to it.
2767-
import subprocess
2768-
2769-
proc = subprocess.Popen(['stty', 'sane']) # noqa: S607
2770-
proc.communicate()
2838+
if self._initial_termios_settings is not None and self.stdin.isatty():
2839+
import io
2840+
import termios
2841+
2842+
# Before the next command runs, fix any terminal problems like those
2843+
# caused by certain binary characters having been printed to it.
2844+
with self.sigint_protection, contextlib.suppress(io.UnsupportedOperation, termios.error):
2845+
# This can fail if stdin is a pseudo-TTY, in which case we just ignore it
2846+
termios.tcsetattr(self.stdin.fileno(), termios.TCSANOW, self._initial_termios_settings)
27712847

27722848
data = plugin.CommandFinalizationData(stop, statement)
27732849
for func in self._cmdfinalization_hooks:

cmd2/rich_utils.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,6 @@ class RichPrintKwargs(TypedDict, total=False):
110110
justify: JustifyMethod | None
111111
overflow: OverflowMethod | None
112112
no_wrap: bool | None
113-
markup: bool | None
114-
emoji: bool | None
115-
highlight: bool | None
116113
width: int | None
117114
height: int | None
118115
crop: bool
@@ -216,9 +213,11 @@ def __init__(self, file: IO[str] | None = None) -> None:
216213
:param file: optional file object where the console should write to.
217214
Defaults to sys.stdout.
218215
"""
219-
# Disable Rich's automatic detection for markup, emoji, and highlighting.
220-
# rich-argparse does markup and highlighting without involving the console
221-
# so these won't affect its internal functionality.
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.
222221
super().__init__(
223222
file=file,
224223
markup=False,
@@ -236,7 +235,7 @@ class Cmd2ExceptionConsole(Cmd2BaseConsole):
236235

237236
def console_width() -> int:
238237
"""Return the width of the console."""
239-
return Cmd2BaseConsole().width
238+
return Console().width
240239

241240

242241
def rich_text_to_string(text: Text) -> str:

0 commit comments

Comments
 (0)