Skip to content

Commit ce60ddb

Browse files
authored
Merge branch 'main' into feat/annotated-argparse
2 parents b9db781 + b726c52 commit ce60ddb

File tree

10 files changed

+243
-79
lines changed

10 files changed

+243
-79
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ jobs:
4040

4141
- name: Upload test results to Codecov
4242
if: ${{ !cancelled() }}
43-
uses: codecov/codecov-action@v5
43+
uses: codecov/codecov-action@v6
4444
with:
4545
flags: python${{ matrix.python-version }}
4646
name: codecov-umbrella-test-results
4747
report_type: test_results
4848
token: ${{ secrets.CODECOV_TOKEN }}
4949
- name: Upload coverage to Codecov
50-
uses: codecov/codecov-action@v5
50+
uses: codecov/codecov-action@v6
5151
with:
5252
env_vars: OS,PYTHON
5353
fail_ci_if_error: true

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ prompt is displayed.
7979
- **pre_prompt**: hook method that is called before the prompt is displayed, but after
8080
`prompt-toolkit` event loop has started
8181
- **read_secret**: read secrets like passwords without displaying them to the terminal
82+
- **ppretty**: a cmd2-compatible replacement for `rich.pretty.pprint()`
8283
- New settables:
8384
- **max_column_completion_results**: (int) the maximum number of completion results to
8485
display in a single column

cmd2/argparse_completer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ def consume_argument(arg_state: _ArgumentState, arg_token: str) -> None:
359359
action = remaining_positionals.popleft()
360360

361361
# Are we at a subcommand? If so, forward to the matching completer
362-
if action == self._subcommand_action:
362+
if self._subcommand_action is not None and action == self._subcommand_action:
363363
if token in self._subcommand_action.choices:
364364
# Merge self._parent_tokens and consumed_arg_values
365365
parent_tokens = {**self._parent_tokens, **consumed_arg_values}

cmd2/argparse_custom.py

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -222,42 +222,43 @@ def get_choices(self) -> Choices:
222222
more details on these arguments.
223223
224224
``argparse.ArgumentParser._get_nargs_pattern`` - adds support for nargs ranges.
225-
See _get_nargs_pattern_wrapper for more details.
225+
See ``_get_nargs_pattern_wrapper`` for more details.
226226
227227
``argparse.ArgumentParser._match_argument`` - adds support for nargs ranges.
228-
See _match_argument_wrapper for more details.
229-
230-
``argparse._SubParsersAction.remove_parser`` - new function which removes a
231-
sub-parser from a sub-parsers group. See _SubParsersAction_remove_parser for
232-
more details.
228+
See ``_match_argument_wrapper`` for more details.
233229
234230
**Added accessor methods**
235231
236232
cmd2 has patched ``argparse.Action`` to include the following accessor methods
237233
for cases in which you need to manually access the cmd2-specific attributes.
238234
239-
- ``argparse.Action.get_choices_callable()`` - See `action_get_choices_callable` for more details.
240-
- ``argparse.Action.set_choices_provider()`` - See `_action_set_choices_provider` for more details.
241-
- ``argparse.Action.set_completer()`` - See `_action_set_completer` for more details.
242-
- ``argparse.Action.get_table_columns()`` - See `_action_get_table_columns` for more details.
243-
- ``argparse.Action.set_table_columns()`` - See `_action_set_table_columns` for more details.
244-
- ``argparse.Action.get_nargs_range()`` - See `_action_get_nargs_range` for more details.
245-
- ``argparse.Action.set_nargs_range()`` - See `_action_set_nargs_range` for more details.
246-
- ``argparse.Action.get_suppress_tab_hint()`` - See `_action_get_suppress_tab_hint` for more details.
247-
- ``argparse.Action.set_suppress_tab_hint()`` - See `_action_set_suppress_tab_hint` for more details.
235+
- ``argparse.Action.get_choices_callable()`` - See ``action_get_choices_callable`` for more details.
236+
- ``argparse.Action.set_choices_provider()`` - See ``_action_set_choices_provider`` for more details.
237+
- ``argparse.Action.set_completer()`` - See ``_action_set_completer`` for more details.
238+
- ``argparse.Action.get_table_columns()`` - See ``_action_get_table_columns`` for more details.
239+
- ``argparse.Action.set_table_columns()`` - See ``_action_set_table_columns`` for more details.
240+
- ``argparse.Action.get_nargs_range()`` - See ``_action_get_nargs_range`` for more details.
241+
- ``argparse.Action.set_nargs_range()`` - See ``_action_set_nargs_range`` for more details.
242+
- ``argparse.Action.get_suppress_tab_hint()`` - See ``_action_get_suppress_tab_hint`` for more details.
243+
- ``argparse.Action.set_suppress_tab_hint()`` - See ``_action_set_suppress_tab_hint`` for more details.
248244
249245
cmd2 has patched ``argparse.ArgumentParser`` to include the following accessor methods
250246
251-
- ``argparse.ArgumentParser.get_ap_completer_type()`` - See `_ArgumentParser_get_ap_completer_type` for more details.
252-
- ``argparse.Action.set_ap_completer_type()`` - See `_ArgumentParser_set_ap_completer_type` for more details.
247+
- ``argparse.ArgumentParser.get_ap_completer_type()`` - See ``_ArgumentParser_get_ap_completer_type`` for more details.
248+
- ``argparse.Action.set_ap_completer_type()`` - See ``_ArgumentParser_set_ap_completer_type`` for more details.
249+
250+
**Subcommand Manipulation**
253251
254-
**Subcommand removal**
252+
cmd2 has patched ``argparse._SubParsersAction`` with new functions to better facilitate the
253+
addition and removal of subcommand parsers.
255254
256-
cmd2 has patched ``argparse._SubParsersAction`` to include a ``remove_parser()``
257-
method which can be used to remove a subcommand.
255+
``argparse._SubParsersAction.attach_parser`` - new function to attach
256+
an existing ArgumentParser to a subparsers action. See ``_SubParsersAction_attach_parser``
257+
for more details.
258258
259-
``argparse._SubParsersAction.remove_parser`` - new function which removes a
260-
sub-parser from a sub-parsers group. See _SubParsersAction_remove_parser` for more details.
259+
``argparse._SubParsersAction.detach_parser`` - new function to detach a
260+
parser from a subparsers action. See ``_SubParsersAction_detach_parser`` for
261+
more details.
261262
"""
262263

263264
import argparse
@@ -944,39 +945,85 @@ def _ArgumentParser_check_value(_self: argparse.ArgumentParser, action: argparse
944945

945946

946947
############################################################################################################
947-
# Patch argparse._SubParsersAction to add remove_parser function
948+
# Patch argparse._SubParsersAction to add attach_parser function
948949
############################################################################################################
949950

950951

951-
def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) -> None: # type: ignore[type-arg] # noqa: N802
952-
"""Remove a sub-parser from a sub-parsers group. Used to remove subcommands from a parser.
952+
def _SubParsersAction_attach_parser( # noqa: N802
953+
self: argparse._SubParsersAction, # type: ignore[type-arg]
954+
name: str,
955+
subcmd_parser: argparse.ArgumentParser,
956+
**add_parser_kwargs: Any,
957+
) -> None:
958+
"""Attach an existing ArgumentParser to a subparsers action.
959+
960+
This is useful when a parser is pre-configured (e.g. by cmd2's subcommand decorator)
961+
and needs to be attached to a parent parser.
953962
954-
This function is added by cmd2 as a method called ``remove_parser()`` to ``argparse._SubParsersAction`` class.
963+
This function is added by cmd2 as a method called ``attach_parser()``
964+
to ``argparse._SubParsersAction`` class.
955965
956-
To call: ``action.remove_parser(name)``
966+
To call: ``action.attach_parser(name, subcmd_parser, **add_parser_kwargs)``
957967
958968
:param self: instance of the _SubParsersAction being edited
959-
:param name: name of the subcommand for the sub-parser to remove
969+
:param name: name of the subcommand to add
970+
:param subcmd_parser: the parser for this new subcommand
971+
:param add_parser_kwargs: registration-specific kwargs for add_parser()
972+
(e.g. help, aliases, deprecated [Python 3.13+])
960973
"""
961-
# Remove this subcommand from its base command's help text
962-
for choice_action in self._choices_actions:
963-
if choice_action.dest == name:
964-
self._choices_actions.remove(choice_action)
965-
break
974+
# Use add_parser to register the subcommand name and any aliases
975+
self.add_parser(name, **add_parser_kwargs)
976+
977+
# Replace the parser created by add_parser() with our pre-configured one
978+
self._name_parser_map[name] = subcmd_parser
979+
980+
# Remap any aliases to our pre-configured parser
981+
for alias in add_parser_kwargs.get("aliases", ()):
982+
self._name_parser_map[alias] = subcmd_parser
983+
984+
985+
setattr(argparse._SubParsersAction, 'attach_parser', _SubParsersAction_attach_parser)
966986

967-
# Remove this subcommand and all its aliases from the base command
987+
############################################################################################################
988+
# Patch argparse._SubParsersAction to add detach_parser function
989+
############################################################################################################
990+
991+
992+
def _SubParsersAction_detach_parser( # noqa: N802
993+
self: argparse._SubParsersAction, # type: ignore[type-arg]
994+
name: str,
995+
) -> argparse.ArgumentParser | None:
996+
"""Detach a parser from a subparsers action and return it.
997+
998+
This function is added by cmd2 as a method called ``detach_parser()`` to ``argparse._SubParsersAction`` class.
999+
1000+
To call: ``action.detach_parser(name)``
1001+
1002+
:param self: instance of the _SubParsersAction being edited
1003+
:param name: name of the subcommand for the parser to detach
1004+
:return: the parser which was detached or None if the subcommand doesn't exist
1005+
"""
9681006
subparser = self._name_parser_map.get(name)
1007+
9691008
if subparser is not None:
1009+
# Remove this subcommand and all its aliases from the base command
9701010
to_remove = []
9711011
for cur_name, cur_parser in self._name_parser_map.items():
9721012
if cur_parser is subparser:
9731013
to_remove.append(cur_name)
9741014
for cur_name in to_remove:
9751015
del self._name_parser_map[cur_name]
9761016

1017+
# Remove this subcommand from its base command's help text
1018+
for choice_action in self._choices_actions:
1019+
if choice_action.dest == name:
1020+
self._choices_actions.remove(choice_action)
1021+
break
1022+
1023+
return subparser
9771024

978-
setattr(argparse._SubParsersAction, 'remove_parser', _SubParsersAction_remove_parser)
9791025

1026+
setattr(argparse._SubParsersAction, 'detach_parser', _SubParsersAction_detach_parser)
9801027

9811028
############################################################################################################
9821029
# Unless otherwise noted, everything below this point are copied from Python's

cmd2/cmd2.py

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
RenderableType,
8989
)
9090
from rich.highlighter import ReprHighlighter
91+
from rich.pretty import Pretty
9192
from rich.rule import Rule
9293
from rich.style import (
9394
Style,
@@ -1128,19 +1129,15 @@ def find_subcommand(
11281129
# Find the argparse action that handles subcommands
11291130
for action in target_parser._actions:
11301131
if isinstance(action, argparse._SubParsersAction):
1131-
# Get the kwargs for add_parser()
1132+
# Get add_parser() kwargs (aliases, help, etc.) defined by the decorator
11321133
add_parser_kwargs = getattr(method, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, {})
11331134

1134-
# Use add_parser to register the subcommand name and any aliases
1135-
action.add_parser(subcommand_name, **add_parser_kwargs)
1136-
1137-
# Replace the parser created by add_parser() with our pre-configured one
1138-
action._name_parser_map[subcommand_name] = subcmd_parser
1139-
1140-
# Also remap any aliases to our pre-configured parser
1141-
for alias in add_parser_kwargs.get("aliases", []):
1142-
action._name_parser_map[alias] = subcmd_parser
1143-
1135+
# Attach existing parser as a subcommand
1136+
action.attach_parser( # type: ignore[attr-defined]
1137+
subcommand_name,
1138+
subcmd_parser,
1139+
**add_parser_kwargs,
1140+
)
11441141
break
11451142

11461143
def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
@@ -1187,7 +1184,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
11871184

11881185
for action in command_parser._actions:
11891186
if isinstance(action, argparse._SubParsersAction):
1190-
action.remove_parser(subcommand_name) # type: ignore[attr-defined]
1187+
action.detach_parser(subcommand_name) # type: ignore[attr-defined]
11911188
break
11921189

11931190
@property
@@ -1822,6 +1819,56 @@ def ppaged(
18221819
rich_print_kwargs=rich_print_kwargs,
18231820
)
18241821

1822+
def ppretty(
1823+
self,
1824+
obj: Any,
1825+
*,
1826+
file: IO[str] | None = None,
1827+
indent_size: int = 4,
1828+
indent_guides: bool = True,
1829+
max_length: int | None = None,
1830+
max_string: int | None = None,
1831+
max_depth: int | None = None,
1832+
expand_all: bool = False,
1833+
end: str = "\n",
1834+
) -> None:
1835+
"""Pretty print an object.
1836+
1837+
This is a cmd2-compatible replacement for rich.pretty.pprint().
1838+
1839+
:param obj: object to pretty print
1840+
:param file: file stream being written to or None for self.stdout.
1841+
Defaults to None.
1842+
:param indent_size: number of spaces in indent. Defaults to 4.
1843+
:param indent_guides: enable indentation guides. Defaults to True.
1844+
:param max_length: maximum length of containers before abbreviating, or None for no abbreviation.
1845+
Defaults to None.
1846+
:param max_string: maximum length of strings before truncating, or None to disable. Defaults to None.
1847+
:param max_depth: maximum depth for nested data structures, or None for unlimited depth. Defaults to None.
1848+
:param expand_all: Expand all containers. Defaults to False.
1849+
:param end: string to write at end of printed text. Defaults to a newline.
1850+
"""
1851+
# The overflow and soft_wrap values match those in rich.pretty.pprint().
1852+
# This ensures long strings are neither truncated with ellipses nor broken
1853+
# up by injected newlines.
1854+
pretty_obj = Pretty(
1855+
obj,
1856+
indent_size=indent_size,
1857+
indent_guides=indent_guides,
1858+
max_length=max_length,
1859+
max_string=max_string,
1860+
max_depth=max_depth,
1861+
expand_all=expand_all,
1862+
overflow="ignore",
1863+
)
1864+
1865+
self.print_to(
1866+
file or self.stdout,
1867+
pretty_obj,
1868+
soft_wrap=True,
1869+
end=end,
1870+
)
1871+
18251872
def get_bottom_toolbar(self) -> list[str | tuple[str, str]] | None:
18261873
"""Get the bottom toolbar content.
18271874

cmd2/decorators.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ def as_subcommand_to(
527527
*,
528528
help: str | None = None, # noqa: A002
529529
aliases: Sequence[str] | None = None,
530+
**add_parser_kwargs: Any,
530531
) -> Callable[[ArgparseCommandFunc[CmdOrSet]], ArgparseCommandFunc[CmdOrSet]]:
531532
"""Tag this method as a subcommand to an existing argparse decorated command.
532533
@@ -537,6 +538,8 @@ def as_subcommand_to(
537538
This is passed as the help argument to subparsers.add_parser().
538539
:param aliases: Alternative names for this subcommand. This is passed as the alias argument to
539540
subparsers.add_parser().
541+
:param add_parser_kwargs: other registration-specific kwargs for add_parser()
542+
(e.g. deprecated [Python 3.13+])
540543
:return: Wrapper function that can receive an argparse.Namespace
541544
"""
542545

@@ -547,13 +550,13 @@ def arg_decorator(func: ArgparseCommandFunc[CmdOrSet]) -> ArgparseCommandFunc[Cm
547550
setattr(func, constants.SUBCMD_ATTR_NAME, subcommand)
548551

549552
# Keyword arguments for subparsers.add_parser()
550-
add_parser_kwargs: dict[str, Any] = {}
553+
final_kwargs: dict[str, Any] = dict(add_parser_kwargs)
551554
if help is not None:
552-
add_parser_kwargs['help'] = help
555+
final_kwargs['help'] = help
553556
if aliases:
554-
add_parser_kwargs['aliases'] = aliases[:]
557+
final_kwargs['aliases'] = tuple(aliases)
555558

556-
setattr(func, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, add_parser_kwargs)
559+
setattr(func, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, final_kwargs)
557560

558561
return func
559562

cmd2/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def __init__(
7474
settable_object: object,
7575
*,
7676
settable_attrib_name: str | None = None,
77-
onchange_cb: Callable[[str, _T, _T], Any] | None = None,
77+
onchange_cb: Callable[[str, Any, Any], Any] | None = None,
7878
choices: Iterable[Any] | None = None,
7979
choices_provider: ChoicesProviderUnbound[CmdOrSet] | None = None,
8080
completer: CompleterUnbound[CmdOrSet] | None = None,

examples/pretty_print.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,26 @@
11
#!/usr/bin/env python3
2-
"""A simple example demonstrating how to pretty print JSON data in a cmd2 app using rich."""
3-
4-
from rich.json import JSON
2+
"""A simple example demonstrating how to pretty print data."""
53

64
import cmd2
75

86
EXAMPLE_DATA = {
97
"name": "John Doe",
108
"age": 30,
119
"address": {"street": "123 Main St", "city": "Anytown", "state": "CA"},
12-
"hobbies": ["reading", "hiking", "coding"],
10+
"hobbies": ["reading", "hiking", "coding", "cooking", "running", "painting", "music", "photography", "cycling"],
11+
"member": True,
12+
"vip": False,
13+
"phone": None,
1314
}
1415

1516

1617
class Cmd2App(cmd2.Cmd):
1718
def __init__(self) -> None:
1819
super().__init__()
19-
self.data = EXAMPLE_DATA
20-
21-
def do_normal(self, _) -> None:
22-
"""Display the data using the normal poutput method."""
23-
self.poutput(self.data)
24-
25-
def do_pretty(self, _) -> None:
26-
"""Display the JSON data in a pretty way using rich."""
2720

28-
json_renderable = JSON.from_data(
29-
self.data,
30-
indent=2,
31-
highlight=True,
32-
skip_keys=False,
33-
ensure_ascii=False,
34-
check_circular=True,
35-
allow_nan=True,
36-
default=None,
37-
sort_keys=False,
38-
)
39-
self.poutput(json_renderable)
21+
def do_pretty(self, _: cmd2.Statement) -> None:
22+
"""Print an object using ppretty()."""
23+
self.ppretty(EXAMPLE_DATA)
4024

4125

4226
if __name__ == '__main__':

0 commit comments

Comments
 (0)