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`

    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348

    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`

    350
    351
    352
    353
    354
    355
    356

    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`

    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387

    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`

    389
    390
    391
    392
    393
    394
    395
    396

    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`

    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422

    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`

    1958
    1959
    1960
    1961
    1962
    1963
    1964
    1965
    1966
    1967
    1968
    1969
    1970
    1971
    1972
    1973
    1974
    1975
    1976
    1977
    1978
    1979
    1980
    1981
    1982
    1983
    1984
    1985
    1986
    1987
    1988
    1989
    1990
    1991
    1992
    1993
    1994
    1995
    1996
    1997
    1998
    1999
    2000
    2001
    2002
    2003
    2004
    2005
    2006
    2007
    2008
    2009
    2010
    2011
    2012
    2013
    2014
    2015
    2016
    2017
    2018
    2019
    2020
    2021
    2022
    2023
    2024

    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`

    2166
    2167
    2168
    2169
    2170
    2171
    2172
    2173
    2174
    2175
    2176
    2177
    2178
    2179
    2180
    2181
    2182
    2183
    2184
    2185
    2186
    2187
    2188
    2189
    2190
    2191
    2192
    2193
    2194
    2195
    2196
    2197
    2198
    2199
    2200
    2201
    2202
    2203
    2204
    2205
    2206
    2207
    2208
    2209
    2210
    2211
    2212
    2213
    2214
    2215
    2216
    2217
    2218
    2219
    2220
    2221
    2222
    2223
    2224
    2225
    2226
    2227
    2228
    2229
    2230
    2231
    2232
    2233
    2234
    2235
    2236
    2237
    2238
    2239
    2240
    2241
    2242
    2243
    2244
    2245
    2246
    2247
    2248
    2249
    2250
    2251
    2252
    2253
    2254
    2255
    2256
    2257
    2258
    2259
    2260
    2261
    2262
    2263
    2264
    2265
    2266
    2267
    2268
    2269
    2270
    2271
    2272
    2273
    2274
    2275
    2276
    2277
    2278
    2279
    2280
    2281
    2282
    2283
    2284
    2285
    2286
    2287
    2288
    2289
    2290
    2291
    2292
    2293
    2294
    2295
    2296
    2297
    2298
    2299
    2300
    2301
    2302
    2303
    2304
    2305
    2306
    2307
    2308
    2309
    2310
    2311
    2312
    2313
    2314
    2315
    2316
    2317
    2318
    2319
    2320
    2321
    2322
    2323
    2324
    2325
    2326
    2327
    2328
    2329
    2330
    2331
    2332
    2333
    2334
    2335
    2336
    2337
    2338
    2339
    2340
    2341
    2342
    2343
    2344
    2345
    2346

    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