Skip to content

Commit 043d225

Browse files
committed
Moved set_parser_prog() to Cmd2ArgumentParser.update_prog().
1 parent e727360 commit 043d225

3 files changed

Lines changed: 73 additions & 68 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ prompt is displayed.
6868
class of it.
6969
- Removed `set_ap_completer_type()` and `get_ap_completer_type()` since `ap_completer_type` is
7070
now a public member of `Cmd2ArgumentParser`.
71+
- Moved `set_parser_prog()` to `Cmd2ArgumentParser.update_prog()`.
7172
- Enhancements
7273
- New `cmd2.Cmd` parameters
7374
- **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These

cmd2/argparse_custom.py

Lines changed: 61 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -303,56 +303,6 @@ def generate_range_error(range_min: int, range_max: float) -> str:
303303
return err_msg
304304

305305

306-
def set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
307-
"""Recursively set prog attribute of a parser and all of its subparsers.
308-
309-
Does so that the root command is a command name and not sys.argv[0].
310-
311-
:param parser: the parser being edited
312-
:param prog: new value for the parser's prog attribute
313-
"""
314-
# Set the prog value for this parser
315-
parser.prog = prog
316-
req_args: list[str] = []
317-
318-
# Set the prog value for the parser's subcommands
319-
for action in parser._actions:
320-
if isinstance(action, argparse._SubParsersAction):
321-
# Set the _SubParsersAction's _prog_prefix value. That way if its add_parser() method is called later,
322-
# the correct prog value will be set on the parser being added.
323-
action._prog_prefix = parser.prog
324-
325-
# The keys of action.choices are subcommand names as well as subcommand aliases. The aliases point to the
326-
# same parser as the actual subcommand. We want to avoid placing an alias into a parser's prog value.
327-
# Unfortunately there is nothing about an action.choices entry which tells us it's an alias. In most cases
328-
# we can filter out the aliases by checking the contents of action._choices_actions. This list only contains
329-
# help information and names for the subcommands and not aliases. However, subcommands without help text
330-
# won't show up in that list. Since dictionaries are ordered in Python 3.6 and above and argparse inserts the
331-
# subcommand name into choices dictionary before aliases, we should be OK assuming the first time we see a
332-
# parser, the dictionary key is a subcommand and not alias.
333-
processed_parsers = []
334-
335-
# Set the prog value for each subcommand's parser
336-
for subcmd_name, subcmd_parser in action.choices.items():
337-
# Check if we've already edited this parser
338-
if subcmd_parser in processed_parsers:
339-
continue
340-
341-
subcmd_prog = parser.prog
342-
if req_args:
343-
subcmd_prog += " " + " ".join(req_args)
344-
subcmd_prog += " " + subcmd_name
345-
set_parser_prog(subcmd_parser, subcmd_prog)
346-
processed_parsers.append(subcmd_parser)
347-
348-
# We can break since argparse only allows 1 group of subcommands per level
349-
break
350-
351-
# Need to save required args so they can be prepended to the subcommand usage
352-
if action.required:
353-
req_args.append(action.dest)
354-
355-
356306
############################################################################################################
357307
# Allow developers to add custom action attributes
358308
############################################################################################################
@@ -928,16 +878,70 @@ def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type:
928878

929879
return super().add_subparsers(**kwargs)
930880

931-
def _find_subparsers_action(self) -> argparse._SubParsersAction: # type: ignore[type-arg]
932-
"""Find the _SubParsersAction for this parser.
881+
def _get_subparsers_action(self) -> argparse._SubParsersAction: # type: ignore[type-arg]
882+
"""Get the _SubParsersAction for this parser if it exists.
933883
934884
:return: the _SubParsersAction for this parser
935885
:raises ValueError: if this parser does not support subcommands
936886
"""
887+
if self._subparsers is not None:
888+
for action in self._subparsers._group_actions:
889+
if isinstance(action, argparse._SubParsersAction):
890+
return action
891+
raise ValueError(f"Command '{self.prog}' does not support subcommands")
892+
893+
def update_prog(self, prog: str) -> None:
894+
"""Recursively update the prog attribute of this parser and all of its subparsers.
895+
896+
:param prog: new value for the parser's prog attribute
897+
"""
898+
# Set the prog value for this parser
899+
self.prog = prog
900+
901+
if self._subparsers is None:
902+
# This parser has no subcommands
903+
return
904+
905+
# Required args which come before the subcommand
906+
req_args: list[str] = []
907+
908+
# Set the prog value for the parser's subcommands
937909
for action in self._actions:
938910
if isinstance(action, argparse._SubParsersAction):
939-
return action
940-
raise ValueError(f"Command '{self.prog}' does not support subcommands")
911+
# Set the _SubParsersAction's _prog_prefix value. That way if its add_parser()
912+
# method is called later, the correct prog value will be set on the parser being added.
913+
action._prog_prefix = self.prog
914+
915+
# The keys of action.choices are subcommand names as well as subcommand aliases.
916+
# The aliases point to the same parser as the actual subcommand. We want to avoid
917+
# placing an alias into a parser's prog value. Unfortunately there is nothing about
918+
# an action.choices entry which tells us it's an alias. In most cases we can filter out
919+
# the aliases by checking the contents of action._choices_actions. This list only contains
920+
# help information and names for the subcommands and not aliases. However, subcommands
921+
# without help text won't show up in that list. Since dictionaries are ordered and
922+
# argparse inserts the subcommand name into choices dictionary before aliases, we should
923+
# be OK assuming the first time we see a parser, the dictionary key is a subcommand and
924+
# not an alias.
925+
processed_parsers = []
926+
927+
# Set the prog value for each subcommand's parser
928+
for subcmd_name, subcmd_parser in action.choices.items():
929+
if subcmd_parser in processed_parsers:
930+
continue
931+
932+
subcmd_prog = self.prog
933+
if req_args:
934+
subcmd_prog += " " + " ".join(req_args)
935+
subcmd_prog += " " + subcmd_name
936+
subcmd_parser.update_prog(subcmd_prog)
937+
processed_parsers.append(subcmd_parser)
938+
939+
# We can break since argparse only allows 1 group of subcommands per level
940+
break
941+
942+
# Need to save required args so they can be prepended to the subcommand usage
943+
if action.required:
944+
req_args.append(action.dest)
941945

942946
def _find_parser(self, subcommand_path: Iterable[str]) -> 'Cmd2ArgumentParser':
943947
"""Find a parser in the hierarchy based on a sequence of subcommand names.
@@ -948,7 +952,7 @@ def _find_parser(self, subcommand_path: Iterable[str]) -> 'Cmd2ArgumentParser':
948952
"""
949953
parser = self
950954
for name in subcommand_path:
951-
subparsers_action = parser._find_subparsers_action()
955+
subparsers_action = parser._get_subparsers_action()
952956
if name not in subparsers_action.choices:
953957
raise ValueError(f"Subcommand '{name}' not found in '{parser.prog}'")
954958
parser = cast(Cmd2ArgumentParser, subparsers_action.choices[name])
@@ -971,7 +975,7 @@ def attach_subcommand(
971975
:raises ValueError: if the command path is invalid or doesn't support subcommands
972976
"""
973977
target_parser = self._find_parser(subcommand_path)
974-
subparsers_action = target_parser._find_subparsers_action()
978+
subparsers_action = target_parser._get_subparsers_action()
975979
subparsers_action.attach_parser(subcommand, parser, **add_parser_kwargs) # type: ignore[attr-defined]
976980

977981
def detach_subcommand(self, subcommand_path: Iterable[str], subcommand: str) -> 'Cmd2ArgumentParser':
@@ -984,7 +988,7 @@ def detach_subcommand(self, subcommand_path: Iterable[str], subcommand: str) ->
984988
:raises ValueError: if the command path is invalid or the subcommand doesn't exist
985989
"""
986990
target_parser = self._find_parser(subcommand_path)
987-
subparsers_action = target_parser._find_subparsers_action()
991+
subparsers_action = target_parser._get_subparsers_action()
988992
detached = subparsers_action.detach_parser(subcommand) # type: ignore[attr-defined]
989993
if detached is None:
990994
raise ValueError(f"Subcommand '{subcommand}' not found in '{target_parser.prog}'")

cmd2/cmd2.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -920,7 +920,7 @@ def _build_parser(
920920
builder_name = getattr(parser_builder, "__name__", str(parser_builder)) # type: ignore[unreachable]
921921
raise TypeError(f"The parser returned by '{builder_name}' must be a Cmd2ArgumentParser or a subclass of it")
922922

923-
argparse_custom.set_parser_prog(parser, prog)
923+
parser.update_prog(prog)
924924

925925
return parser
926926

@@ -1027,16 +1027,16 @@ def unregister_command_set(self, cmdset: CommandSet) -> None:
10271027
def _check_uninstallable(self, cmdset: CommandSet) -> None:
10281028
def check_parser_uninstallable(parser: Cmd2ArgumentParser) -> None:
10291029
cmdset_id = id(cmdset)
1030-
for action in parser._actions:
1031-
if isinstance(action, argparse._SubParsersAction):
1032-
for subparser in action.choices.values():
1033-
attached_cmdset_id = getattr(subparser, constants.PARSER_ATTR_COMMANDSET_ID, None)
1034-
if attached_cmdset_id is not None and attached_cmdset_id != cmdset_id:
1035-
raise CommandSetRegistrationError(
1036-
'Cannot uninstall CommandSet when another CommandSet depends on it'
1037-
)
1038-
check_parser_uninstallable(subparser)
1039-
break
1030+
try:
1031+
subparsers_action = parser._get_subparsers_action()
1032+
for subparser in subparsers_action.choices.values():
1033+
attached_cmdset_id = getattr(subparser, constants.PARSER_ATTR_COMMANDSET_ID, None)
1034+
if attached_cmdset_id is not None and attached_cmdset_id != cmdset_id:
1035+
raise CommandSetRegistrationError('Cannot uninstall CommandSet when another CommandSet depends on it')
1036+
check_parser_uninstallable(cast(Cmd2ArgumentParser, subparser))
1037+
except ValueError:
1038+
# No subcommands to check
1039+
pass
10401040

10411041
methods: list[tuple[str, Callable[..., Any]]] = inspect.getmembers(
10421042
cmdset,

0 commit comments

Comments
 (0)