@@ -271,8 +271,8 @@ def get_choices(self) -> Choices:
271271 from .argparse_completer import ArgparseCompleter
272272
273273
274- def generate_range_error (range_min : int , range_max : float ) -> str :
275- """Generate an error message when the the number of arguments provided is not within the expected range."""
274+ def build_range_error (range_min : int , range_max : float ) -> str :
275+ """Build an error message when the the number of arguments provided is not within the expected range."""
276276 err_msg = "expected "
277277
278278 if range_max == constants .INFINITY :
@@ -577,7 +577,7 @@ def _set_color(self, color: bool, **kwargs: Any) -> None:
577577 super ()._set_color (color )
578578
579579 def _build_nargs_range_str (self , nargs_range : tuple [int , int | float ]) -> str :
580- """Generate nargs range string for help text."""
580+ """Build nargs range string for help text."""
581581 if nargs_range [1 ] == constants .INFINITY :
582582 # {min+}
583583 range_str = f"{{{ nargs_range [0 ]} +}}"
@@ -761,16 +761,17 @@ def __init__(
761761 conflict_handler = conflict_handler ,
762762 add_help = add_help ,
763763 allow_abbrev = allow_abbrev ,
764- exit_on_error = exit_on_error , # added in Python 3.9
764+ exit_on_error = exit_on_error ,
765765 ** kwargs , # added in Python 3.14
766766 )
767767
768- # Recast to assist type checkers since these can be Rich renderables in a Cmd2HelpFormatter.
768+ self .ap_completer_type = ap_completer_type
769+
770+ # To assist type checkers, recast these to reflect our usage of rich-argparse.
771+ self .formatter_class : type [Cmd2HelpFormatter ]
769772 self .description : RenderableType | None # type: ignore[assignment]
770773 self .epilog : RenderableType | None # type: ignore[assignment]
771774
772- self .ap_completer_type = ap_completer_type
773-
774775 def add_subparsers ( # type: ignore[override]
775776 self ,
776777 ** kwargs : Any ,
@@ -802,6 +803,48 @@ def _get_subparsers_action(self) -> "argparse._SubParsersAction[Cmd2ArgumentPars
802803 return action
803804 raise ValueError (f"Command '{ self .prog } ' does not support subcommands" )
804805
806+ def _build_subparsers_prog_prefix (self , positionals : list [argparse .Action ]) -> str :
807+ """Build the 'prog' prefix for a subparsers action.
808+
809+ This prefix is stored in the _SubParsersAction's '_prog_prefix' attribute and
810+ is used to construct the 'prog' attribute for its child parsers. It
811+ typically consists of the current parser's 'prog' name followed by any
812+ positional arguments that appear before the _SubParsersAction.
813+
814+ This method uses a temporary Cmd2ArgumentParser to leverage argparse's
815+ functionality for generating these strings. Subclasses can override this if
816+ they need to change how subcommand 'prog' values are constructed (e.g., if
817+ add_subparsers() was overridden with custom naming logic or if a different
818+ formatting style is desired).
819+
820+ Note: This method explicitly instantiates Cmd2ArgumentParser rather than
821+ type(self) to avoid potential side effects or mandatory constructor
822+ arguments in user-defined subclasses.
823+
824+ :param positionals: positional arguments which appear before the _SubParsersAction
825+ :return: the built 'prog' prefix
826+ """
827+ # 1. usage=None: In Python < 3.14, this prevents the default usage
828+ # string from affecting subparser prog strings. This was fixed in 3.14:
829+ # https://github.com/python/cpython/commit/0cb4d6c6549d2299f7518f083bbe7d10314ecd66
830+ #
831+ # 2. add_help=False: No need for a help action since we already know which
832+ # actions are needed to build the prefix and have passed them in
833+ # via the 'positionals' argument.
834+ temp_parser = Cmd2ArgumentParser (
835+ prog = self .prog ,
836+ usage = None ,
837+ formatter_class = self .formatter_class ,
838+ add_help = False ,
839+ )
840+
841+ # Inject the current positional state so add_subparsers() has the right context
842+ temp_parser ._actions = positionals
843+ temp_parser ._mutually_exclusive_groups = self ._mutually_exclusive_groups
844+
845+ # Call add_subparsers() to build _prog_prefix
846+ return temp_parser .add_subparsers ()._prog_prefix
847+
805848 def update_prog (self , prog : str ) -> None :
806849 """Recursively update the prog attribute of this parser and all of its subparsers.
807850
@@ -810,47 +853,39 @@ def update_prog(self, prog: str) -> None:
810853 # Set the prog value for this parser
811854 self .prog = prog
812855
813- if self ._subparsers is None :
856+ try :
857+ subparsers_action = self ._get_subparsers_action ()
858+ except ValueError :
814859 # This parser has no subcommands
815860 return
816861
817- # argparse includes positional arguments that appear before the subcommand in its
818- # subparser prog strings. Track these while iterating through actions.
862+ # Get all positional arguments which appear before the subcommand.
819863 positionals : list [argparse .Action ] = []
820-
821- # Set the prog value for the parser's subcommands
822864 for action in self ._actions :
823- if isinstance (action , argparse ._SubParsersAction ):
824- # Use a formatter to generate _prog_prefix exactly as argparse does in
825- # add_subparsers(). This ensures that any subcommands added later via
826- # add_parser() will have the correct prog value.
827- formatter = self ._get_formatter ()
828- formatter .add_usage (self .usage , positionals , self ._mutually_exclusive_groups , '' )
829- action ._prog_prefix = formatter .format_help ().strip ()
830-
831- # Note: action.choices contains both subcommand names and aliases.
832- # To ensure subcommands (and not aliases) are used in 'prog':
833- # 1. We can't use action._choices_actions because it excludes subcommands without help text.
834- # 2. Since dictionaries are ordered and argparse inserts the primary name before aliases,
835- # we assume the first time we encounter a parser, the key is the true subcommand name.
836- updated_parsers : set [Cmd2ArgumentParser ] = set ()
837-
838- # Set the prog value for each subcommand's parser
839- for subcmd_name , subcmd_parser in action .choices .items ():
840- if subcmd_parser in updated_parsers :
841- continue
842-
843- subcmd_prog = f"{ action ._prog_prefix } { subcmd_name } "
844- subcmd_parser .update_prog (subcmd_prog ) # type: ignore[attr-defined]
845- updated_parsers .add (subcmd_parser )
846-
847- # We can break since argparse only allows 1 group of subcommands per level
865+ if action is subparsers_action :
848866 break
849867
850868 # Save positional argument
851869 if not action .option_strings :
852870 positionals .append (action )
853871
872+ # Update _prog_prefix. This ensures that any subcommands added later via
873+ # add_parser() will have the correct prog value.
874+ subparsers_action ._prog_prefix = self ._build_subparsers_prog_prefix (positionals )
875+
876+ # subparsers_action.choices includes aliases. Since primary names are inserted first,
877+ # we skip already updated parsers to ensure primary names are used in 'prog'.
878+ updated_parsers : set [Cmd2ArgumentParser ] = set ()
879+
880+ # Set the prog value for each subcommand's parser
881+ for subcmd_name , subcmd_parser in subparsers_action .choices .items ():
882+ if subcmd_parser in updated_parsers :
883+ continue
884+
885+ subcmd_prog = f"{ subparsers_action ._prog_prefix } { subcmd_name } "
886+ subcmd_parser .update_prog (subcmd_prog )
887+ updated_parsers .add (subcmd_parser )
888+
854889 def _find_parser (self , subcommand_path : Iterable [str ]) -> 'Cmd2ArgumentParser' :
855890 """Find a parser in the hierarchy based on a sequence of subcommand names.
856891
@@ -987,7 +1022,7 @@ def _match_argument(self, action: argparse.Action, arg_strings_pattern: str) ->
9871022 if match is None :
9881023 nargs_range = action .get_nargs_range () # type: ignore[attr-defined]
9891024 if nargs_range is not None :
990- raise ArgumentError (action , generate_range_error (nargs_range [0 ], nargs_range [1 ]))
1025+ raise ArgumentError (action , build_range_error (nargs_range [0 ], nargs_range [1 ]))
9911026
9921027 return super ()._match_argument (action , arg_strings_pattern )
9931028
0 commit comments