Skip to content

Commit 06aff3f

Browse files
authored
Merge branch 'main' into dependabot/github_actions/codecov/codecov-action-6
2 parents 81f55fb + 88285e3 commit 06aff3f

File tree

4 files changed

+139
-51
lines changed

4 files changed

+139
-51
lines changed

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: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,19 +1129,15 @@ def find_subcommand(
11291129
# Find the argparse action that handles subcommands
11301130
for action in target_parser._actions:
11311131
if isinstance(action, argparse._SubParsersAction):
1132-
# Get the kwargs for add_parser()
1132+
# Get add_parser() kwargs (aliases, help, etc.) defined by the decorator
11331133
add_parser_kwargs = getattr(method, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, {})
11341134

1135-
# Use add_parser to register the subcommand name and any aliases
1136-
action.add_parser(subcommand_name, **add_parser_kwargs)
1137-
1138-
# Replace the parser created by add_parser() with our pre-configured one
1139-
action._name_parser_map[subcommand_name] = subcmd_parser
1140-
1141-
# Also remap any aliases to our pre-configured parser
1142-
for alias in add_parser_kwargs.get("aliases", []):
1143-
action._name_parser_map[alias] = subcmd_parser
1144-
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+
)
11451141
break
11461142

11471143
def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
@@ -1188,7 +1184,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
11881184

11891185
for action in command_parser._actions:
11901186
if isinstance(action, argparse._SubParsersAction):
1191-
action.remove_parser(subcommand_name) # type: ignore[attr-defined]
1187+
action.detach_parser(subcommand_name) # type: ignore[attr-defined]
11921188
break
11931189

11941190
@property

cmd2/decorators.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ def as_subcommand_to(
353353
*,
354354
help: str | None = None, # noqa: A002
355355
aliases: Sequence[str] | None = None,
356+
**add_parser_kwargs: Any,
356357
) -> Callable[[ArgparseCommandFunc[CmdOrSet]], ArgparseCommandFunc[CmdOrSet]]:
357358
"""Tag this method as a subcommand to an existing argparse decorated command.
358359
@@ -363,6 +364,8 @@ def as_subcommand_to(
363364
This is passed as the help argument to subparsers.add_parser().
364365
:param aliases: Alternative names for this subcommand. This is passed as the alias argument to
365366
subparsers.add_parser().
367+
:param add_parser_kwargs: other registration-specific kwargs for add_parser()
368+
(e.g. deprecated [Python 3.13+])
366369
:return: Wrapper function that can receive an argparse.Namespace
367370
"""
368371

@@ -373,13 +376,13 @@ def arg_decorator(func: ArgparseCommandFunc[CmdOrSet]) -> ArgparseCommandFunc[Cm
373376
setattr(func, constants.SUBCMD_ATTR_NAME, subcommand)
374377

375378
# Keyword arguments for subparsers.add_parser()
376-
add_parser_kwargs: dict[str, Any] = {}
379+
final_kwargs: dict[str, Any] = dict(add_parser_kwargs)
377380
if help is not None:
378-
add_parser_kwargs['help'] = help
381+
final_kwargs['help'] = help
379382
if aliases:
380-
add_parser_kwargs['aliases'] = aliases[:]
383+
final_kwargs['aliases'] = tuple(aliases)
381384

382-
setattr(func, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, add_parser_kwargs)
385+
setattr(func, constants.SUBCMD_ATTR_ADD_PARSER_KWARGS, final_kwargs)
383386

384387
return func
385388

tests/test_argparse_custom.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,48 @@ def test_cmd2_attribute_wrapper() -> None:
308308
assert wrapper.get() == new_val
309309

310310

311+
def test_parser_attachment() -> None:
312+
# Attach a parser as a subcommand
313+
root_parser = Cmd2ArgumentParser(description="root command")
314+
root_subparsers = root_parser.add_subparsers()
315+
316+
child_parser = Cmd2ArgumentParser(description="child command")
317+
root_subparsers.attach_parser( # type: ignore[attr-defined]
318+
"child",
319+
child_parser,
320+
help="a child command",
321+
aliases=["child_alias"],
322+
)
323+
324+
# Verify the same parser instance was used
325+
assert root_subparsers._name_parser_map["child"] is child_parser
326+
assert root_subparsers._name_parser_map["child_alias"] is child_parser
327+
328+
# Verify an action with the help text exists
329+
child_action = None
330+
for action in root_subparsers._choices_actions:
331+
if action.dest == "child":
332+
child_action = action
333+
break
334+
assert child_action is not None
335+
assert child_action.help == "a child command"
336+
337+
# Detatch the subcommand
338+
detached_parser = root_subparsers.detach_parser("child") # type: ignore[attr-defined]
339+
340+
# Verify subcommand and its aliases were removed
341+
assert detached_parser is child_parser
342+
assert "child" not in root_subparsers._name_parser_map
343+
assert "child_alias" not in root_subparsers._name_parser_map
344+
345+
# Verify the help text action was removed
346+
choices_actions = [action.dest for action in root_subparsers._choices_actions]
347+
assert "child" not in choices_actions
348+
349+
# Verify it returns None when subcommand does not exist
350+
assert root_subparsers.detach_parser("fake") is None # type: ignore[attr-defined]
351+
352+
311353
def test_completion_items_as_choices(capsys) -> None:
312354
"""Test cmd2's patch to Argparse._check_value() which supports CompletionItems as choices.
313355
Choices are compared to CompletionItems.orig_value instead of the CompletionItem instance.

0 commit comments

Comments
 (0)