Skip to content

cmd2.annotated

cmd2.annotated

Build argparse parsers from type-annotated function signatures.

Experimental

This module is experimental and its behavior may change in future releases.

The with_annotated decorator inspects a command function's type hints and default values to build a Cmd2ArgumentParser. Argument and Option metadata classes give finer per-parameter control via typing.Annotated.

Parameters without defaults become positional arguments; parameters with defaults become --option flags; keyword-only parameters (after *) are always options. A bool option is a flag, not a value: when absent it means False (or None for bool | None), so it defaults to that and is never required. A *args parameter becomes a variadic positional accepting zero or more values (nargs='*'), collected into a tuple. Underscores in a parameter name become dashes in the generated flag (dry_run -> --dry-run); pass an explicit Option("--my_flag") to opt out. Positional-only parameters (before /) and **kwargs raise TypeError. The parameter names dest and subcommand are reserved; cmd2_statement receives the parsed Statement and (with base_command=True) cmd2_subcommand_func receives the subcommand handler:

class MyApp(cmd2.Cmd):
    @cmd2.with_annotated
    def do_greet(self, name: str, count: int = 1, loud: bool = False):
        for _ in range(count):
            msg = f"Hello {name}"
            self.poutput(msg.upper() if loud else msg)

Use Annotated with Argument or Option for finer control over individual parameters:

from typing import Annotated


class MyApp(cmd2.Cmd):
    def color_choices(self) -> cmd2.Choices:
        return cmd2.Choices.from_values(["red", "green", "blue"])

    @cmd2.with_annotated
    def do_paint(
        self,
        item: str,
        color: Annotated[str, Option("--color", "-c", choices_provider=color_choices, help_text="Color to use")] = "blue",
    ):
        self.poutput(f"Painting {item} {color}")

How annotations map to argparse settings:

  • str -- default string argument
  • int, float -- sets type=
  • bool option -- --flag / --no-flag via BooleanOptionalAction; defaults to False (or None for bool | None) when omitted, so it is never required
  • positional bool -- parsed from true/false, yes/no, on/off, 1/0
  • pathlib.Path -- sets type=Path
  • enum.Enum subclass -- type=converter, choices from member values
  • decimal.Decimal -- sets type=Decimal
  • Literal[...] -- type=converter and choices from the literal values
  • list[T] / set[T] / tuple[T, ...] -- nargs='+' (or '*' with a default or | None)
  • tuple[T, T] (fixed arity, same type) -- nargs=N with type=T
  • *args: T -- variadic positional (nargs='*'); T is each value's type, not the collected tuple. Annotated[T, Argument(...)] metadata is honored
  • T | None (no default) -- positional with nargs='?' (0-or-1 tokens)
  • T | None = None -- --flag option with default=None

A value option with no default is made required (omitting it would pass None, violating a non-Optional hint); annotate it T | None or give it a default to make it omittable.

Explicit Option(action=...) is type-checked so the parsed result matches the declared type:

  • store_true / store_false -- require a bool parameter (type= is dropped; argparse supplies the False/True default)
  • count -- requires an int parameter; defaults to 0 (None for int | None)
  • append / extend -- require a list[T] parameter and default to [] (append takes one value per flag; extend takes nargs values per flag)
  • store_const / append_const -- store the Option(const=...) value (type= is dropped). The action is inferred from the type when action= is omitted: a scalar Option(const=X) becomes store_const (present -> const, absent -> the default, which must exist or be T | None); a list[T] Option(const=X) becomes append_const (each flag appends const; defaults to []). A scalar Option(const=X) given an explicit nargs (e.g. nargs='?') instead keeps the store action for argparse's optional-value idiom (absent -> default, bare flag -> const, flag VALUE -> converted VALUE); the const is stored verbatim and must match the declared type. const is validated against the declared type and is rejected on a positional Argument (argparse ignores it there)
  • a custom argparse.Action subclass -- passed straight through to add_argument. The user's class owns storage, so the collection-casting wrapper is dropped and the action-specific type/const/collection-shape constraints are skipped. The type-inferred converter, default, and required are still applied; the action receives them like any hand-built add_argument call. action='help' and action='version' are not supported.

The zero-argument actions above (store_true / store_false / count / store_const / append_const) take no value from the command line, so the value-oriented metadata inferred from the type is dropped before add_argument is called: the type= converter, the static choices, and any inferred tab-completer (e.g. the path completer for Path). There is nothing to complete or convert on a value-less action. A completer that was only inferred from the type is dropped silently, but a completer / choices_provider you supply explicitly on such an action is a contradiction and raises TypeError (matching argparse, which rejects it outright). Actions that do consume values (append / extend on a list[T], or a plain value option) keep the inferred converter and completer unchanged.

The metadata classes refuse a handful of add_argument kwargs that the decorator derives from the signature itself, so passing them through Argument(...) / Option(...) raises TypeError: type (from the annotation), dest (from the parameter name), help (use the help_text parameter, which maps to it -- a raw help would silently shadow it), and -- on Argument only -- action / required (which have no meaning on a positional). Every other add_argument parameter, including those registered via register_argparse_argument_parameter, passes through unchanged.

A default may be supplied either as the function-signature default (param: T = v) or as Argument(default=v) / Option(default=v) -- the two forms are equivalent. Specifying both at once raises TypeError (the value would have two sources of truth), and argparse.SUPPRESS is rejected as a default from either source because it would remove the keyword argument the function expects.

Parser-level customization is forwarded to Cmd2ArgumentParser's constructor via PEP 692 **parser_kwargs: Unpack[Cmd2ParserKwargs]. Anything the parser ctor accepts -- description, epilog, prog, usage, parents, argument_default, prefix_chars, fromfile_prefix_chars, conflict_handler, add_help, allow_abbrev, exit_on_error, formatter_class, completer_class, and on Python >= 3.14 suggest_on_error / color -- flows straight through; the Cmd2ParserKwargs TypedDict is the single source of truth and gives type-checkers/IDEs autocomplete on the decorator's call site. parser_class stays as its own explicit kwarg because it selects the class itself, not a value passed to it. Two behaviors layer on top of the raw passthrough: if description is omitted, the first paragraph of func.__doc__ (up to the first blank line) is used so docstrings double as help text without leaking :param: directives; and prog is rejected with subcommand_to because cmd2 rewrites it from the parent command's hierarchy. Mutually exclusive groups accept Group(required=True) to require exactly one member; the same flag on a plain groups= entry raises ValueError (argparse's add_argument_group has no required).

Unsupported patterns (raise TypeError):

  • a non-Optional type with a None default (e.g. name: str = None); annotate it T | None or use a non-None default. Any/object/unannotated are exempt
  • a scalar type with no converter (e.g. datetime.datetime, uuid.UUID, bytes, or any custom class), which would silently arrive as a plain string. Supported scalars are str, int, float, bool, decimal.Decimal, pathlib.Path, enum.Enum subclasses, and Literal[...] (str/Any/object pass through raw)
  • str | int -- a union of multiple non-None types is ambiguous
  • tuple[int, str, float] -- mixed element types (argparse applies one type= per argument)
  • *args: tuple[T, ...] (or any collection element) -- the annotation is each value's type, so a collection element means a tuple-of-collections; annotate the element, e.g. *args: str
  • *args: Annotated[T, Option(...)] -- *args is always positional; use Argument()
  • *args: Annotated[T, Argument(nargs=N)] -- *args arity is fixed to nargs='*'
  • a keyword-only parameter annotated with Argument() -- it marks a positional; use Option()
  • a required option (no default, not T | None) in a mutually_exclusive_groups group -- only one member is supplied, so the others arrive as None; give it a default or T | None
  • Annotated[T, Argument(nargs=N)] producing a list ('*', '+', integer >= 1) on a non-collection T; use list[T] or tuple[T, ...] to match the runtime shape
  • Annotated[tuple[T, T], Argument(nargs=N)] where N differs from the tuple's arity
  • Option(action=...) whose result type mismatches the declared type, an unsupported action, or a non-list action on a collection (use append/extend/append_const with list[T])
  • a variable-arity positional (T | None, list[T], tuple[T, ...]) followed by another positional -- it must come last (def f(self, a: str, *rest: str) is fine)

When combining Annotated with Optional, the union should go inside: Annotated[T | None, meta]. Annotated[T, meta] | None is ambiguous and raises -- unless the inner type already carries the None (Annotated[T | None, meta] | None), in which case the redundant outer | None is accepted as equivalent to Annotated[T | None, meta].

Path and Enum annotations also get automatic tab completion. A user-supplied choices_provider or completer drives completion in place of the inferred static choices, while the inferred type converter is kept so values still coerce to the declared type (an Enum to its member, Literal[1, 2] to int) and out-of-type values are rejected at parse time. An Enum accepts both member values and member names on the command line (completion and --help show the values).

An explicit choices= is reconciled with the inferred type rather than fighting it: its values are run through the inferred type converter so they match argparse's post-conversion comparison (Annotated[int, Option('--n', choices=['1', '2'])] becomes choices=[1, 2], so --n 1 matches; a value the converter rejects is a build-time TypeError), and an explicit choices= takes precedence over a type-inferred completer (the Path completer is dropped so the choices drive both validation and completion). A choices_provider / completer you supply yourself still wins over choices=.

ArgMetadata module-attribute

ArgMetadata = Argument | Option | None

Cmd2ParserKwargs

Bases: TypedDict

Forwarded ctor kwargs for Cmd2ArgumentParser (PEP 692 Unpack).

Single source of truth mirroring the parser's __init__: add a field here to expose a new ctor kwarg on the decorator's call site. All optional (total=False); suggest_on_error and color only take effect on Python >= 3.14.

prog instance-attribute

prog

usage instance-attribute

usage

description instance-attribute

description

epilog instance-attribute

epilog

parents instance-attribute

parents

formatter_class instance-attribute

formatter_class

prefix_chars instance-attribute

prefix_chars

fromfile_prefix_chars instance-attribute

fromfile_prefix_chars

argument_default instance-attribute

argument_default

conflict_handler instance-attribute

conflict_handler

add_help instance-attribute

add_help

allow_abbrev instance-attribute

allow_abbrev

exit_on_error instance-attribute

exit_on_error

suggest_on_error instance-attribute

suggest_on_error

color instance-attribute

color

completer_class instance-attribute

completer_class

Argument

Argument(
    *,
    help_text=None,
    metavar=None,
    nargs=None,
    choices=None,
    choices_provider=None,
    completer=None,
    table_columns=None,
    suppress_tab_hint=None,
    const=_UNSET,
    default=_UNSET,
    **extra_kwargs,
)

Bases: _BaseArgMetadata

Metadata for a positional argument in an Annotated type hint.

Initialise shared metadata fields.

const is the value stored on a present flag with no argument (Option only: store_const/append_const); _UNSET distinguishes "no const" from const=None. default mirrors the signature default (Option(default=v) == ... = v); supplying both, or argparse.SUPPRESS, is rejected. extra_kwargs forwards any other add_argument parameter (incl. those from register_argparse_argument_parameter) straight through.

Source code in cmd2/annotated.py
def __init__(
    self,
    *,
    help_text: str | None = None,
    metavar: str | tuple[str, ...] | None = None,
    nargs: _NargsValue | None = None,
    choices: Iterable[Any] | None = None,
    choices_provider: UnboundChoicesProvider[CmdOrSetT] | None = None,
    completer: UnboundCompleter[CmdOrSetT] | None = None,
    table_columns: Sequence[str | Column] | None = None,
    suppress_tab_hint: bool | None = None,
    const: Any = _UNSET,
    default: Any = _UNSET,
    **extra_kwargs: Any,
) -> None:
    """Initialise shared metadata fields.

    ``const`` is the value stored on a present flag with no argument (``Option`` only:
    ``store_const``/``append_const``); ``_UNSET`` distinguishes "no const" from ``const=None``.
    ``default`` mirrors the signature default (``Option(default=v)`` == ``... = v``); supplying
    both, or ``argparse.SUPPRESS``, is rejected.  ``extra_kwargs`` forwards any other
    ``add_argument`` parameter (incl. those from
    [`register_argparse_argument_parameter`][cmd2.argparse_utils.register_argparse_argument_parameter]) straight through.
    """
    reserved = self._RESERVED_EXTRA_KWARGS & extra_kwargs.keys()
    if reserved:
        name = sorted(reserved)[0]
        # Per-key remediation hint for the reserved kwarg.
        hint = {
            "type": "The converter is derived from the parameter annotation; change the annotation instead.",
            "dest": "The dest is the parameter name; rename the parameter instead.",
            "action": "Use Option(action=...) (only Option supports an action; Argument is always positional).",
            "required": (
                "Use Option(required=True); a positional Argument is always required unless it has "
                "a default or is annotated as `T | None`."
            ),
            "help": "Use the help_text= parameter instead; it maps to argparse's help= and would otherwise be shadowed.",
        }[name]
        raise TypeError(f"{type(self).__name__}({name}=...) is not accepted by @with_annotated. {hint}")
    self.help_text = help_text
    self.metavar = metavar
    self.nargs = nargs
    self.choices = choices
    self.choices_provider = choices_provider
    self.completer = completer
    self.table_columns = table_columns
    self.suppress_tab_hint = suppress_tab_hint
    self.const = const
    self.default = default
    self.extra_kwargs = extra_kwargs

help_text instance-attribute

help_text = help_text

metavar instance-attribute

metavar = metavar

nargs instance-attribute

nargs = nargs

choices instance-attribute

choices = choices

choices_provider instance-attribute

choices_provider = choices_provider

completer instance-attribute

completer = completer

table_columns instance-attribute

table_columns = table_columns

suppress_tab_hint instance-attribute

suppress_tab_hint = suppress_tab_hint

const instance-attribute

const = const

default instance-attribute

default = default

extra_kwargs instance-attribute

extra_kwargs = extra_kwargs

to_kwargs

to_kwargs()

Return non-None mapped fields, an explicit const, and any passthrough extra_kwargs.

Source code in cmd2/annotated.py
def to_kwargs(self) -> dict[str, Any]:
    """Return non-None mapped fields, an explicit ``const``, and any passthrough ``extra_kwargs``."""
    kwargs = {kwarg: val for attr, kwarg in self._KWARGS_MAP.items() if (val := getattr(self, attr)) is not None}
    if self.const is not _UNSET:
        kwargs["const"] = self.const
    kwargs.update(self.extra_kwargs)
    return kwargs

Option

Option(*names, action=None, required=False, **kwargs)

Bases: _BaseArgMetadata

Metadata for an optional/flag argument in an Annotated type hint.

Positional *names are the flag strings (e.g. "--color", "-c"); when omitted the decorator generates --param-name (underscores become dashes).

Initialise Option metadata.

action is a supported string action (store_true/store_false/count/ append/extend/store_const/append_const) or a custom argparse.Action subclass (passed through; it owns storage, so the inferred action and the action-specific constraints are skipped).

Source code in cmd2/annotated.py
def __init__(
    self,
    *names: str,
    action: str | type[argparse.Action] | None = None,
    required: bool = False,
    **kwargs: Any,
) -> None:
    """Initialise Option metadata.

    ``action`` is a supported string action (``store_true``/``store_false``/``count``/
    ``append``/``extend``/``store_const``/``append_const``) or a custom
    `argparse.Action` subclass (passed through; it owns storage, so the inferred
    action and the action-specific constraints are skipped).
    """
    super().__init__(**kwargs)
    self.names = names
    self.action = action
    self.required = required

names instance-attribute

names = names

action instance-attribute

action = action

required instance-attribute

required = required

help_text instance-attribute

help_text = help_text

metavar instance-attribute

metavar = metavar

nargs instance-attribute

nargs = nargs

choices instance-attribute

choices = choices

choices_provider instance-attribute

choices_provider = choices_provider

completer instance-attribute

completer = completer

table_columns instance-attribute

table_columns = table_columns

suppress_tab_hint instance-attribute

suppress_tab_hint = suppress_tab_hint

const instance-attribute

const = const

default instance-attribute

default = default

extra_kwargs instance-attribute

extra_kwargs = extra_kwargs

to_kwargs

to_kwargs()

Return non-None fields as an argparse kwargs dict.

Source code in cmd2/annotated.py
def to_kwargs(self) -> dict[str, Any]:
    """Return non-None fields as an argparse kwargs dict."""
    kwargs = super().to_kwargs()
    if self.action is not None:
        kwargs["action"] = self.action
    if self.required:
        kwargs["required"] = True
    return kwargs

Group

Group(
    *members, title=None, description=None, required=False
)

Argument-group definition for with_annotated(groups=...) / mutually_exclusive_groups=....

Initialise an argument group definition.

PARAMETER DESCRIPTION
members

parameter names to place in the group (at least one)

TYPE: str DEFAULT: ()

title

group title shown as a help section header

TYPE: str | None DEFAULT: None

description

group description shown under the title

TYPE: str | None DEFAULT: None

required

mutually_exclusive_groups only -- require exactly one member. On a groups= entry it raises ValueError.

TYPE: bool DEFAULT: False

Source code in cmd2/annotated.py
def __init__(
    self,
    *members: str,
    title: str | None = None,
    description: str | None = None,
    required: bool = False,
) -> None:
    """Initialise an argument group definition.

    :param members: parameter names to place in the group (at least one)
    :param title: group title shown as a help section header
    :param description: group description shown under the title
    :param required: ``mutually_exclusive_groups`` only -- require exactly one member.
                     On a ``groups=`` entry it raises ``ValueError``.
    """
    if not members:
        raise ValueError("Group requires at least one member parameter name")
    self.members = members
    self.title = title
    self.description = description
    self.required = required

members instance-attribute

members = members

title instance-attribute

title = title

description instance-attribute

description = description

required instance-attribute

required = required

build_parser_from_function

build_parser_from_function(
    func,
    *,
    skip_params=_SKIP_PARAMS,
    groups=None,
    mutually_exclusive_groups=None,
    parser_class=None,
    **parser_kwargs,
)

Inspect a function's signature and build a Cmd2ArgumentParser.

The lower-level entry point behind with_annotated. parser_kwargs is forwarded to the parser ctor (see Cmd2ParserKwargs); when description is omitted, the first paragraph of func.__doc__ is used.

PARAMETER DESCRIPTION
func

the command function to inspect

TYPE: Callable[..., Any]

skip_params

parameter names to exclude from the parser

TYPE: frozenset[str] DEFAULT: _SKIP_PARAMS

groups

Group instances assigning parameters to argument groups

TYPE: tuple[Group, ...] | None DEFAULT: None

mutually_exclusive_groups

Group instances of mutually exclusive parameters

TYPE: tuple[Group, ...] | None DEFAULT: None

parser_class

custom parser class (defaults to the configured default)

TYPE: type[Cmd2ArgumentParser] | None DEFAULT: None

parser_kwargs

forwarded Cmd2ParserKwargs

TYPE: Unpack[Cmd2ParserKwargs] DEFAULT: {}

RETURNS DESCRIPTION
Cmd2ArgumentParser

a fully configured Cmd2ArgumentParser

Source code in cmd2/annotated.py
def build_parser_from_function(
    func: Callable[..., Any],
    *,
    skip_params: frozenset[str] = _SKIP_PARAMS,
    groups: tuple[Group, ...] | None = None,
    mutually_exclusive_groups: tuple[Group, ...] | None = None,
    parser_class: type[Cmd2ArgumentParser] | None = None,
    **parser_kwargs: Unpack[Cmd2ParserKwargs],
) -> Cmd2ArgumentParser:
    """Inspect a function's signature and build a ``Cmd2ArgumentParser``.

    The lower-level entry point behind [`with_annotated`][cmd2.annotated.with_annotated].  ``parser_kwargs`` is forwarded to
    the parser ctor (see [`Cmd2ParserKwargs`][cmd2.annotated.Cmd2ParserKwargs]); when ``description`` is omitted, the first
    paragraph of ``func.__doc__`` is used.

    :param func: the command function to inspect
    :param skip_params: parameter names to exclude from the parser
    :param groups: [`Group`][cmd2.annotated.Group] instances assigning parameters to argument groups
    :param mutually_exclusive_groups: [`Group`][cmd2.annotated.Group] instances of mutually exclusive parameters
    :param parser_class: custom parser class (defaults to the configured default)
    :param parser_kwargs: forwarded [`Cmd2ParserKwargs`][cmd2.annotated.Cmd2ParserKwargs]
    :return: a fully configured ``Cmd2ArgumentParser``
    """
    from . import argparse_utils

    parser_cls = parser_class or argparse_utils.DEFAULT_ARGUMENT_PARSER
    if "description" not in parser_kwargs:
        auto_description = _docstring_first_paragraph(func.__doc__)
        if auto_description is not None:
            parser_kwargs["description"] = auto_description
    parser = parser_cls(**parser_kwargs)

    # _resolve_parameters validates each argument and the cross-argument/cross-config rules (e.g. a
    # variable-arity positional must be last; double-assignment and required-mutex-member) once the
    # whole list is built and the group memberships are linked.
    resolved = _resolve_parameters(
        func,
        skip_params=skip_params,
        groups=groups,
        mutually_exclusive_groups=mutually_exclusive_groups,
    )

    # ``argument_default=argparse.SUPPRESS`` drops an absent argument from the parsed namespace.
    # @with_annotated builds the call from the function signature, so every declared parameter is
    # expected at invocation -- an argument vanishing from the namespace can never be valid here.
    # Reject it outright, mirroring the per-argument ``default=argparse.SUPPRESS`` rejection.
    if parser_kwargs.get("argument_default") is argparse.SUPPRESS:
        raise TypeError(
            f"argument_default=argparse.SUPPRESS is not supported by @with_annotated for {func.__qualname__}: "
            f"it drops absent arguments from the parsed namespace, but every parameter built from the "
            f"signature is expected at invocation. Drop argument_default=argparse.SUPPRESS."
        )

    # Build the group lookup (member references already validated by _resolve_parameters).
    target_for, argument_group_for = _build_argument_group_targets(parser, groups=groups)
    _apply_mutex_group_targets(
        parser,
        target_for=target_for,
        argument_group_for=argument_group_for,
        mutually_exclusive_groups=mutually_exclusive_groups,
    )

    # Add each argument to its target (its group/mutex group if assigned, else the parser).
    for arg in resolved:
        arg.add_to(target_for.get(arg.name, parser))

    return parser

with_annotated

with_annotated(
    func: Callable[..., Any],
) -> Callable[..., Any]
with_annotated(
    func: None = ...,
    *,
    ns_provider: Callable[..., Namespace] | None = ...,
    preserve_quotes: bool = ...,
    with_unknown_args: bool = ...,
    base_command: bool = ...,
    subcommand_to: str | None = ...,
    help: str | None = ...,
    aliases: Sequence[str] = ...,
    deprecated: bool = ...,
    groups: tuple[Group, ...] | None = ...,
    mutually_exclusive_groups: tuple[Group, ...]
    | None = ...,
    parser_class: type[Cmd2ArgumentParser] | None = ...,
    subcommand_required: bool = ...,
    subcommand_metavar: str = ...,
    subcommand_title: str | None = ...,
    subcommand_description: str | None = ...,
    **parser_kwargs: Unpack[Cmd2ParserKwargs],
) -> Callable[[Callable[..., Any]], Callable[..., Any]]
with_annotated(
    func=None,
    *,
    ns_provider=None,
    preserve_quotes=False,
    with_unknown_args=False,
    base_command=False,
    subcommand_to=None,
    help=None,
    aliases=(),
    deprecated=False,
    groups=None,
    mutually_exclusive_groups=None,
    parser_class=None,
    subcommand_required=True,
    subcommand_metavar="SUBCOMMAND",
    subcommand_title=None,
    subcommand_description=None,
    **parser_kwargs,
)

Decorate a do_* method to build its argparse parser from type annotations.

PARAMETER DESCRIPTION
func

the command function (when used without parentheses)

TYPE: Callable[..., Any] | None DEFAULT: None

ns_provider

callable returning a prepopulated Namespace (not with subcommand_to)

TYPE: Callable[..., Namespace] | None DEFAULT: None

preserve_quotes

preserve quotes in arguments (not with subcommand_to)

TYPE: bool DEFAULT: False

with_unknown_args

capture unknown args as the _unknown kwarg (not with subcommand_to)

TYPE: bool DEFAULT: False

base_command

add add_subparsers(); requires a cmd2_subcommand_func param and no positionals

TYPE: bool DEFAULT: False

subcommand_to

parent command name; function must be named {parent_underscored}_{subcommand}

TYPE: str | None DEFAULT: None

help

subcommand help text (only with subcommand_to)

TYPE: str | None DEFAULT: None

aliases

alternative subcommand names (only with subcommand_to)

TYPE: Sequence[str] DEFAULT: ()

deprecated

mark the subcommand deprecated in --help (only with subcommand_to)

TYPE: bool DEFAULT: False

groups

Group instances assigning parameters to titled argument groups

TYPE: tuple[Group, ...] | None DEFAULT: None

mutually_exclusive_groups

Group instances of mutually exclusive parameters

TYPE: tuple[Group, ...] | None DEFAULT: None

parser_class

custom parser class (defaults to the configured default)

TYPE: type[Cmd2ArgumentParser] | None DEFAULT: None

subcommand_required

whether a subcommand must be supplied (base_command only)

TYPE: bool DEFAULT: True

subcommand_metavar

metavar for the subcommands group (base_command only)

TYPE: str DEFAULT: 'SUBCOMMAND'

subcommand_title

title for the subcommands --help section (base_command only)

TYPE: str | None DEFAULT: None

subcommand_description

description for that section (base_command only)

TYPE: str | None DEFAULT: None

parser_kwargs

any Cmd2ArgumentParser ctor kwarg (see Cmd2ParserKwargs). description defaults to the docstring's first paragraph when omitted; prog is rejected with subcommand_to (cmd2 rewrites it from the parent).

TYPE: Unpack[Cmd2ParserKwargs] DEFAULT: {}

Source code in cmd2/annotated.py
def with_annotated(
    func: Callable[..., Any] | None = None,
    *,
    ns_provider: Callable[..., argparse.Namespace] | None = None,
    preserve_quotes: bool = False,
    with_unknown_args: bool = False,
    base_command: bool = False,
    subcommand_to: str | None = None,
    help: str | None = None,  # noqa: A002
    aliases: Sequence[str] = (),
    deprecated: bool = False,
    groups: tuple[Group, ...] | None = None,
    mutually_exclusive_groups: tuple[Group, ...] | None = None,
    parser_class: type[Cmd2ArgumentParser] | None = None,
    subcommand_required: bool = True,
    subcommand_metavar: str = "SUBCOMMAND",
    subcommand_title: str | None = None,
    subcommand_description: str | None = None,
    **parser_kwargs: Unpack[Cmd2ParserKwargs],
) -> Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]:
    """Decorate a ``do_*`` method to build its argparse parser from type annotations.

    :param func: the command function (when used without parentheses)
    :param ns_provider: callable returning a prepopulated Namespace (not with ``subcommand_to``)
    :param preserve_quotes: preserve quotes in arguments (not with ``subcommand_to``)
    :param with_unknown_args: capture unknown args as the ``_unknown`` kwarg (not with ``subcommand_to``)
    :param base_command: add ``add_subparsers()``; requires a ``cmd2_subcommand_func`` param and no positionals
    :param subcommand_to: parent command name; function must be named ``{parent_underscored}_{subcommand}``
    :param help: subcommand help text (only with ``subcommand_to``)
    :param aliases: alternative subcommand names (only with ``subcommand_to``)
    :param deprecated: mark the subcommand deprecated in ``--help`` (only with ``subcommand_to``)
    :param groups: [`Group`][cmd2.annotated.Group] instances assigning parameters to titled argument groups
    :param mutually_exclusive_groups: [`Group`][cmd2.annotated.Group] instances of mutually exclusive parameters
    :param parser_class: custom parser class (defaults to the configured default)
    :param subcommand_required: whether a subcommand must be supplied (``base_command`` only)
    :param subcommand_metavar: metavar for the subcommands group (``base_command`` only)
    :param subcommand_title: title for the subcommands ``--help`` section (``base_command`` only)
    :param subcommand_description: description for that section (``base_command`` only)
    :param parser_kwargs: any [`Cmd2ArgumentParser`][cmd2.argparse_utils.Cmd2ArgumentParser] ctor kwarg
                          (see [`Cmd2ParserKwargs`][cmd2.annotated.Cmd2ParserKwargs]).
                          ``description`` defaults to the docstring's first paragraph when omitted;
                          ``prog`` is rejected with ``subcommand_to`` (cmd2 rewrites it from the parent).
    """
    if aliases is None:
        raise TypeError(
            "aliases must be a sequence of subcommand-name strings (e.g. ('co', 'ci')), not None; "
            "omit it or pass an empty tuple for no aliases."
        )
    if (help is not None or aliases or deprecated) and subcommand_to is None:
        raise TypeError("'help', 'aliases', and 'deprecated' are only valid with subcommand_to")
    if subcommand_to is not None:
        unsupported: list[str] = []
        if ns_provider is not None:
            unsupported.append("ns_provider")
        if preserve_quotes:
            unsupported.append("preserve_quotes")
        if with_unknown_args:
            unsupported.append("with_unknown_args")
        if "prog" in parser_kwargs:
            # cmd2's subcommand machinery (``update_prog``) rewrites prog from the parent
            # command hierarchy, so any value supplied here would be silently overwritten.
            unsupported.append("prog")
        if unsupported:
            names = ", ".join(unsupported)
            raise TypeError(
                f"{names} {'is' if len(unsupported) == 1 else 'are'} not supported with subcommand_to. "
                "Configure these behaviors on the base command instead."
            )

    options = _ParserBuildOptions(
        groups=groups,
        mutually_exclusive_groups=mutually_exclusive_groups,
        parser_class=parser_class,
        parser_kwargs=dict(parser_kwargs),
        subcommand_required=subcommand_required,
        subcommand_metavar=subcommand_metavar,
        subcommand_title=subcommand_title,
        subcommand_description=subcommand_description,
    )

    def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
        if with_unknown_args:
            unknown_param = inspect.signature(fn).parameters.get("_unknown")
            if unknown_param is None:
                raise TypeError("with_annotated(with_unknown_args=True) requires a parameter named _unknown")
            if unknown_param.kind is inspect.Parameter.POSITIONAL_ONLY:
                raise TypeError("Parameter _unknown must be keyword-compatible when with_unknown_args=True")

        if not base_command and constants.NS_ATTR_SUBCOMMAND_FUNC in inspect.signature(fn).parameters:
            raise TypeError(
                f"Parameter '{constants.NS_ATTR_SUBCOMMAND_FUNC}' in {fn.__qualname__} "
                "is only valid when with_annotated(base_command=True) is used."
            )

        if subcommand_to is not None:
            handler, subcmd_name, subcmd_parser_builder = _build_subcommand_handler(
                fn,
                subcommand_to,
                base_command=base_command,
                options=options,
            )
            subcommand_spec = SubcommandSpec(
                name=subcmd_name,
                command=subcommand_to,
                help=help,
                aliases=tuple(aliases),
                deprecated=deprecated,
                parser_source=subcmd_parser_builder,
            )
            setattr(handler, constants.SUBCOMMAND_ATTR_SPEC, subcommand_spec)
            return handler

        command_name = fn.__name__[len(constants.COMMAND_FUNC_PREFIX) :]

        skip_params = _SKIP_PARAMS | ({"_unknown"} if with_unknown_args else frozenset())
        if base_command:
            # Validate eagerly (decoration time); the base-command rows in _CONSTRAINTS fire here.
            _resolve_parameters(fn, skip_params=skip_params, base_command=True)

        # Cache signature introspection at decoration time, not per-invocation
        accepted = set(list(inspect.signature(fn).parameters.keys())[1:])
        leading_names, var_positional_name = _var_positional_call_plan(fn)

        parser_builder = _make_parser_builder(fn, skip_params=skip_params, base_command=base_command, options=options)

        @functools.wraps(fn)
        def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
            cmd2_app, statement_arg = _parse_positionals(args)
            owner = args[0]  # Cmd or CommandSet instance
            statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(
                command_name, statement_arg, preserve_quotes
            )

            arg_parser = cmd2_app.command_parsers.get(cmd_wrapper)
            if arg_parser is None:
                raise ValueError(f"No argument parser found for {command_name}")

            if ns_provider is None:
                namespace = None
            else:
                provider_self = cmd2_app._resolve_func_self(ns_provider, args[0])
                namespace = ns_provider(provider_self if provider_self is not None else cmd2_app)

            try:
                if with_unknown_args:
                    ns, unknown = arg_parser.parse_known_args(parsed_arglist, namespace)
                else:
                    ns = arg_parser.parse_args(parsed_arglist, namespace)
                    unknown = None
            except SystemExit as exc:
                raise Cmd2ArgparseError from exc

            setattr(ns, constants.NS_ATTR_STATEMENT, statement)
            handler = getattr(ns, constants.NS_ATTR_SUBCOMMAND_FUNC, None)
            if base_command and handler is not None:
                handler = functools.partial(handler, ns)
            setattr(ns, constants.NS_ATTR_SUBCOMMAND_FUNC, handler)

            func_kwargs = _filtered_namespace_kwargs(ns, accepted=accepted, exclude_subcommand=base_command)

            if with_unknown_args:
                func_kwargs["_unknown"] = unknown

            func_kwargs.update(kwargs)
            result: bool | None = _invoke_command_func(
                fn, owner, func_kwargs, leading_names=leading_names, var_positional_name=var_positional_name
            )
            return result

        argparse_command_spec = ArgparseCommandSpec(
            parser_source=parser_builder,
            preserve_quotes=preserve_quotes,
        )
        setattr(cmd_wrapper, constants.ARGPARSE_COMMAND_ATTR_SPEC, argparse_command_spec)

        return cmd_wrapper

    # Support both @with_annotated and @with_annotated(...)
    if func is not None:
        return decorator(func)
    return decorator