@@ -352,38 +352,71 @@ def with_annotated(
352352 ns_provider : Callable [..., argparse .Namespace ] | None = None ,
353353 preserve_quotes : bool = False ,
354354 with_unknown_args : bool = False ,
355- subcommand_to : str | None = None ,
356355 base_command : bool = False ,
356+ subcommand_to : str | None = None ,
357357 help : str | None = None , # noqa: A002
358358 aliases : Sequence [str ] | None = None ,
359+ groups : tuple [tuple [str , ...], ...] | None = None ,
360+ mutually_exclusive_groups : tuple [tuple [str , ...], ...] | None = None ,
359361) -> Any :
360362 """Decorate a ``do_*`` method to build its argparse parser from type annotations.
361363
362- Can be used bare or with keyword arguments::
363-
364- @with_annotated
365- def do_greet(self, name: str, count: int = 1): ...
366-
367- @with_annotated(preserve_quotes=True)
368- def do_raw(self, text: str): ...
369-
370364 :param func: the command function (when used without parentheses)
371- :param ns_provider: optional namespace provider, mirroring ``with_argparser``
372- :param preserve_quotes: if True, preserve quotes in arguments
373- :param with_unknown_args: if True, capture unknown args (passed as extra kwarg ``_unknown``)
365+ :param ns_provider: optional callable returning a prepopulated argparse.Namespace.
366+ Not supported with ``subcommand_to``.
367+ :param preserve_quotes: if True, preserve quotes in arguments.
368+ Not supported with ``subcommand_to``.
369+ :param with_unknown_args: if True, capture unknown args (passed as extra kwarg ``_unknown``).
370+ Not supported with ``subcommand_to``.
371+ :param base_command: if True, this command has subcommands (adds ``add_subparsers()``).
372+ Requires a ``cmd2_handler`` parameter and no positional arguments.
373+ :param subcommand_to: parent command name (e.g. ``'team'`` or ``'team member'``).
374+ Function must be named ``{parent_underscored}_{subcommand}``.
375+ :param help: help text for the subcommand (only valid with ``subcommand_to``)
376+ :param aliases: alternative names for the subcommand (only valid with ``subcommand_to``)
377+ :param groups: tuples of parameter names to place in argument groups (for help display)
378+ :param mutually_exclusive_groups: tuples of parameter names that are mutually exclusive
379+
380+ Example::
381+
382+ class MyApp(cmd2.Cmd):
383+ @with_annotated
384+ def do_greet(self, name: str, count: int = 1): ...
385+
386+ @with_annotated(base_command=True)
387+ def do_team(self, *, cmd2_handler): ...
388+
389+ @with_annotated(subcommand_to='team', help='create a team')
390+ def team_create(self, name: str): ...
391+
374392 """
375393 from .annotated import (
394+ _SKIP_PARAMS ,
376395 _filtered_namespace_kwargs ,
377396 _validate_base_command_params ,
378397 build_parser_from_function ,
379398 build_subcommand_handler ,
380399 )
381400 from .argparse_custom import Cmd2AttributeWrapper
382401
383- def decorator (fn : Callable [..., Any ]) -> Callable [..., Any ]:
384- if subcommand_to is None and (help is not None or aliases ):
385- raise TypeError ("with_annotated(help=..., aliases=...) requires subcommand_to=..." )
402+ if (help is not None or aliases is not None ) and subcommand_to is None :
403+ raise TypeError ("'help' and 'aliases' are only valid with subcommand_to" )
404+ if subcommand_to is not None :
405+ unsupported : list [str ] = []
406+ if ns_provider is not None :
407+ unsupported .append ('ns_provider' )
408+ if preserve_quotes :
409+ unsupported .append ('preserve_quotes' )
410+ if with_unknown_args :
411+ unsupported .append ('with_unknown_args' )
412+ if unsupported :
413+ names = ', ' .join (unsupported )
414+ raise TypeError (
415+ f"{ names } { 'is' if len (unsupported ) == 1 else 'are' } not supported with subcommand_to. "
416+ "Configure these behaviors on the base command instead."
417+ )
386418
419+ def decorator (fn : Callable [..., Any ]) -> Callable [..., Any ]:
387420 if with_unknown_args :
388421 unknown_param = inspect .signature (fn ).parameters .get ('_unknown' )
389422 if unknown_param is None :
@@ -396,10 +429,12 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
396429 fn ,
397430 subcommand_to ,
398431 base_command = base_command ,
432+ groups = groups ,
433+ mutually_exclusive_groups = mutually_exclusive_groups ,
399434 )
400435 setattr (handler , constants .SUBCMD_ATTR_COMMAND , subcommand_to )
401- setattr (handler , constants .CMD_ATTR_ARGPARSER , subcmd_parser_builder )
402436 setattr (handler , constants .SUBCMD_ATTR_NAME , subcmd_name )
437+ setattr (handler , constants .CMD_ATTR_ARGPARSER , subcmd_parser_builder )
403438 add_parser_kwargs : dict [str , Any ] = {}
404439 if help is not None :
405440 add_parser_kwargs ['help' ] = help
@@ -409,13 +444,21 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
409444 return handler
410445
411446 command_name = fn .__name__ [len (constants .COMMAND_FUNC_PREFIX ) :]
447+
448+ skip_params = _SKIP_PARAMS | ({'_unknown' } if with_unknown_args else frozenset ())
412449 if base_command :
413- _validate_base_command_params (fn )
450+ _validate_base_command_params (fn , skip_params = skip_params )
414451
415- accepted = set (inspect .signature (fn ).parameters .keys ()) - {'self' }
452+ # Cache signature introspection at decoration time, not per-invocation
453+ accepted = set (list (inspect .signature (fn ).parameters .keys ())[1 :])
416454
417455 def parser_builder () -> argparse .ArgumentParser :
418- parser = build_parser_from_function (fn )
456+ parser = build_parser_from_function (
457+ fn ,
458+ skip_params = skip_params ,
459+ groups = groups ,
460+ mutually_exclusive_groups = mutually_exclusive_groups ,
461+ )
419462 if base_command :
420463 parser .add_subparsers (dest = 'subcommand' , metavar = 'SUBCOMMAND' , required = True )
421464 return parser
0 commit comments