@@ -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 } '" )
0 commit comments