Skip to content

Commit 2829a9c

Browse files
authored
Merge branch 'main' into update_subcommand_api
2 parents f61a80b + 5b3bc53 commit 2829a9c

15 files changed

Lines changed: 288 additions & 71 deletions

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ prompt is displayed.
9494
- `RawDescriptionCmd2HelpFormatter`
9595
- `RawTextCmd2HelpFormatter`
9696
- `TextGroup`
97-
- Replaced the global `APP_THEME` constant in `rich_utils.py` with `get_theme()` and
98-
`set_theme()` functions in `theme.py` to support lazy initialization and safer in-place
99-
updates of the theme.
97+
- Replaced the global `APP_THEME` constant in `rich_utils.py` with `get_theme()`,
98+
`reset_theme()`, and `update_theme()` functions in `theme.py` to support lazy
99+
initialization and safer in-place updates of the theme.
100100
- Renamed `Cmd._command_parsers` to `Cmd.command_parsers`.
101101
- Removed `RichPrintKwargs` `TypedDict` in favor of using `Mapping[str, Any]`, allowing for
102102
greater flexibility in passing keyword arguments to `console.print()` calls.

cmd2/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@
5757
from .styles import Cmd2Style
5858
from .theme import (
5959
get_theme,
60-
set_theme,
60+
reset_theme,
61+
update_theme,
6162
)
6263
from .utils import (
6364
CustomCompletionSettings,
@@ -114,7 +115,8 @@
114115
"Cmd2Style",
115116
# Theme
116117
"get_theme",
117-
"set_theme",
118+
"reset_theme",
119+
"update_theme",
118120
# Utilities
119121
"categorize",
120122
"CustomCompletionSettings",

cmd2/cmd2.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1341,7 +1341,21 @@ def allow_style_type(value: str) -> ru.AllowStyle:
13411341
)
13421342
self.add_settable(Settable("debug", bool, "Show full traceback on exception", self))
13431343
self.add_settable(Settable("echo", bool, "Echo command issued into output", self))
1344-
self.add_settable(Settable("editor", str, "Program used by 'edit'", self))
1344+
1345+
editor_description = Text.assemble(
1346+
"Program used by ",
1347+
("'edit'", Style(bold=True)),
1348+
" command",
1349+
)
1350+
self.add_settable(
1351+
Settable(
1352+
"editor",
1353+
str,
1354+
ru.rich_text_to_string(editor_description),
1355+
self,
1356+
)
1357+
)
1358+
13451359
self.add_settable(
13461360
Settable(
13471361
"max_completion_table_items",
@@ -1363,6 +1377,20 @@ def allow_style_type(value: str) -> ru.AllowStyle:
13631377
self.add_settable(Settable("timing", bool, "Report execution times", self))
13641378
self.add_settable(Settable("traceback_show_locals", bool, "Display local variables in tracebacks", self))
13651379

1380+
traceback_width_description = Text.assemble(
1381+
"Maximum display width for tracebacks. Set to ",
1382+
("None", Style(bold=True)),
1383+
" (case-insensitive) to fill entire terminal width.",
1384+
)
1385+
self.add_settable(
1386+
Settable(
1387+
"traceback_width",
1388+
utils.optional_int,
1389+
ru.rich_text_to_string(traceback_width_description),
1390+
self,
1391+
)
1392+
)
1393+
13661394
@property
13671395
def allow_style(self) -> ru.AllowStyle:
13681396
"""Property needed to support do_set when it reads allow_style."""
@@ -1389,6 +1417,22 @@ def traceback_show_locals(self, value: bool) -> None:
13891417
"""Setter property needed to support do_set when it updates traceback_show_locals."""
13901418
self.traceback_kwargs["show_locals"] = value
13911419

1420+
@property
1421+
def traceback_width(self) -> int | None:
1422+
"""Property needed to support do_set when it reads traceback_width."""
1423+
if "width" in self.traceback_kwargs:
1424+
return cast(int | None, self.traceback_kwargs["width"])
1425+
1426+
# If setting is not present, then return its default value.
1427+
traceback_sig = inspect.signature(Traceback.__init__)
1428+
width = traceback_sig.parameters["width"].default
1429+
return cast(int | None, width)
1430+
1431+
@traceback_width.setter
1432+
def traceback_width(self, value: int | None) -> None:
1433+
"""Setter property needed to support do_set when it updates traceback_width."""
1434+
self.traceback_kwargs["width"] = value
1435+
13921436
@property
13931437
def visible_prompt(self) -> str:
13941438
"""Read-only property to get the visible prompt with any ANSI style sequences stripped.

cmd2/pt_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def get_completions(self, document: Document, _complete_event: object) -> Iterab
175175
print_formatted_text(pt_filter_style("\n" + capture.get()))
176176

177177
if not completions:
178-
# # Print hint if present
178+
# Print hint if present
179179
if completions.hint:
180180
print_formatted_text(pt_filter_style(completions.hint))
181181
return

cmd2/rich_utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ def __repr__(self) -> str:
107107
class Cmd2HelpFormatter(RichHelpFormatter):
108108
"""Custom help formatter to configure ordering of help text."""
109109

110-
# Have our own copy of the styles so set_theme() can synchronize them with
111-
# the cmd2 application theme without overwriting RichHelpFormatter's defaults.
110+
# Create our own copy of the styles so cmd2 can synchronize them with
111+
# the application theme without overwriting RichHelpFormatter's defaults.
112112
styles: ClassVar[dict[str, StyleType]] = DEFAULT_ARGPARSE_STYLES.copy()
113113

114114
# Disable automatic highlighting in the help text.
@@ -341,10 +341,10 @@ def __init__(
341341
"Passing 'force_interactive' is not allowed. Its behavior is controlled by the 'ALLOW_STYLE' setting."
342342
)
343343

344-
# Don't allow a theme to be passed in, as it is controlled by get_theme() and set_theme().
345-
# Use set_theme() to set the global theme or use a temporary theme with console.use_theme().
344+
# Don't allow a theme to be passed in. Use update_theme() to modify the global theme
345+
# or use a temporary theme with console.use_theme().
346346
if "theme" in kwargs:
347-
raise TypeError("Passing 'theme' is not allowed. Its behavior is controlled by get_theme() and set_theme().")
347+
raise TypeError("Passing 'theme' is not allowed. Modify the global theme with update_theme().")
348348

349349
# Store the configuration key used by cmd2 to cache this console.
350350
self._config_key = self._build_config_key(file=file, **kwargs)

cmd2/styles.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
their own default styles.
1515
1616
For a complete theming experience, you can create a custom theme that includes
17-
styles from Rich and rich-argparse. The `cmd2.theme.set_theme()` function
17+
styles from Rich and rich-argparse. The `cmd2.theme.update_theme()` function
1818
automatically updates rich-argparse's styles with any custom styles provided in
1919
your theme dictionary, so you don't have to modify them directly.
2020

cmd2/theme.py

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
from typing import cast
2424

2525
from prompt_toolkit.styles import Style as PtStyle
26-
from rich.style import StyleType
26+
from rich.style import (
27+
Style,
28+
StyleType,
29+
)
2730
from rich.theme import Theme
2831

2932
from .pt_utils import rich_to_pt_style
@@ -35,16 +38,16 @@
3538
)
3639

3740
# The application-wide theme, defined using Rich's styling system.
38-
# Use get_theme() to access it and set_theme() to modify it.
41+
# Use get_theme() to access it.
42+
# Use reset_theme() and update_theme() to modify it.
3943
_THEME: Theme | None = None
4044

4145
# The prompt-toolkit version of the theme, synchronized from the Rich theme.
42-
# Use get_pt_theme() to access it. This object is automatically updated whenever
43-
# set_theme() is called.
46+
# Use get_pt_theme() to access it.
4447
_PT_THEME: PtStyle | None = None
4548

4649
# Maps style names to internal UI component names used by prompt-toolkit.
47-
# This allows developers to use application-specific style names in set_theme()
50+
# This allows developers to use application-specific style names in update_theme()
4851
# while ensuring the underlying prompt-toolkit UI is styled correctly.
4952
# Use register_pt_mapping() and unregister_pt_mapping() to manage these mappings.
5053
#
@@ -72,52 +75,73 @@
7275
def get_theme() -> Theme:
7376
"""Get the application-wide Rich theme. Initializes it on the first call."""
7477
if _THEME is None:
75-
set_theme()
78+
reset_theme()
7679
return cast(Theme, _THEME)
7780

7881

7982
def get_pt_theme() -> PtStyle:
8083
"""Get the application-wide prompt-toolkit style. Initializes it on the first call."""
8184
if _PT_THEME is None:
82-
set_theme()
85+
reset_theme()
8386
return cast(PtStyle, _PT_THEME)
8487

8588

86-
def set_theme(styles: Mapping[str, StyleType] | None = None) -> None:
87-
"""Set the application-wide theme.
89+
def reset_theme() -> None:
90+
"""Reset the application-wide theme to its initial state.
8891
89-
This function performs an in-place update of the existing Rich theme's
92+
This function performs an in-place reset of the existing Rich theme's
9093
styles. This ensures that any Console objects already using the theme
9194
will reflect the changes immediately without needing to be recreated.
9295
93-
It also automatically synchronizes the prompt-toolkit theme for any
94-
styles with registered prefixes or mapped UI components.
95-
96-
Call set_theme() with no arguments to reset to the default theme.
97-
This will clear any custom styles that were previously applied.
98-
99-
:param styles: optional mapping of style names to styles
96+
Changes are automatically propagated to all synchronized components.
10097
"""
10198
global _THEME # noqa: PLW0603
99+
100+
# Include default styles from cmd2, rich-argparse, and Rich.
101+
styles = DEFAULT_CMD2_STYLES.copy()
102+
styles.update(DEFAULT_ARGPARSE_STYLES)
103+
default_theme = Theme(styles, inherit=True)
104+
102105
if _THEME is None:
103-
_THEME = Theme()
106+
# Initial assignment
107+
_THEME = default_theme
108+
else:
109+
# Perform in-place reset to preserve existing references
110+
_THEME.styles.clear()
111+
_THEME.styles.update(default_theme.styles)
112+
113+
_sync_all()
114+
115+
116+
def update_theme(styles: Mapping[str, StyleType]) -> None:
117+
"""Update the existing theme.
118+
119+
This function performs an in-place update of the existing Rich theme's
120+
styles. This ensures that any Console objects already using the theme
121+
will reflect the changes immediately without needing to be recreated.
122+
123+
Changes are automatically propagated to all synchronized components.
104124
105-
# Start with a fresh copy of the default styles.
106-
unparsed_styles: dict[str, StyleType] = {}
107-
unparsed_styles.update(_create_default_theme().styles)
125+
:param styles: mapping of style names to styles
126+
"""
127+
# Convert any string styles to Style objects
128+
parsed_styles = {name: style if isinstance(style, Style) else Style.parse(style) for name, style in styles.items()}
129+
130+
# Perform in-place update to preserve existing references
131+
get_theme().styles.update(parsed_styles)
108132

109-
# Add the custom styles, which may contain unparsed strings
110-
if styles is not None:
111-
unparsed_styles.update(styles)
133+
_sync_all()
112134

113-
# Use Rich's Theme class to perform the parsing
114-
parsed_styles = Theme(unparsed_styles).styles
115135

116-
# Perform the in-place update with the results
117-
_THEME.styles.clear()
118-
_THEME.styles.update(parsed_styles)
136+
def _sync_all() -> None:
137+
"""Propagate the global theme to rich-argparse and prompt-toolkit.
138+
139+
If the theme hasn't been initialized yet, this is a no-op.
140+
"""
141+
if _THEME is None:
142+
return
119143

120-
# Synchronize rich-argparse styles with the main application theme.
144+
# Synchronize rich-argparse styles
121145
for name in Cmd2HelpFormatter.styles.keys() & _THEME.styles.keys():
122146
Cmd2HelpFormatter.styles[name] = _THEME.styles[name]
123147

@@ -126,11 +150,16 @@ def set_theme(styles: Mapping[str, StyleType] | None = None) -> None:
126150

127151

128152
def _sync_pt_theme() -> None:
129-
"""Build a new global PT style object based on the current Rich theme."""
130-
theme = get_theme()
153+
"""Build a new global prompt-toolkit style object based on the current Rich theme.
154+
155+
If the theme hasn't been initialized yet, this is a no-op.
156+
"""
157+
if _THEME is None:
158+
return
159+
131160
style_rules: list[tuple[str, str]] = []
132161

133-
for name, rich_style in theme.styles.items():
162+
for name, rich_style in _THEME.styles.items():
134163
# Only synchronize if it has a registered prefix or mapped UI component.
135164
is_framework_style = any(name.startswith(p) for p in _SYNCHRONIZED_PREFIXES)
136165
is_mapped_style = name in _PT_UI_MAP
@@ -149,16 +178,6 @@ def _sync_pt_theme() -> None:
149178
_PT_THEME = PtStyle(style_rules)
150179

151180

152-
def _create_default_theme() -> Theme:
153-
"""Create a default theme for the application.
154-
155-
This theme combines the default styles from cmd2, rich-argparse, and Rich.
156-
"""
157-
app_styles = DEFAULT_CMD2_STYLES.copy()
158-
app_styles.update(DEFAULT_ARGPARSE_STYLES)
159-
return Theme(app_styles, inherit=True)
160-
161-
162181
def register_pt_mapping(style_name: str, pt_ui_names: str | Iterable[str]) -> None:
163182
"""Map a Rich theme style name to one or more prompt-toolkit UI components.
164183

cmd2/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ def to_bool(val: Any) -> bool:
6464
return bool(val)
6565

6666

67+
def optional_int(val: Any) -> int | None:
68+
"""Convert a value to an integer or None if it's "None" (case-insensitive).
69+
70+
:param val: value being converted
71+
:return: int or None
72+
:raises ValueError: if the value is not "None" and cannot be converted to an integer
73+
"""
74+
if val is None:
75+
return None
76+
if isinstance(val, str) and val.lower() == "none":
77+
return None
78+
try:
79+
return int(val)
80+
except (ValueError, TypeError):
81+
raise ValueError("must be an integer or None (case-insensitive)") from None
82+
83+
6784
class Settable:
6885
"""Used to configure an attribute to be settable via the set command in the CLI."""
6986

docs/features/generating_output.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ all colors available to your `cmd2` application.
131131

132132
`cmd2` uses a `rich` [Theme](https://rich.readthedocs.io/en/stable/reference/theme.html) object to
133133
define styles for various UI elements. You can define your own custom theme using
134-
[cmd2.rich_utils.set_theme][]. See the
134+
[cmd2.theme.update_theme][]. See the
135135
[rich_theme.py](https://github.com/python-cmd2/cmd2/blob/main/examples/rich_theme.py) example for
136136
more information.
137137

docs/features/theme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Theme
22

33
`cmd2` provides the ability to configure an overall theme for your application using the
4-
[cmd2.rich_utils.set_theme][] function. This is based on the
4+
[cmd2.theme.update_theme][] function. This is based on the
55
[rich.theme](https://rich.readthedocs.io/en/stable/reference/theme.html) container for style
66
information. You can use this to brand your application and set an overall consistent look and feel
77
that is appealing to your user base.

0 commit comments

Comments
 (0)