@@ -204,6 +204,14 @@ def __init__(self, msg: str = '') -> None:
204204 suggest_similar ,
205205)
206206
207+ try :
208+ from .typer_custom import TyperParser
209+ except ImportError :
210+
211+ class TyperParser : # type: ignore[no-redef]
212+ """Sentinel: isinstance checks always return False when typer is not installed."""
213+
214+
207215if TYPE_CHECKING : # pragma: no cover
208216 StaticArgParseBuilder = staticmethod [[], argparse .ArgumentParser ]
209217 ClassArgParseBuilder = classmethod ['Cmd' | CommandSet , [], argparse .ArgumentParser ]
@@ -229,14 +237,16 @@ class _CommandParsers:
229237 """Create and store all command method argument parsers for a given Cmd instance.
230238
231239 Parser creation and retrieval are accomplished through the get() method.
240+ Supports both argparse-based and Typer/Click-based commands.
232241 """
233242
234243 def __init__ (self , cmd : 'Cmd' ) -> None :
235244 self ._cmd = cmd
236245
237246 # Keyed by the fully qualified method names. This is more reliable than
238247 # the methods themselves, since wrapping a method will change its address.
239- self ._parsers : dict [str , argparse .ArgumentParser ] = {}
248+ # Values are argparse.ArgumentParser for argparse commands or TyperParser (click.Command) for Typer commands.
249+ self ._parsers : dict [str , argparse .ArgumentParser | TyperParser ] = {}
240250
241251 @staticmethod
242252 def _fully_qualified_name (command_method : CommandFunc ) -> str :
@@ -250,15 +260,16 @@ def __contains__(self, command_method: CommandFunc) -> bool:
250260 """Return whether a given method's parser is in self.
251261
252262 If the parser does not yet exist, it will be created if applicable.
253- This is basically for checking if a method is argarse-based .
263+ This is basically for checking if a method uses argparse or Typer .
254264 """
255265 parser = self .get (command_method )
256266 return bool (parser )
257267
258- def get (self , command_method : CommandFunc ) -> argparse .ArgumentParser | None :
259- """Return a given method's parser or None if the method is not argparse -based.
268+ def get (self , command_method : CommandFunc ) -> argparse .ArgumentParser | TyperParser | None :
269+ """Return a given method's parser or None if the method is not parser -based.
260270
261271 If the parser does not yet exist, it will be created.
272+ Handles both argparse and Typer/Click commands.
262273 """
263274 full_method_name = self ._fully_qualified_name (command_method )
264275 if not full_method_name :
@@ -269,6 +280,14 @@ def get(self, command_method: CommandFunc) -> argparse.ArgumentParser | None:
269280 return None
270281 command = command_method .__name__ [len (COMMAND_FUNC_PREFIX ) :]
271282
283+ # Check for Typer command
284+ if getattr (command_method , constants .CMD_ATTR_TYPER_FUNC , None ) is not None :
285+ from .typer_custom import build_typer_command
286+
287+ parent = self ._cmd .find_commandset_for_command (command ) or self ._cmd
288+ self ._parsers [full_method_name ] = build_typer_command (parent , command_method )
289+ return self ._parsers [full_method_name ]
290+
272291 parser_builder = getattr (command_method , constants .CMD_ATTR_ARGPARSER , None )
273292 if parser_builder is None :
274293 return None
@@ -1030,7 +1049,7 @@ def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
10301049 # is the actual command since command synonyms don't own it.
10311050 if cmd_func_name == command_method .__name__ :
10321051 command_parser = self ._command_parsers .get (command_method )
1033- if command_parser is not None :
1052+ if isinstance ( command_parser , argparse . ArgumentParser ) :
10341053 check_parser_uninstallable (command_parser )
10351054
10361055 def _register_subcommands (self , cmdset : Union [CommandSet , 'Cmd' ]) -> None :
@@ -1075,7 +1094,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
10751094 if command_func is None :
10761095 raise CommandSetRegistrationError (f"Could not find command '{ command_name } ' needed by subcommand: { method } " )
10771096 command_parser = self ._command_parsers .get (command_func )
1078- if command_parser is None :
1097+ if not isinstance ( command_parser , argparse . ArgumentParser ) :
10791098 raise CommandSetRegistrationError (
10801099 f"Could not find argparser for command '{ command_name } ' needed by subcommand: { method } "
10811100 )
@@ -1161,7 +1180,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
11611180 # but keeping in case it does for some strange reason
11621181 raise CommandSetRegistrationError (f"Could not find command '{ command_name } ' needed by subcommand: { method } " )
11631182 command_parser = self ._command_parsers .get (command_func )
1164- if command_parser is None : # pragma: no cover
1183+ if not isinstance ( command_parser , argparse . ArgumentParser ) : # pragma: no cover
11651184 # This really shouldn't be possible since _register_subcommands would prevent this from happening
11661185 # but keeping in case it does for some strange reason
11671186 raise CommandSetRegistrationError (
@@ -2333,11 +2352,11 @@ def _perform_completion(
23332352 if func_attr is not None :
23342353 completer_func = func_attr
23352354 else :
2336- # There's no completer function, next see if the command uses argparse
2355+ # There's no completer function, next see if the command uses a parser
23372356 func = self .cmd_func (command )
23382357 argparser = None if func is None else self ._command_parsers .get (func )
23392358
2340- if func is not None and argparser is not None :
2359+ if func is not None and isinstance ( argparser , argparse . ArgumentParser ) :
23412360 # Get arguments for complete()
23422361 preserve_quotes = getattr (func , constants .CMD_ATTR_PRESERVE_QUOTES )
23432362 cmd_set = self .find_commandset_for_command (command )
@@ -2349,6 +2368,16 @@ def _perform_completion(
23492368 completer_func = functools .partial (
23502369 completer .complete , tokens = raw_tokens [1 :] if preserve_quotes else tokens [1 :], cmd_set = cmd_set
23512370 )
2371+ elif func is not None and isinstance (argparser , TyperParser ):
2372+ from .typer_custom import typer_complete
2373+
2374+ preserve_quotes = getattr (func , constants .CMD_ATTR_PRESERVE_QUOTES )
2375+ completer_func = functools .partial (
2376+ typer_complete ,
2377+ self ,
2378+ command_func = func ,
2379+ args = raw_tokens [1 :] if preserve_quotes else tokens [1 :],
2380+ )
23522381 else :
23532382 completer_func = self .completedefault # type: ignore[assignment]
23542383
@@ -4097,11 +4126,30 @@ def complete_help_subcommands(
40974126 return Completions ()
40984127
40994128 # Check if this command uses argparse
4100- if (func := self .cmd_func (command )) is None or (argparser := self ._command_parsers .get (func )) is None :
4129+ func = self .cmd_func (command )
4130+ if func is None :
41014131 return Completions ()
41024132
4103- completer = argparse_completer .DEFAULT_AP_COMPLETER (argparser , self )
4104- return completer .complete_subcommand_help (text , line , begidx , endidx , arg_tokens ['subcommands' ])
4133+ argparser = self ._command_parsers .get (func )
4134+ if isinstance (argparser , argparse .ArgumentParser ):
4135+ completer = argparse_completer .DEFAULT_AP_COMPLETER (argparser , self )
4136+ return completer .complete_subcommand_help (text , line , begidx , endidx , arg_tokens ['subcommands' ])
4137+
4138+ # Typer/Click subcommand help completion
4139+ if isinstance (argparser , TyperParser ):
4140+ from .typer_custom import typer_complete_subcommand_help
4141+
4142+ return typer_complete_subcommand_help (
4143+ self ,
4144+ text ,
4145+ line ,
4146+ begidx ,
4147+ endidx ,
4148+ command_func = func ,
4149+ subcommands = arg_tokens ['subcommands' ],
4150+ )
4151+
4152+ return Completions ()
41054153
41064154 def _build_command_info (self ) -> tuple [dict [str , list [str ]], list [str ], list [str ], list [str ]]:
41074155 """Categorizes and sorts visible commands and help topics for display.
@@ -4211,10 +4259,21 @@ def do_help(self, args: argparse.Namespace) -> None:
42114259 argparser = None if func is None else self ._command_parsers .get (func )
42124260
42134261 # If the command function uses argparse, then use argparse's help
4214- if func is not None and argparser is not None :
4262+ if func is not None and isinstance ( argparser , argparse . ArgumentParser ) :
42154263 completer = argparse_completer .DEFAULT_AP_COMPLETER (argparser , self )
42164264 completer .print_help (args .subcommands , self .stdout )
4265+ # If the command function uses Typer, then use Click's help
4266+ elif func is not None and isinstance (argparser , TyperParser ):
4267+ from .typer_custom import resolve_typer_subcommand
42174268
4269+ try :
4270+ target_command , resolved_names = resolve_typer_subcommand (argparser , args .subcommands )
4271+ except KeyError :
4272+ target_command , resolved_names = argparser , []
4273+ info_name = ' ' .join ([args .command , * resolved_names ])
4274+ ctx = target_command .make_context (info_name , [], resilient_parsing = True )
4275+ with contextlib .redirect_stdout (self .stdout ):
4276+ target_command .get_help (ctx )
42184277 # If the command has a custom help function, then call it
42194278 elif help_func is not None :
42204279 help_func ()
0 commit comments