Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions cmd2/annotated.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""Build argparse parsers from type-annotated function signatures.

.. warning:: Experimental
!!! warning "Experimental"

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

The :func:`with_annotated` decorator inspects a command function's type hints and
default values to build a ``Cmd2ArgumentParser``. :class:`Argument` and
:class:`Option` metadata classes give finer per-parameter control via
The [`with_annotated`][cmd2.annotated.with_annotated] decorator inspects a command function's type hints and
default values to build a ``Cmd2ArgumentParser``. [`Argument`][cmd2.annotated.Argument] and
[`Option`][cmd2.annotated.Option] metadata classes give finer per-parameter control via
``typing.Annotated``.

Parameters without defaults become positional arguments; parameters with defaults
Expand All @@ -18,7 +18,7 @@
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_handler`` receives the subcommand handler::
``Statement`` and (with ``base_command=True``) ``cmd2_handler`` receives the subcommand handler:

class MyApp(cmd2.Cmd):
@cmd2.with_annotated
Expand All @@ -27,8 +27,8 @@ def do_greet(self, name: str, count: int = 1, loud: bool = False):
msg = f"Hello {name}"
self.poutput(msg.upper() if loud else msg)

Use ``Annotated`` with :class:`Argument` or :class:`Option` for finer
control over individual parameters::
Use ``Annotated`` with [`Argument`][cmd2.annotated.Argument] or [`Option`][cmd2.annotated.Option] for finer
control over individual parameters:

from typing import Annotated

Expand Down Expand Up @@ -84,7 +84,7 @@ def do_paint(
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 :class:`argparse.Action` subclass -- passed straight through to ``add_argument``.
- 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.
Expand All @@ -106,20 +106,20 @@ def do_paint(
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
:func:`~cmd2.argparse_utils.register_argparse_argument_parameter`, passes through unchanged.
[`register_argparse_argument_parameter`][cmd2.argparse_utils.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 :class:`~cmd2.Cmd2ArgumentParser`'s constructor via PEP
Parser-level customization is forwarded to [`Cmd2ArgumentParser`][cmd2.argparse_utils.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``, ``ap_completer_type``, and on Python >= 3.14 ``suggest_on_error`` / ``color`` --
flows straight through; the :class:`Cmd2ParserKwargs` ``TypedDict`` is the single source of truth
flows straight through; the [`Cmd2ParserKwargs`][cmd2.annotated.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
Expand Down Expand Up @@ -218,7 +218,7 @@ def do_paint(


class Cmd2ParserKwargs(TypedDict, total=False):
"""Forwarded ctor kwargs for :class:`~cmd2.Cmd2ArgumentParser` (PEP 692 ``Unpack``).
"""Forwarded ctor kwargs for [`Cmd2ArgumentParser`][cmd2.argparse_utils.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``
Expand Down Expand Up @@ -302,7 +302,7 @@ def __init__(
``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
:func:`~cmd2.argparse_utils.register_argparse_argument_parameter`) straight through.
[`register_argparse_argument_parameter`][cmd2.argparse_utils.register_argparse_argument_parameter]) straight through.
"""
reserved = self._RESERVED_EXTRA_KWARGS & extra_kwargs.keys()
if reserved:
Expand Down Expand Up @@ -362,7 +362,7 @@ def __init__(

``action`` is a supported string action (``store_true``/``store_false``/``count``/
``append``/``extend``/``store_const``/``append_const``) or a custom
:class:`argparse.Action` subclass (passed through; it owns storage, so the inferred
`argparse.Action` subclass (passed through; it owns storage, so the inferred
action and the action-specific constraints are skipped).
"""
super().__init__(**kwargs)
Expand Down Expand Up @@ -1940,16 +1940,16 @@ def build_parser_from_function(
) -> Cmd2ArgumentParser:
"""Inspect a function's signature and build a ``Cmd2ArgumentParser``.

The lower-level entry point behind :func:`with_annotated`. ``parser_kwargs`` is forwarded to
the parser ctor (see :class:`Cmd2ParserKwargs`); when ``description`` is omitted, the first
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: :class:`Group` instances assigning parameters to argument groups
:param mutually_exclusive_groups: :class:`Group` instances of mutually exclusive parameters
: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 :class:`Cmd2ParserKwargs`
:param parser_kwargs: forwarded [`Cmd2ParserKwargs`][cmd2.annotated.Cmd2ParserKwargs]
:return: a fully configured ``Cmd2ArgumentParser``
"""
parser_cls = parser_class or DEFAULT_ARGUMENT_PARSER
Expand Down Expand Up @@ -2175,14 +2175,15 @@ def with_annotated(
: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: :class:`Group` instances assigning parameters to titled argument groups
:param mutually_exclusive_groups: :class:`Group` instances of mutually exclusive parameters
: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 :class:`~cmd2.Cmd2ArgumentParser` ctor kwarg (see :class:`Cmd2ParserKwargs`).
: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).
"""
Expand Down
3 changes: 3 additions & 0 deletions docs/api/annotated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# cmd2.annotated

::: cmd2.annotated
61 changes: 40 additions & 21 deletions docs/features/annotated.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,21 +155,31 @@ Both `Argument` and `Option` accept the same cmd2-specific fields as `add_argume
`Option` additionally accepts `action`, `required`, and positional `*names` for custom flag strings
(e.g. `Option("--color", "-c")`).

### Actions

When an `Option(action=...)` uses a zero-argument argparse action that takes no value from the
command line (`count`, `store_true`, `store_false`, `store_const`, `append_const`),
`@with_annotated` removes the value-oriented metadata inferred from the type before calling
`add_argument()`: the `type` converter, the static `choices`, and any inferred tab-completer (such
as the path completer for `Path`) or `choices_provider`. This matches argparse behavior (which
rejects a completer on a value-less action) and avoids parser-construction errors such as combining
`action='count'` with `type=int`. Actions that do consume values (`append` / `extend` on a
`list[T]`, or a plain value option) keep the inferred converter and completer. `action='help'` and
`action='version'` are not supported.
`@with_annotated` strips the value-oriented metadata it inferred from the type before calling
`add_argument()`:

- the `type` converter,
- the static `choices`, and
- any inferred tab-completer (such as the path completer for `Path`) or `choices_provider`.

This matches argparse behavior (which rejects a completer on a value-less action) and avoids
parser-construction errors such as combining `action='count'` with `type=int`. Actions that _do_
consume values (`append` / `extend` on a `list[T]`, or a plain value option) keep the inferred
converter and completer.

Pairing `const` with an explicit `nargs` on a scalar `Option` selects argparse's optional-value
idiom instead of `store_const`. `Annotated[str | None, Option("--log", nargs='?', const="CONSOLE")]`
keeps the `store` action and the inferred `type` converter, so the flag is three-way: absent yields
the default, a bare `--log` yields the `const`, and `--log VALUE` yields the converted `VALUE`. The
`const` is stored verbatim (it is not run through the converter), so it must already match the
keeps the `store` action and the inferred `type` converter, so the flag is three-way:

- absent yields the default,
- a bare `--log` yields the `const`, and
- `--log VALUE` yields the converted `VALUE`.

The `const` is stored verbatim (it is not run through the converter), so it must already match the
declared type. Without an explicit `nargs`, `const` alone still infers the value-less `store_const`
(present yields the `const`, and supplying a value is an error).

Expand All @@ -192,6 +202,8 @@ def do_shout(self, name: Annotated[str, Option("--name", action=UpperAction)] =
`action='help'` and `action='version'` are not supported by `@with_annotated`; use `@with_argparser`
if you need them.

### Reserved keyword arguments

`Argument()` and `Option()` refuse a handful of `add_argument()` kwargs that the decorator derives
from the function signature itself, so misusing them surfaces as a clear `TypeError` instead of a
silent override. The refused kwargs are:
Expand All @@ -204,6 +216,8 @@ silent override. The refused kwargs are:
Every other `add_argument()` parameter passes through, including any custom parameter registered via
[register_argparse_argument_parameter][cmd2.argparse_utils.register_argparse_argument_parameter].

### Defaults

A `default` may be supplied either through the function signature or as a metadata kwarg. The two
forms are equivalent:

Expand All @@ -223,6 +237,8 @@ Parser-construction kwargs such as `add_help`, `prefix_chars`, `fromfile_prefix_
`argument_default`, `conflict_handler`, and `allow_abbrev` are not exposed by `@with_annotated`. Set
them on a custom `parser_class` subclass and pass it via `parser_class=`.

### Choices and enums

When a user-supplied `choices_provider` or `completer` is given for an inferred `Enum` or `Literal`,
the inferred static `choices` list is dropped so completion is driven by the provider or completer.
The inferred `type` converter is preserved, so parsed values still coerce to the declared type
Expand Down Expand Up @@ -268,17 +284,10 @@ list the values.
- `groups` -- `Group` instances assigning parameter names to argument groups
- `mutually_exclusive_groups` -- `Group` instances of mutually exclusive parameters
- `parser_class` -- a custom parser class (defaults to the configured default)
- `**parser_kwargs` -- every other parser-construction kwarg accepted by `Cmd2ArgumentParser` is
forwarded through PEP 692
[`Unpack`][typing.Unpack][`[Cmd2ParserKwargs]`][cmd2.annotated.Cmd2ParserKwargs]: `description`,
`epilog`, `prog`, `usage`, `parents`, `argument_default`, `prefix_chars`, `fromfile_prefix_chars`,
`conflict_handler`, `add_help`, `allow_abbrev`, `exit_on_error`, `formatter_class`,
`ap_completer_type`, and on Python ≥ 3.14 `suggest_on_error` / `color`. Two behaviors layer on
top of the raw passthrough:
- `description` -- when omitted, the first paragraph of the function's docstring (up to the
first blank line) is used; pass an explicit value to override.
- `prog` -- rejected when `subcommand_to` is set; cmd2's subcommand machinery rewrites `prog`
from the parent command hierarchy and any value here would be silently overwritten.
- `**parser_kwargs` -- every other `Cmd2ArgumentParser` constructor kwarg, forwarded through PEP 692
[`Unpack[Cmd2ParserKwargs]`][cmd2.annotated.Cmd2ParserKwargs]. See
[Parser customization](#parser-customization) below for the full list and the `description` /
`prog` special cases.

```py
@with_annotated(with_unknown_args=True)
Expand All @@ -296,6 +305,16 @@ the forwarded kwargs and gives type-checkers/IDEs autocomplete on the decorator'
a new ctor kwarg to `Cmd2ArgumentParser` only needs a matching field on `Cmd2ParserKwargs`, and the
annotated decorator picks it up automatically.

The forwarded kwargs are `description`, `epilog`, `prog`, `usage`, `parents`, `argument_default`,
`prefix_chars`, `fromfile_prefix_chars`, `conflict_handler`, `add_help`, `allow_abbrev`,
`exit_on_error`, `formatter_class`, `ap_completer_type`, and on Python ≥ 3.14 `suggest_on_error` /
`color`. Two of them layer extra behavior on top of the raw passthrough:

- `description` -- when omitted, it is filled from the function's docstring (detailed below); pass
an explicit value to override.
- `prog` -- rejected when `subcommand_to` is set; cmd2's subcommand machinery rewrites `prog` from
the parent command hierarchy, so any value here would be silently overwritten.

`parser_class` stays as its own explicit kwarg because it selects the class itself rather than a
value passed to it. Argument groups are declared with [Group][cmd2.annotated.Group]; pass `title`
and `description` for a titled help section (omit them for an untitled group):
Expand Down
1 change: 1 addition & 0 deletions docs/features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<div class="grid cards" markdown>
<!--intro-start-->
- [Argument Processing](argument_processing.md)
- [Annotated Argument Processing](annotated.md)
- [Async Commands](async_commands.md)
- [Builtin Commands](builtin_commands.md)
- [Clipboard Integration](clipboard.md)
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ nav:
- API Reference:
- api/index.md
- api/cmd.md
- api/annotated.md
- api/argparse_completer.md
- api/argparse_utils.md
- api/clipboard.md
Expand Down
Loading