Skip to content

Commit f9a0acc

Browse files
committed
Cache stdout and stderr consoles to avoid creating new instances each time we print.
1 parent 7fbee93 commit f9a0acc

2 files changed

Lines changed: 84 additions & 12 deletions

File tree

cmd2/cmd2.py

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,14 @@ class AsyncAlert:
308308
timestamp: float = field(default_factory=time.monotonic, init=False)
309309

310310

311+
class _ConsoleCache(threading.local):
312+
"""Thread-local storage for cached Rich consoles used by print_to()."""
313+
314+
def __init__(self) -> None:
315+
self.stdout: Cmd2BaseConsole | None = None
316+
self.stderr: Cmd2BaseConsole | None = None
317+
318+
311319
class Cmd:
312320
"""An easy but powerful framework for writing line-oriented command interpreters.
313321
@@ -441,6 +449,9 @@ def __init__(
441449
self.scripts_add_to_history = True # Scripts and pyscripts add commands to history
442450
self.timing = False # Prints elapsed time for each command
443451

452+
# Thread-local storage for cached Rich consoles used by print_to()
453+
self._console_cache = _ConsoleCache()
454+
444455
# The maximum number of items to display in a completion table. If the number of completion
445456
# suggestions exceeds this number, then no table will appear.
446457
self.max_completion_table_items: int = 50
@@ -1332,20 +1343,48 @@ def _create_base_printing_console(
13321343
markup: bool,
13331344
highlight: bool,
13341345
) -> Cmd2BaseConsole:
1335-
"""Create a Cmd2BaseConsole with formatting overrides.
1346+
"""Get a Cmd2BaseConsole configured for the specified stream and formatting settings.
1347+
1348+
This method manages a thread-local cache for consoles printing to self.stdout or
1349+
sys.stderr to avoid the overhead of repeated initialization. It returns a cached
1350+
instance if its configuration matches the request. Otherwise, a new console is
1351+
created and cached.
1352+
1353+
Note: This implementation works around a bug in Rich where passing formatting settings
1354+
(emoji, markup, and highlight) directly to console.print() or console.log() does not
1355+
always work when printing certain Renderables. Passing them to the constructor instead
1356+
ensures they are correctly propagated. Once this bug is fixed, these parameters can
1357+
be removed from this method. For more details, see:
1358+
https://github.com/Textualize/rich/issues/4028
1359+
"""
1360+
# Dictionary of settings to check against cached consoles
1361+
kwargs = {
1362+
"emoji": emoji,
1363+
"markup": markup,
1364+
"highlight": highlight,
1365+
}
13361366

1337-
This works around a bug in Rich where passing these formatting settings directly to
1338-
console.print() or console.log() does not always work when printing certain Renderables.
1339-
Passing them to the constructor instead ensures they are correctly propagated.
1367+
# Check if we should use or update a cached console
1368+
if file is self.stdout:
1369+
cached = self._console_cache.stdout
1370+
if cached is not None and cached.matches_config(file, **kwargs):
1371+
return cached
13401372

1341-
See: https://github.com/Textualize/rich/issues/4028
1342-
"""
1343-
return Cmd2BaseConsole(
1344-
file=file,
1345-
emoji=emoji,
1346-
markup=markup,
1347-
highlight=highlight,
1348-
)
1373+
# Create new console and update cache
1374+
self._console_cache.stdout = Cmd2BaseConsole(file=file, **kwargs)
1375+
return self._console_cache.stdout
1376+
1377+
if file is sys.stderr:
1378+
cached = self._console_cache.stderr
1379+
if cached is not None and cached.matches_config(file, **kwargs):
1380+
return cached
1381+
1382+
# Create new console and update cache
1383+
self._console_cache.stderr = Cmd2BaseConsole(file=file, **kwargs)
1384+
return self._console_cache.stderr
1385+
1386+
# For any other file, just create a new console
1387+
return Cmd2BaseConsole(file=file, **kwargs)
13491388

13501389
def print_to(
13511390
self,

cmd2/rich_utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ def __init__(
160160
"Passing 'theme' is not allowed. Its behavior is controlled by the global APP_THEME and set_theme()."
161161
)
162162

163+
# Store the configuration used to create this console for caching purposes.
164+
self._config_key = self._generate_config_key(file, kwargs)
165+
163166
force_terminal: bool | None = None
164167
force_interactive: bool | None = None
165168

@@ -180,6 +183,36 @@ def __init__(
180183
**kwargs,
181184
)
182185

186+
@staticmethod
187+
def _generate_config_key(
188+
file: IO[str] | None,
189+
kwargs: dict[str, Any],
190+
) -> tuple[Any, ...]:
191+
"""Generate a key representing the settings used to initialize a console.
192+
193+
This key includes the file identity, global settings (ALLOW_STYLE, APP_THEME),
194+
and any other settings passed in via kwargs.
195+
"""
196+
return (
197+
id(file),
198+
ALLOW_STYLE,
199+
id(APP_THEME),
200+
tuple(sorted(kwargs.items())),
201+
)
202+
203+
def matches_config(
204+
self,
205+
file: IO[str] | None,
206+
**kwargs: Any,
207+
) -> bool:
208+
"""Check if this console instance is compatible with the given settings.
209+
210+
:param file: file stream being checked
211+
:param kwargs: formatting settings being checked
212+
:return: True if the settings match this console's configuration
213+
"""
214+
return self._config_key == self._generate_config_key(file, kwargs)
215+
183216
def on_broken_pipe(self) -> None:
184217
"""Override which raises BrokenPipeError instead of SystemExit."""
185218
self.quiet = True

0 commit comments

Comments
 (0)