@@ -277,14 +277,14 @@ def do_build(self, target: str, common: CommonArgs):
277277)
278278from pathlib import Path
279279from typing import (
280+ TYPE_CHECKING ,
280281 Annotated ,
281282 Any ,
282283 ClassVar ,
283284 Literal ,
284285 NamedTuple ,
285286 ParamSpec ,
286287 Protocol ,
287- TypeAlias ,
288288 TypedDict ,
289289 TypeGuard ,
290290 TypeVar ,
@@ -314,15 +314,25 @@ def do_build(self, target: str, common: CommonArgs):
314314 BoundCommandFunc ,
315315 CmdOrSetT ,
316316 UnboundChoicesProvider ,
317- UnboundCommandFunc ,
318317 UnboundCompleter ,
319318)
320319
321320#: ``nargs`` values accepted by cmd2's patched ``add_argument`` (incl. ranged tuples).
322321_NargsValue = int | str | tuple [int ] | tuple [int , int ] | tuple [int , float ]
323322
324- # Parser construction works with a command method before or after descriptor binding.
325- _CommandFunc : TypeAlias = BoundCommandFunc [...] | UnboundCommandFunc [Any , ...]
323+
324+ class _CommandFunc (Protocol ):
325+ """Callable metadata needed to inspect a command before or after descriptor binding."""
326+
327+ if TYPE_CHECKING :
328+
329+ @property
330+ def __name__ (self ) -> str : ...
331+
332+ @property
333+ def __qualname__ (self ) -> str : ...
334+
335+ def __call__ (self , * args : Any , ** kwargs : Any ) -> Any : ...
326336
327337
328338class Cmd2ParserKwargs (TypedDict , total = False ):
@@ -2879,38 +2889,58 @@ def parser_builder() -> Cmd2ArgumentParser:
28792889
28802890_CommandParams = ParamSpec ("_CommandParams" )
28812891_DecoratorParams = ParamSpec ("_DecoratorParams" )
2892+ _CommandReturn = TypeVar ("_CommandReturn" )
2893+ _CommandReturn_co = TypeVar ("_CommandReturn_co" , covariant = True )
2894+
2895+
2896+ class _BoundAnnotatedCommand (Protocol [_CommandParams , _CommandReturn_co ]):
2897+ """A bound annotated command whose complete call signature is preserved."""
28822898
2899+ if TYPE_CHECKING :
28832900
2884- class _AnnotatedCommand (Protocol [CmdOrSetT , _CommandParams ]):
2901+ @property
2902+ def __name__ (self ) -> str : ...
2903+
2904+ @property
2905+ def __qualname__ (self ) -> str : ...
2906+
2907+ def __call__ (self , * args : _CommandParams .args , ** kwargs : _CommandParams .kwargs ) -> _CommandReturn_co : ...
2908+
2909+
2910+ class _AnnotatedCommand (Protocol [CmdOrSetT , _CommandParams , _CommandReturn_co ]):
28852911 """A callable command method with the metadata used by ``with_annotated``."""
28862912
28872913 __name__ : str
28882914 __qualname__ : str
28892915
2890- def __call__ (self , __self : CmdOrSetT , / , * args : _CommandParams .args , ** kwargs : _CommandParams .kwargs ) -> bool | None : ...
2916+ def __call__ (
2917+ self , __self : CmdOrSetT , / , * args : _CommandParams .args , ** kwargs : _CommandParams .kwargs
2918+ ) -> _CommandReturn_co : ...
28912919
28922920 @overload
2893- def __get__ (self , instance : None , owner : Any = ...) -> "_AnnotatedCommand[CmdOrSetT, _CommandParams]" : ...
2921+ def __get__ (
2922+ self , instance : None , owner : Any = ...
2923+ ) -> "_AnnotatedCommand[CmdOrSetT, _CommandParams, _CommandReturn_co]" : ...
28942924
28952925 @overload
2896- def __get__ (self , instance : CmdOrSetT , owner : Any = ...) -> BoundCommandFunc [_CommandParams ]: ...
2926+ def __get__ (self , instance : CmdOrSetT , owner : Any = ...) -> _BoundAnnotatedCommand [_CommandParams , _CommandReturn_co ]: ...
28972927
28982928
28992929class _WithAnnotatedDecorator (Protocol ):
29002930 """The signature-preserving decorator ``with_annotated(...)`` returns (generic per call)."""
29012931
29022932 def __call__ (
2903- self , fn : _AnnotatedCommand [CmdOrSetT , _CommandParams ], /
2904- ) -> _AnnotatedCommand [CmdOrSetT , _CommandParams ]: ...
2933+ self , fn : _AnnotatedCommand [CmdOrSetT , _CommandParams , _CommandReturn ], /
2934+ ) -> _AnnotatedCommand [CmdOrSetT , _CommandParams , _CommandReturn ]: ...
29052935
29062936
29072937def _build_subcommand_handler (
2908- func : _AnnotatedCommand [Any , ...],
2938+ func : _AnnotatedCommand [Any , ..., Any ],
29092939 subcommand_to : str ,
29102940 * ,
29112941 base_command : bool = False ,
29122942 options : _ParserBuildOptions ,
2913- ) -> tuple [_AnnotatedCommand [Any , ...], str , Callable [[], Cmd2ArgumentParser ]]:
2943+ ) -> tuple [_AnnotatedCommand [Any , ..., Any ], str , Callable [[], Cmd2ArgumentParser ]]:
29142944 """Build a subcommand's parser and a handler that unpacks the Namespace into typed kwargs.
29152945
29162946 :param func: the subcommand handler function
@@ -2936,7 +2966,7 @@ def _build_subcommand_handler(
29362966 _leading_names , _var_positional_name = _var_positional_call_plan (func )
29372967
29382968 @functools .wraps (func )
2939- def handler (* args : Any , ** kwargs : Any ) -> bool | None :
2969+ def handler (* args : Any , ** kwargs : Any ) -> Any :
29402970 """Unpack Namespace into typed kwargs for the subcommand handler."""
29412971 if kwargs :
29422972 raise TypeError ("subcommand handlers do not accept keyword arguments" )
@@ -2952,19 +2982,18 @@ def handler(*args: Any, **kwargs: Any) -> bool | None:
29522982 if isinstance (cmd2_h , functools .partial ) and getattr (cmd2_h .func , "__func__" , cmd2_h .func ) is handler :
29532983 filtered [constants .NS_ATTR_SUBCOMMAND_FUNC ] = None
29542984 _reconstruct_dataclass_blocks (filtered , _blocks , ns )
2955- result = _invoke_command_func (
2985+ return _invoke_command_func (
29562986 func , self_arg , filtered , leading_names = _leading_names , var_positional_name = _var_positional_name
29572987 )
2958- if result is None or isinstance (result , bool ):
2959- return result
2960- raise TypeError ("annotated command functions must return bool or None" )
29612988
29622989 parser_builder = _make_parser_builder (func , skip_params = _SKIP_PARAMS , base_command = base_command , options = options )
2963- return cast (_AnnotatedCommand [Any , ...], handler ), subcmd_name , parser_builder
2990+ return cast (_AnnotatedCommand [Any , ..., Any ], handler ), subcmd_name , parser_builder
29642991
29652992
29662993@overload
2967- def with_annotated (func : _AnnotatedCommand [CmdOrSetT , _CommandParams ], / ) -> _AnnotatedCommand [CmdOrSetT , _CommandParams ]: ...
2994+ def with_annotated (
2995+ func : _AnnotatedCommand [CmdOrSetT , _CommandParams , _CommandReturn ], /
2996+ ) -> _AnnotatedCommand [CmdOrSetT , _CommandParams , _CommandReturn ]: ...
29682997
29692998
29702999@overload
@@ -2991,7 +3020,7 @@ def with_annotated(
29913020
29923021
29933022def with_annotated (
2994- func : _AnnotatedCommand [CmdOrSetT , _CommandParams ] | None = None ,
3023+ func : _AnnotatedCommand [CmdOrSetT , _CommandParams , _CommandReturn ] | None = None ,
29953024 * ,
29963025 ns_provider : Callable [..., argparse .Namespace ] | None = None ,
29973026 preserve_quotes : bool = False ,
@@ -3009,7 +3038,7 @@ def with_annotated(
30093038 subcommand_title : str | None = None ,
30103039 subcommand_description : str | None = None ,
30113040 ** parser_kwargs : Unpack [Cmd2ParserKwargs ],
3012- ) -> _AnnotatedCommand [CmdOrSetT , _CommandParams ] | _WithAnnotatedDecorator :
3041+ ) -> _AnnotatedCommand [CmdOrSetT , _CommandParams , _CommandReturn ] | _WithAnnotatedDecorator :
30133042 """Decorate a ``do_*`` method to build its argparse parser from type annotations.
30143043
30153044 :param func: the command function (when used without parentheses)
@@ -3071,8 +3100,8 @@ def with_annotated(
30713100 )
30723101
30733102 def decorator (
3074- command_func : _AnnotatedCommand [CmdOrSetT , _DecoratorParams ],
3075- ) -> _AnnotatedCommand [CmdOrSetT , _DecoratorParams ]:
3103+ command_func : _AnnotatedCommand [CmdOrSetT , _DecoratorParams , _CommandReturn ],
3104+ ) -> _AnnotatedCommand [CmdOrSetT , _DecoratorParams , _CommandReturn ]:
30763105 if with_unknown_args :
30773106 unknown_param = inspect .signature (command_func ).parameters .get ("_unknown" )
30783107 if unknown_param is None :
@@ -3102,7 +3131,7 @@ def decorator(
31023131 parser_source = subcmd_parser_builder ,
31033132 )
31043133 setattr (handler , constants .SUBCOMMAND_ATTR_SPEC , subcommand_spec )
3105- return cast (_AnnotatedCommand [CmdOrSetT , _DecoratorParams ], handler )
3134+ return cast (_AnnotatedCommand [CmdOrSetT , _DecoratorParams , _CommandReturn ], handler )
31063135
31073136 command_name = command_func .__name__ [len (constants .COMMAND_FUNC_PREFIX ) :]
31083137
@@ -3125,14 +3154,14 @@ def decorator(
31253154 )
31263155
31273156 @functools .wraps (command_func )
3128- def cmd_wrapper (* args : Any , ** kwargs : Any ) -> bool | None :
3157+ def cmd_wrapper (* args : Any , ** kwargs : Any ) -> _CommandReturn :
31293158 cmd2_app , statement_arg = _parse_positionals (args )
31303159 owner = args [0 ] # Cmd or CommandSet instance
31313160 statement , parsed_arglist = cmd2_app .statement_parser .get_command_arg_list (
31323161 command_name , statement_arg , preserve_quotes
31333162 )
31343163
3135- arg_parser = cmd2_app .command_parsers .get (cmd_wrapper )
3164+ arg_parser = cmd2_app .command_parsers .get (cast ( BoundCommandFunc [...], cmd_wrapper ) )
31363165 if arg_parser is None :
31373166 raise ValueError (f"No argument parser found for { command_name } " )
31383167
@@ -3168,18 +3197,18 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
31683197
31693198 func_kwargs .update (kwargs )
31703199 _reconstruct_dataclass_blocks (func_kwargs , blocks , ns )
3171- result : bool | None = _invoke_command_func (
3200+ result = _invoke_command_func (
31723201 command_func , owner , func_kwargs , leading_names = leading_names , var_positional_name = var_positional_name
31733202 )
3174- return result
3203+ return cast ( _CommandReturn , result )
31753204
31763205 argparse_command_spec = ArgparseCommandSpec (
31773206 parser_source = parser_builder ,
31783207 preserve_quotes = preserve_quotes ,
31793208 )
31803209 setattr (cmd_wrapper , constants .ARGPARSE_COMMAND_ATTR_SPEC , argparse_command_spec )
31813210
3182- return cast (_AnnotatedCommand [CmdOrSetT , _DecoratorParams ], cmd_wrapper )
3211+ return cast (_AnnotatedCommand [CmdOrSetT , _DecoratorParams , _CommandReturn ], cmd_wrapper )
31833212
31843213 # Support both @with_annotated and @with_annotated(...)
31853214 if func is not None :
0 commit comments