Skip to content

Commit 4448899

Browse files
committed
Moved rich-argparse classes from argparse_utils to rich_utils to eliminate circular dependencies.
1 parent 3d3034c commit 4448899

9 files changed

Lines changed: 463 additions & 428 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,18 @@ prompt is displayed.
8484
- `TextGroup` is now a standalone Rich renderable.
8585
- Removed `formatter_creator` parameter from `TextGroup.__init__()`.
8686
- Removed `Cmd2ArgumentParser.create_text_group()` method.
87-
- Renamed `argparse_custom` module to `argparse_utils`.
87+
- `argparse` and `Rich` integration refactoring:
88+
- Renamed `argparse_custom` module to `argparse_utils`.
89+
- Moved the following classes from `argparse_utils` to `rich_utils`:
90+
- `Cmd2HelpFormatter`
91+
- `ArgumentDefaultsCmd2HelpFormatter`
92+
- `MetavarTypeCmd2HelpFormatter`
93+
- `RawDescriptionCmd2HelpFormatter`
94+
- `RawTextCmd2HelpFormatter`
95+
- `TextGroup`
96+
- Replaced the global `APP_THEME` constant in `rich_utils.py` with `get_theme()` and
97+
`set_theme()` functions to support lazy initialization and safer in-place updates of the
98+
theme.
8899
- Enhancements
89100
- New `cmd2.Cmd` parameters
90101
- **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These

cmd2/__init__.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from .argparse_completer import set_default_ap_completer_type
1515
from .argparse_utils import (
1616
Cmd2ArgumentParser,
17-
TextGroup,
1817
register_argparse_argument_parameter,
1918
set_default_argument_parser_type,
2019
)
@@ -45,7 +44,17 @@
4544
)
4645
from .parsing import Statement
4746
from .py_bridge import CommandResult
48-
from .rich_utils import RichPrintKwargs
47+
from .rich_utils import (
48+
ArgumentDefaultsCmd2HelpFormatter,
49+
Cmd2HelpFormatter,
50+
MetavarTypeCmd2HelpFormatter,
51+
RawDescriptionCmd2HelpFormatter,
52+
RawTextCmd2HelpFormatter,
53+
RichPrintKwargs,
54+
TextGroup,
55+
get_theme,
56+
set_theme,
57+
)
4958
from .string_utils import stylize
5059
from .styles import Cmd2Style
5160
from .utils import (
@@ -60,7 +69,6 @@
6069
'DEFAULT_SHORTCUTS',
6170
# Argparse Exports
6271
'Cmd2ArgumentParser',
63-
'TextGroup',
6472
'register_argparse_argument_parameter',
6573
'set_default_ap_completer_type',
6674
'set_default_argument_parser_type',
@@ -91,7 +99,15 @@
9199
'rich_utils',
92100
'string_utils',
93101
# Rich Utils
102+
'ArgumentDefaultsCmd2HelpFormatter',
103+
'Cmd2HelpFormatter',
104+
'get_theme',
105+
'MetavarTypeCmd2HelpFormatter',
106+
'RawDescriptionCmd2HelpFormatter',
107+
'RawTextCmd2HelpFormatter',
94108
'RichPrintKwargs',
109+
'set_theme',
110+
'TextGroup',
95111
# String Utils
96112
'stylize',
97113
# Styles,

cmd2/argparse_utils.py

Lines changed: 2 additions & 243 deletions
Original file line numberDiff line numberDiff line change
@@ -231,38 +231,21 @@ def get_choices(self) -> Choices:
231231
from collections.abc import (
232232
Callable,
233233
Iterable,
234-
Iterator,
235234
Sequence,
236235
)
237236
from typing import (
238237
TYPE_CHECKING,
239238
Any,
240-
ClassVar,
241239
NoReturn,
242240
cast,
243241
)
244242

245-
from rich.console import (
246-
Console,
247-
ConsoleOptions,
248-
Group,
249-
RenderableType,
250-
RenderResult,
251-
)
243+
from rich.console import RenderableType
252244
from rich.table import Column
253-
from rich.text import Text
254-
from rich_argparse import (
255-
ArgumentDefaultsRichHelpFormatter,
256-
MetavarTypeRichHelpFormatter,
257-
RawDescriptionRichHelpFormatter,
258-
RawTextRichHelpFormatter,
259-
RichHelpFormatter,
260-
)
261245

262246
from . import constants
263-
from . import rich_utils as ru
264247
from .completion import CompletionItem
265-
from .rich_utils import Cmd2RichArgparseConsole
248+
from .rich_utils import Cmd2HelpFormatter
266249
from .styles import Cmd2Style
267250
from .types import (
268251
CmdOrSetT,
@@ -512,230 +495,6 @@ def _ActionsContainer_add_argument( # noqa: N802
512495
argparse._ActionsContainer.add_argument = _ActionsContainer_add_argument # type: ignore[method-assign]
513496

514497

515-
############################################################################################################
516-
# Unless otherwise noted, everything below this point are copied from Python's
517-
# argparse implementation with minor tweaks to adjust output.
518-
# Changes are noted if it's buried in a block of copied code. Otherwise the
519-
# function will check for a special case and fall back to the parent function
520-
############################################################################################################
521-
522-
523-
class Cmd2HelpFormatter(RichHelpFormatter):
524-
"""Custom help formatter to configure ordering of help text."""
525-
526-
# Disable automatic highlighting in the help text.
527-
highlights: ClassVar[list[str]] = []
528-
529-
# Disable markup rendering in usage, help, description, and epilog text.
530-
# cmd2's built-in commands do not escape opening brackets in their help text
531-
# and therefore rely on these settings being False. If you desire to use
532-
# markup in your help text, inherit from Cmd2HelpFormatter and override
533-
# these settings in that child class.
534-
usage_markup: ClassVar[bool] = False
535-
help_markup: ClassVar[bool] = False
536-
text_markup: ClassVar[bool] = False
537-
538-
def __init__(
539-
self,
540-
prog: str,
541-
indent_increment: int = 2,
542-
max_help_position: int = 24,
543-
width: int | None = None,
544-
*,
545-
console: Cmd2RichArgparseConsole | None = None,
546-
**kwargs: Any,
547-
) -> None:
548-
"""Initialize Cmd2HelpFormatter."""
549-
super().__init__(prog, indent_increment, max_help_position, width, console=console, **kwargs)
550-
551-
# Recast to assist type checkers
552-
self._console: Cmd2RichArgparseConsole | None
553-
554-
@property # type: ignore[override]
555-
def console(self) -> Cmd2RichArgparseConsole:
556-
"""Return our console instance."""
557-
if self._console is None:
558-
self._console = Cmd2RichArgparseConsole()
559-
return self._console
560-
561-
@console.setter
562-
def console(self, console: Cmd2RichArgparseConsole) -> None:
563-
"""Set our console instance."""
564-
self._console = console
565-
566-
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
567-
"""Provide this help formatter to renderables via the console."""
568-
if isinstance(console, Cmd2RichArgparseConsole):
569-
old_formatter = console.help_formatter
570-
console.help_formatter = self
571-
try:
572-
yield from super().__rich_console__(console, options)
573-
finally:
574-
console.help_formatter = old_formatter
575-
else:
576-
# Handle rendering on a console type other than Cmd2RichArgparseConsole.
577-
# In this case, we don't set the help_formatter on the console.
578-
yield from super().__rich_console__(console, options)
579-
580-
def _set_color(self, color: bool, **kwargs: Any) -> None:
581-
"""Set the color for the help output.
582-
583-
This override is needed because Python 3.15 added a 'file' keyword argument
584-
to _set_color() which some versions of RichHelpFormatter don't support.
585-
"""
586-
# Argparse didn't add color support until 3.14
587-
if sys.version_info < (3, 14):
588-
return
589-
590-
try: # type: ignore[unreachable]
591-
super()._set_color(color, **kwargs)
592-
except TypeError:
593-
# Fallback for older versions of RichHelpFormatter that don't support keyword arguments
594-
super()._set_color(color)
595-
596-
def _build_nargs_range_str(self, nargs_range: tuple[int, int | float]) -> str:
597-
"""Build nargs range string for help text."""
598-
if nargs_range[1] == constants.INFINITY:
599-
# {min+}
600-
range_str = f"{{{nargs_range[0]}+}}"
601-
else:
602-
# {min..max}
603-
range_str = f"{{{nargs_range[0]}..{nargs_range[1]}}}"
604-
605-
return range_str
606-
607-
def _format_args(self, action: argparse.Action, default_metavar: str) -> str:
608-
"""Override to handle cmd2's custom nargs formatting.
609-
610-
All formats in this function need to be handled by _rich_metavar_parts().
611-
"""
612-
get_metavar = self._metavar_formatter(action, default_metavar)
613-
614-
# Handle nargs specified as a range
615-
nargs_range = action.get_nargs_range() # type: ignore[attr-defined]
616-
if nargs_range is not None:
617-
arg_str = '%s' % get_metavar(1) # noqa: UP031
618-
range_str = self._build_nargs_range_str(nargs_range)
619-
return f"{arg_str}{range_str}"
620-
621-
# When nargs is just a number, argparse repeats the arg in the help text.
622-
# For instance, when nargs=5 the help text looks like: 'command arg arg arg arg arg'.
623-
# To make this less verbose, format it like: 'command arg{5}'.
624-
# Do not customize the output when metavar is a tuple of strings. Allow argparse's
625-
# formatter to handle that instead.
626-
if not isinstance(action.metavar, tuple) and isinstance(action.nargs, int) and action.nargs > 1:
627-
arg_str = '%s' % get_metavar(1) # noqa: UP031
628-
return f"{arg_str}{{{action.nargs}}}"
629-
630-
# Fallback to parent for all other cases
631-
return super()._format_args(action, default_metavar)
632-
633-
def _rich_metavar_parts(
634-
self,
635-
action: argparse.Action,
636-
default_metavar: str,
637-
) -> Iterator[tuple[str, bool]]:
638-
"""Override to handle all cmd2-specific formatting in _format_args()."""
639-
get_metavar = self._metavar_formatter(action, default_metavar)
640-
641-
# Handle nargs specified as a range
642-
nargs_range = action.get_nargs_range() # type: ignore[attr-defined]
643-
if nargs_range is not None:
644-
yield "%s" % get_metavar(1), True # noqa: UP031
645-
yield self._build_nargs_range_str(nargs_range), False
646-
return
647-
648-
# Handle specific integer nargs (e.g., nargs=5 -> arg{5})
649-
if not isinstance(action.metavar, tuple) and isinstance(action.nargs, int) and action.nargs > 1:
650-
yield "%s" % get_metavar(1), True # noqa: UP031
651-
yield f"{{{action.nargs}}}", False
652-
return
653-
654-
# Fallback to parent for all other cases
655-
yield from super()._rich_metavar_parts(action, default_metavar)
656-
657-
658-
class RawDescriptionCmd2HelpFormatter(
659-
RawDescriptionRichHelpFormatter,
660-
Cmd2HelpFormatter,
661-
):
662-
"""Cmd2 help message formatter which retains any formatting in descriptions and epilogs."""
663-
664-
665-
class RawTextCmd2HelpFormatter(
666-
RawTextRichHelpFormatter,
667-
Cmd2HelpFormatter,
668-
):
669-
"""Cmd2 help message formatter which retains formatting of all help text."""
670-
671-
672-
class ArgumentDefaultsCmd2HelpFormatter(
673-
ArgumentDefaultsRichHelpFormatter,
674-
Cmd2HelpFormatter,
675-
):
676-
"""Cmd2 help message formatter which adds default values to argument help."""
677-
678-
679-
class MetavarTypeCmd2HelpFormatter(
680-
MetavarTypeRichHelpFormatter,
681-
Cmd2HelpFormatter,
682-
):
683-
"""Cmd2 help message formatter which uses the argument 'type' as the default
684-
metavar value (instead of the argument 'dest').
685-
""" # noqa: D205
686-
687-
688-
class TextGroup:
689-
"""A block of text which is formatted like an argparse argument group, including a title.
690-
691-
Title:
692-
Here is the first row of text.
693-
Here is yet another row of text.
694-
"""
695-
696-
def __init__(
697-
self,
698-
title: str,
699-
text: RenderableType,
700-
) -> None:
701-
"""TextGroup initializer.
702-
703-
:param title: the group's title
704-
:param text: the group's text (string or object that may be rendered by Rich)
705-
"""
706-
self.title = title
707-
self.text = text
708-
709-
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
710-
"""Return a renderable Rich Group object for the class instance.
711-
712-
This method formats the title and indents the text to match argparse
713-
group styling, making the object displayable by a Rich console.
714-
"""
715-
formatter: Cmd2HelpFormatter | None = None
716-
if isinstance(console, Cmd2RichArgparseConsole):
717-
formatter = console.help_formatter
718-
719-
# This occurs if the console is not a Cmd2RichArgparseConsole or if the
720-
# TextGroup is printed directly instead of as part of an argparse help message.
721-
if formatter is None:
722-
# If console is the wrong type, then have Cmd2HelpFormatter create its own.
723-
formatter = Cmd2HelpFormatter(
724-
prog="",
725-
console=console if isinstance(console, Cmd2RichArgparseConsole) else None,
726-
)
727-
728-
styled_title = Text(
729-
type(formatter).group_name_formatter(f"{self.title}:"),
730-
style=formatter.styles["argparse.groups"],
731-
)
732-
733-
# Indent text like an argparse argument group does
734-
indented_text = ru.indent(self.text, formatter._indent_increment)
735-
736-
yield Group(styled_title, indented_text)
737-
738-
739498
class Cmd2ArgumentParser(argparse.ArgumentParser):
740499
"""Custom ArgumentParser class that improves error and help output."""
741500

cmd2/cmd2.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,7 @@
108108
)
109109
from . import rich_utils as ru
110110
from . import string_utils as su
111-
from .argparse_utils import (
112-
Cmd2ArgumentParser,
113-
TextGroup,
114-
)
111+
from .argparse_utils import Cmd2ArgumentParser
115112
from .clipboard import (
116113
get_paste_buffer,
117114
write_to_paste_buffer,
@@ -160,6 +157,7 @@
160157
Cmd2GeneralConsole,
161158
Cmd2SimpleTable,
162159
RichPrintKwargs,
160+
TextGroup,
163161
)
164162
from .styles import Cmd2Style
165163
from .types import (
@@ -5122,7 +5120,7 @@ def _build_history_parser(cls) -> Cmd2ArgumentParser:
51225120
history_description = "View, run, edit, save, or clear previously entered commands."
51235121

51245122
history_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(
5125-
description=history_description, formatter_class=argparse_utils.RawTextCmd2HelpFormatter
5123+
description=history_description, formatter_class=ru.RawTextCmd2HelpFormatter
51265124
)
51275125
history_action_group = history_parser.add_mutually_exclusive_group()
51285126
history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items')

0 commit comments

Comments
 (0)