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 argumentint,float-- setstype=booloption ----flag / --no-flagviaBooleanOptionalAction; defaults toFalse(orNoneforbool | None) when omitted, so it is neverrequired- positional
bool-- parsed fromtrue/false,yes/no,on/off,1/0 pathlib.Path-- setstype=Pathenum.Enumsubclass --type=converter,choicesfrom member values- a union of Enums (e.g.
EnumA | EnumB) -- each member keeps its own converter; a token resolves to the first member that accepts it, and the mergedchoicesare the concatenation of each member's choices decimal.Decimal-- setstype=DecimalLiteral[...]--type=converterandchoicesfrom the literal valueslist[T]/set[T]/frozenset[T]/tuple[T, ...]--nargs='+'(or'*'with a default or| None)tuple[T, T](fixed arity, same type) --nargs=Nwithtype=T*args: T-- variadic positional (nargs='*');Tis each value's type, not the collected tuple.Annotated[T, Argument(...)]metadata is honoredT | None(no default) -- positional withnargs='?'(0-or-1 tokens)T | None = None----flagoption withdefault=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 aboolparameter (type=is dropped; argparse supplies theFalse/Truedefault)count-- requires anintparameter; defaults to0(Noneforint | None)append/extend-- require alist[T]parameter and default to[](appendtakes one value per flag;extendtakesnargsvalues per flag)store_const/append_const-- store theOption(const=...)value (type=is dropped). The action is inferred from the type whenaction=is omitted: a scalarOption(const=X)becomesstore_const(present ->const, absent -> the default, which must exist or beT | None); alist[T]Option(const=X)becomesappend_const(each flag appendsconst; defaults to[]). A scalarOption(const=X)given an explicitnargs(e.g.nargs='?') instead keeps thestoreaction for argparse's optional-value idiom (absent -> default, bare flag ->const,flag VALUE-> convertedVALUE); theconstis stored verbatim and must match the declared type.constis validated against the declared type and is rejected on a positionalArgument(argparse ignores it there)- a custom
argparse.Actionsubclass -- passed straight through toadd_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, andrequiredare still applied; the action receives them like any hand-builtadd_argumentcall.action='help'andaction='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). Give a
mutually_exclusive_groups entry a title/description to render it as a titled help section
(argparse's one supported nesting -- a mutex inside an argument group), and use
Option(action='store_true') for any bool member so the mutex reads as [--foo | --bar]
instead of expanding to --no-* variants. To put non-mutex parameters in the same section, list
its members in a groups= entry instead and leave the title off the mutex; declaring the section in
both places, a mutex that sits only partly in a groups= entry, or one that spans two of them all
raise ValueError. The other three nesting directions (an argument group in an argument group or
in a mutex, and a mutex in a mutex) are removed in argparse on Python 3.14 and cannot be expressed
here. These group-spec rules (and member references, double-assignment, and the required=True
rejection) are checked at decoration time from parameter names alone -- type hints are not resolved,
so forward-referenced annotations still decorate -- meaning a misconfigured group raises when the
class is defined rather than on first command use. The one group rule that needs the annotations
(a required member in a mutually exclusive group) fires when the parser is built.
Unsupported patterns (raise TypeError):
- a non-Optional type with a
Nonedefault (e.g.name: str = None); annotate itT | Noneor 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 arestr,int,float,bool,decimal.Decimal,pathlib.Path,enum.Enumsubclasses, andLiteral[...](str/Any/objectpass through raw) str | int-- a union of multiple non-None types is ambiguous (unless every member is anenum.Enumsubclass, which resolves by trying each member's converter in turn)tuple[int, str, float]-- mixed element types (argparse applies onetype=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(...)]--*argsis always positional; useArgument()*args: Annotated[T, Argument(nargs=N)]--*argsarity is fixed tonargs='*'- a keyword-only parameter annotated with
Argument()-- it marks a positional; useOption() - a required option (no default, not
T | None) in amutually_exclusive_groupsgroup -- only one member is supplied, so the others arrive asNone; give it a default orT | None Annotated[T, Argument(nargs=N)]producing a list ('*','+', integer>= 1) on a non-collectionT; uselist[T]ortuple[T, ...]to match the runtime shapeAnnotated[tuple[T, T], Argument(nargs=N)]whereNdiffers from the tuple's arityOption(action=...)whose result type mismatches the declared type, an unsupported action, or a non-list action on a collection (useappend/extend/append_constwithlist[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=.
Two hooks customize the string -> value conversion, parity with a hand-built add_argument(type=...)
(a raw type= in the metadata is rejected; use these instead):
converter-- aCallable[[str], Any]that replaces the inferredtype=converter. Because the converter owns the conversion, the annotation is no longer required to be a supported scalar -- any type is legal (Annotated[datetime, Argument(converter=parse_iso)]), and the "unsupported type" error is suppressed. The inferredchoicesand completer (which described the inferred value-space) are dropped; supplychoices=/completer/choices_providerto re-add them (an explicitchoices=is still run through your converter). argparse applies it per token, so on alist[T]it converts each value; a non-collection annotation such asAnykeeps a single token, so the converter may itself return a collection (Annotated[Any, Option('--idx', converter=parse_intset)]).preprocess-- aCallable[[str], str]that runs before the inferred converter, transforming the raw token while keeping the inferredtype=,choices, completer, and coercion. Use it to normalize input for a type that already has rich inference, e.g.Annotated[Color, Argument(preprocess= str.lower)]acceptsREDwhile still showing theColorchoices, orAnnotated[Path, Argument(preprocess=os.path.expanduser)]keeps the path completer. With a plainstr(no inferred converter) it becomes thetype=directly.
converter and preprocess are mutually exclusive on one parameter (fold the preprocessing into the
converter, which already receives the raw token), and neither may be combined with a value-less action
(store_true / store_false / count / store_const / append_const), which consumes no
token to convert.
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.
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,
allow_unknown_entry=False,
converter=None,
preprocess=None,
**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. allow_unknown_entry only affects Enum
annotations: when set, a token matched by neither a member value nor name is routed through
the enum's _missing_ hook (for aliases / special keywords) instead of being rejected
outright. converter replaces the inferred type= converter (and makes any annotation
type legal); preprocess runs before the inferred converter to transform the raw token while
keeping the inferred choices/completer. The two are mutually exclusive and neither combines with
a value-less action (see the module docstring). extra_kwargs forwards any other
add_argument parameter (incl. those from
register_argparse_argument_parameter) straight through.
Source code in cmd2/annotated.py
to_kwargs
Return non-None mapped fields, an explicit const, and any passthrough extra_kwargs.
Source code in cmd2/annotated.py
Option
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
to_kwargs
Return non-None fields as an argparse kwargs dict.
Source code in cmd2/annotated.py
Group
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:
|
title
|
group title shown as a help section header
TYPE:
|
description
|
group description shown under the title
TYPE:
|
required
|
TYPE:
|
Source code in cmd2/annotated.py
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:
|
skip_params
|
parameter names to exclude from the parser
TYPE:
|
groups
|
TYPE:
|
mutually_exclusive_groups
|
TYPE:
|
parser_class
|
custom parser class (defaults to the configured default)
TYPE:
|
parser_kwargs
|
forwarded
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Cmd2ArgumentParser
|
a fully configured |
Source code in cmd2/annotated.py
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 | |
with_annotated
with_annotated(
func: Callable[_CommandParams, _CommandReturn],
) -> Callable[_CommandParams, _CommandReturn]
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],
) -> _WithAnnotatedDecorator
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:
|
ns_provider
|
callable returning a prepopulated Namespace (not with
TYPE:
|
preserve_quotes
|
preserve quotes in arguments (not with
TYPE:
|
with_unknown_args
|
capture unknown args as the
TYPE:
|
base_command
|
add
TYPE:
|
subcommand_to
|
parent command name; function must be named
TYPE:
|
help
|
subcommand help text (only with
TYPE:
|
aliases
|
alternative subcommand names (only with
TYPE:
|
deprecated
|
mark the subcommand deprecated in
TYPE:
|
groups
|
TYPE:
|
mutually_exclusive_groups
|
TYPE:
|
parser_class
|
custom parser class (defaults to the configured default)
TYPE:
|
subcommand_required
|
whether a subcommand must be supplied (
TYPE:
|
subcommand_metavar
|
metavar for the subcommands group (
TYPE:
|
subcommand_title
|
title for the subcommands
TYPE:
|
subcommand_description
|
description for that section (
TYPE:
|
parser_kwargs
|
any
TYPE:
|
Source code in cmd2/annotated.py
2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 | |