@@ -355,38 +355,71 @@ def with_annotated(
355355 ns_provider : Callable [..., argparse .Namespace ] | None = None ,
356356 preserve_quotes : bool = False ,
357357 with_unknown_args : bool = False ,
358- subcommand_to : str | None = None ,
359358 base_command : bool = False ,
359+ subcommand_to : str | None = None ,
360360 help : str | None = None , # noqa: A002
361361 aliases : Sequence [str ] | None = None ,
362+ groups : tuple [tuple [str , ...], ...] | None = None ,
363+ mutually_exclusive_groups : tuple [tuple [str , ...], ...] | None = None ,
362364) -> Any :
363365 """Decorate a ``do_*`` method to build its argparse parser from type annotations.
364366
365- Can be used bare or with keyword arguments::
366-
367- @with_annotated
368- def do_greet(self, name: str, count: int = 1): ...
369-
370- @with_annotated(preserve_quotes=True)
371- def do_raw(self, text: str): ...
372-
373367 :param func: the command function (when used without parentheses)
374- :param ns_provider: optional namespace provider, mirroring ``with_argparser``
375- :param preserve_quotes: if True, preserve quotes in arguments
376- :param with_unknown_args: if True, capture unknown args (passed as extra kwarg ``_unknown``)
368+ :param ns_provider: optional callable returning a prepopulated argparse.Namespace.
369+ Not supported with ``subcommand_to``.
370+ :param preserve_quotes: if True, preserve quotes in arguments.
371+ Not supported with ``subcommand_to``.
372+ :param with_unknown_args: if True, capture unknown args (passed as extra kwarg ``_unknown``).
373+ Not supported with ``subcommand_to``.
374+ :param base_command: if True, this command has subcommands (adds ``add_subparsers()``).
375+ Requires a ``cmd2_handler`` parameter and no positional arguments.
376+ :param subcommand_to: parent command name (e.g. ``'team'`` or ``'team member'``).
377+ Function must be named ``{parent_underscored}_{subcommand}``.
378+ :param help: help text for the subcommand (only valid with ``subcommand_to``)
379+ :param aliases: alternative names for the subcommand (only valid with ``subcommand_to``)
380+ :param groups: tuples of parameter names to place in argument groups (for help display)
381+ :param mutually_exclusive_groups: tuples of parameter names that are mutually exclusive
382+
383+ Example::
384+
385+ class MyApp(cmd2.Cmd):
386+ @with_annotated
387+ def do_greet(self, name: str, count: int = 1): ...
388+
389+ @with_annotated(base_command=True)
390+ def do_team(self, *, cmd2_handler): ...
391+
392+ @with_annotated(subcommand_to='team', help='create a team')
393+ def team_create(self, name: str): ...
394+
377395 """
378396 from .annotated import (
397+ _SKIP_PARAMS ,
379398 _filtered_namespace_kwargs ,
380399 _validate_base_command_params ,
381400 build_parser_from_function ,
382401 build_subcommand_handler ,
383402 )
384403 from .argparse_custom import Cmd2AttributeWrapper
385404
386- def decorator (fn : Callable [..., Any ]) -> Callable [..., Any ]:
387- if subcommand_to is None and (help is not None or aliases ):
388- raise TypeError ("with_annotated(help=..., aliases=...) requires subcommand_to=..." )
405+ if (help is not None or aliases is not None ) and subcommand_to is None :
406+ raise TypeError ("'help' and 'aliases' are only valid with subcommand_to" )
407+ if subcommand_to is not None :
408+ unsupported : list [str ] = []
409+ if ns_provider is not None :
410+ unsupported .append ('ns_provider' )
411+ if preserve_quotes :
412+ unsupported .append ('preserve_quotes' )
413+ if with_unknown_args :
414+ unsupported .append ('with_unknown_args' )
415+ if unsupported :
416+ names = ', ' .join (unsupported )
417+ raise TypeError (
418+ f"{ names } { 'is' if len (unsupported ) == 1 else 'are' } not supported with subcommand_to. "
419+ "Configure these behaviors on the base command instead."
420+ )
389421
422+ def decorator (fn : Callable [..., Any ]) -> Callable [..., Any ]:
390423 if with_unknown_args :
391424 unknown_param = inspect .signature (fn ).parameters .get ('_unknown' )
392425 if unknown_param is None :
@@ -399,10 +432,12 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
399432 fn ,
400433 subcommand_to ,
401434 base_command = base_command ,
435+ groups = groups ,
436+ mutually_exclusive_groups = mutually_exclusive_groups ,
402437 )
403438 setattr (handler , constants .SUBCMD_ATTR_COMMAND , subcommand_to )
404- setattr (handler , constants .CMD_ATTR_ARGPARSER , subcmd_parser_builder )
405439 setattr (handler , constants .SUBCMD_ATTR_NAME , subcmd_name )
440+ setattr (handler , constants .CMD_ATTR_ARGPARSER , subcmd_parser_builder )
406441 add_parser_kwargs : dict [str , Any ] = {}
407442 if help is not None :
408443 add_parser_kwargs ['help' ] = help
@@ -412,13 +447,21 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
412447 return handler
413448
414449 command_name = fn .__name__ [len (constants .COMMAND_FUNC_PREFIX ) :]
450+
451+ skip_params = _SKIP_PARAMS | ({'_unknown' } if with_unknown_args else frozenset ())
415452 if base_command :
416- _validate_base_command_params (fn )
453+ _validate_base_command_params (fn , skip_params = skip_params )
417454
418- accepted = set (inspect .signature (fn ).parameters .keys ()) - {'self' }
455+ # Cache signature introspection at decoration time, not per-invocation
456+ accepted = set (list (inspect .signature (fn ).parameters .keys ())[1 :])
419457
420458 def parser_builder () -> argparse .ArgumentParser :
421- parser = build_parser_from_function (fn )
459+ parser = build_parser_from_function (
460+ fn ,
461+ skip_params = skip_params ,
462+ groups = groups ,
463+ mutually_exclusive_groups = mutually_exclusive_groups ,
464+ )
422465 if base_command :
423466 parser .add_subparsers (dest = 'subcommand' , metavar = 'SUBCOMMAND' , required = True )
424467 return parser
0 commit comments