4949 Callable ,
5050 Iterable ,
5151 Mapping ,
52- MutableSequence ,
5352 Sequence ,
5453)
5554from dataclasses import (
@@ -1085,40 +1084,8 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
10851084 if not subcommand_valid :
10861085 raise CommandSetRegistrationError (f'Subcommand { subcommand_name } is not valid: { errmsg } ' )
10871086
1088- command_tokens = full_command_name .split ()
1089- command_name = command_tokens [0 ]
1090- subcommand_names = command_tokens [1 :]
1091-
1092- # Search for the base command function and verify it has an argparser defined
1093- if command_name in self .disabled_commands :
1094- command_func = self .disabled_commands [command_name ].command_function
1095- else :
1096- command_func = self .cmd_func (command_name )
1097-
1098- if command_func is None :
1099- raise CommandSetRegistrationError (f"Could not find command '{ command_name } ' needed by subcommand: { method } " )
1100- command_parser = self ._command_parsers .get (command_func )
1101- if command_parser is None :
1102- raise CommandSetRegistrationError (
1103- f"Could not find argparser for command '{ command_name } ' needed by subcommand: { method } "
1104- )
1105-
1106- def find_subcommand (action : Cmd2ArgumentParser , subcmd_names : MutableSequence [str ]) -> Cmd2ArgumentParser :
1107- if not subcmd_names :
1108- return action
1109- cur_subcmd = subcmd_names .pop (0 )
1110- for sub_action in action ._actions :
1111- if isinstance (sub_action , argparse ._SubParsersAction ):
1112- for choice_name , choice in sub_action .choices .items ():
1113- if choice_name == cur_subcmd :
1114- return find_subcommand (choice , subcmd_names )
1115- break
1116- raise CommandSetRegistrationError (f"Could not find subcommand '{ action } '" )
1117-
1118- target_parser = find_subcommand (command_parser , subcommand_names )
1119-
11201087 # Create the subcommand parser and configure it
1121- subcmd_parser = self ._build_parser (cmdset , subcmd_parser_builder , f'{ command_name } { subcommand_name } ' )
1088+ subcmd_parser = self ._build_parser (cmdset , subcmd_parser_builder , f'{ full_command_name } { subcommand_name } ' )
11221089 if subcmd_parser .description is None and method .__doc__ :
11231090 subcmd_parser .description = strip_doc_annotations (method .__doc__ )
11241091
@@ -1129,19 +1096,14 @@ def find_subcommand(action: Cmd2ArgumentParser, subcmd_names: MutableSequence[st
11291096 # Set what instance the handler is bound to
11301097 setattr (subcmd_parser , constants .PARSER_ATTR_COMMANDSET_ID , id (cmdset ))
11311098
1132- # Find the argparse action that handles subcommands
1133- for action in target_parser ._actions :
1134- if isinstance (action , argparse ._SubParsersAction ):
1135- # Get add_parser() kwargs (aliases, help, etc.) defined by the decorator
1136- add_parser_kwargs = getattr (method , constants .SUBCMD_ATTR_ADD_PARSER_KWARGS , {})
1137-
1138- # Attach existing parser as a subcommand
1139- action .attach_parser ( # type: ignore[attr-defined]
1140- subcommand_name ,
1141- subcmd_parser ,
1142- ** add_parser_kwargs ,
1143- )
1144- break
1099+ # Get add_parser() kwargs (aliases, help, etc.) defined by the decorator
1100+ add_parser_kwargs = getattr (method , constants .SUBCMD_ATTR_ADD_PARSER_KWARGS , {})
1101+
1102+ # Attach existing parser as a subcommand
1103+ try :
1104+ self .attach_subcommand (full_command_name , subcommand_name , subcmd_parser , ** add_parser_kwargs )
1105+ except ValueError as ex :
1106+ raise CommandSetRegistrationError (str (ex )) from ex
11451107
11461108 def _unregister_subcommands (self , cmdset : Union [CommandSet , 'Cmd' ]) -> None :
11471109 """Unregister subcommands from their base command.
@@ -1165,30 +1127,77 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
11651127 # iterate through all matching methods
11661128 for _method_name , method in methods :
11671129 subcommand_name = getattr (method , constants .SUBCMD_ATTR_NAME )
1168- command_name = getattr (method , constants .SUBCMD_ATTR_COMMAND )
1130+ full_command_name = getattr (method , constants .SUBCMD_ATTR_COMMAND )
11691131
1170- # Search for the base command function and verify it has an argparser defined
1171- if command_name in self .disabled_commands :
1172- command_func = self .disabled_commands [command_name ].command_function
1173- else :
1174- command_func = self .cmd_func (command_name )
1175-
1176- if command_func is None : # pragma: no cover
1177- # This really shouldn't be possible since _register_subcommands would prevent this from happening
1178- # but keeping in case it does for some strange reason
1179- raise CommandSetRegistrationError (f"Could not find command '{ command_name } ' needed by subcommand: { method } " )
1180- command_parser = self ._command_parsers .get (command_func )
1181- if command_parser is None : # pragma: no cover
1182- # This really shouldn't be possible since _register_subcommands would prevent this from happening
1183- # but keeping in case it does for some strange reason
1184- raise CommandSetRegistrationError (
1185- f"Could not find argparser for command '{ command_name } ' needed by subcommand: { method } "
1186- )
1132+ with contextlib .suppress (ValueError ):
1133+ self .detach_subcommand (full_command_name , subcommand_name )
11871134
1188- for action in command_parser ._actions :
1189- if isinstance (action , argparse ._SubParsersAction ):
1190- action .detach_parser (subcommand_name ) # type: ignore[attr-defined]
1191- break
1135+ def _get_root_parser_and_subcmd_path (self , command : str ) -> tuple [Cmd2ArgumentParser , list [str ]]:
1136+ """Tokenize a command string and resolve the associated root parser and relative subcommand path.
1137+
1138+ This helper handles the initial resolution of a command string (e.g., 'foo bar baz') by
1139+ identifying 'foo' as the root command (even if disabled), retrieving its associated
1140+ parser, and returning any remaining tokens (['bar', 'baz']) as a path relative
1141+ to that parser for further traversal.
1142+
1143+ :param command: full space-delimited command path leading to a parser (e.g. 'foo' or 'foo bar')
1144+ :return: a tuple containing the Cmd2ArgumentParser for the root command and a list of
1145+ strings representing the relative path to the desired hosting parser.
1146+ :raises ValueError: if the command is empty, the root command is not found, or
1147+ the root command does not use an argparse parser.
1148+ """
1149+ tokens = command .split ()
1150+ if not tokens :
1151+ raise ValueError ("Command path cannot be empty" )
1152+
1153+ root_command = tokens [0 ]
1154+ subcommand_path = tokens [1 :]
1155+
1156+ # Search for the base command function and verify it has an argparser defined
1157+ if root_command in self .disabled_commands :
1158+ command_func = self .disabled_commands [root_command ].command_function
1159+ else :
1160+ command_func = self .cmd_func (root_command )
1161+
1162+ if command_func is None :
1163+ raise ValueError (f"Root command '{ root_command } ' not found" )
1164+
1165+ root_parser = self ._command_parsers .get (command_func )
1166+ if root_parser is None :
1167+ raise ValueError (f"Command '{ root_command } ' does not use argparse" )
1168+
1169+ return root_parser , subcommand_path
1170+
1171+ def attach_subcommand (
1172+ self ,
1173+ command : str ,
1174+ subcommand : str ,
1175+ parser : Cmd2ArgumentParser ,
1176+ ** add_parser_kwargs : Any ,
1177+ ) -> None :
1178+ """Attach a parser as a subcommand to a command at the specified path.
1179+
1180+ :param command: full command path (space-delimited) leading to the parser that will
1181+ host the new subcommand (e.g. 'foo bar')
1182+ :param subcommand: name of the new subcommand
1183+ :param parser: the parser to attach
1184+ :param add_parser_kwargs: additional arguments for the subparser registration (e.g. help, aliases)
1185+ :raises ValueError: if the command path is invalid or doesn't support subcommands
1186+ """
1187+ root_parser , subcommand_path = self ._get_root_parser_and_subcmd_path (command )
1188+ root_parser .attach_subcommand (subcommand_path , subcommand , parser , ** add_parser_kwargs )
1189+
1190+ def detach_subcommand (self , command : str , subcommand : str ) -> Cmd2ArgumentParser :
1191+ """Detach a subcommand from a command at the specified path.
1192+
1193+ :param command: full command path (space-delimited) leading to the parser hosting the
1194+ subcommand to be detached (e.g. 'foo bar')
1195+ :param subcommand: name of the subcommand to detach
1196+ :return: the detached parser
1197+ :raises ValueError: if the command path is invalid or the subcommand doesn't exist
1198+ """
1199+ root_parser , subcommand_path = self ._get_root_parser_and_subcmd_path (command )
1200+ return root_parser .detach_subcommand (subcommand_path , subcommand )
11921201
11931202 @property
11941203 def always_prefix_settables (self ) -> bool :
0 commit comments