diff --git a/cmd2/annotated.py b/cmd2/annotated.py index f4d4cc075..268a71f09 100644 --- a/cmd2/annotated.py +++ b/cmd2/annotated.py @@ -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 @@ -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 @@ -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 @@ -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. @@ -106,7 +106,7 @@ 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 @@ -114,12 +114,12 @@ def do_paint( 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 @@ -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`` @@ -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: @@ -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) @@ -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 @@ -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). """ diff --git a/docs/api/annotated.md b/docs/api/annotated.md new file mode 100644 index 000000000..7e15f7382 --- /dev/null +++ b/docs/api/annotated.md @@ -0,0 +1,3 @@ +# cmd2.annotated + +::: cmd2.annotated diff --git a/docs/features/annotated.md b/docs/features/annotated.md index 04c7e8472..1b04286d1 100644 --- a/docs/features/annotated.md +++ b/docs/features/annotated.md @@ -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). @@ -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: @@ -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: @@ -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 @@ -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) @@ -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): diff --git a/docs/features/index.md b/docs/features/index.md index 619626ef3..ff6122b60 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -3,6 +3,7 @@