1818flag (``dry_run`` -> ``--dry-run``); pass an explicit ``Option("--my_flag")`` to opt out.
1919Positional-only parameters (before ``/``) and ``**kwargs`` raise ``TypeError``. The parameter
2020names ``dest`` and ``subcommand`` are reserved; ``cmd2_statement`` receives the parsed
21- ``Statement`` and (with ``base_command=True``) ``cmd2_handler `` receives the subcommand handler:
21+ ``Statement`` and (with ``base_command=True``) ``cmd2_subcommand_func `` receives the subcommand handler:
2222
2323 class MyApp(cmd2.Cmd):
2424 @cmd2.with_annotated
@@ -118,7 +118,7 @@ def do_paint(
118118692 ``**parser_kwargs: Unpack[Cmd2ParserKwargs]``. Anything the parser ctor accepts -- ``description``,
119119``epilog``, ``prog``, ``usage``, ``parents``, ``argument_default``, ``prefix_chars``,
120120``fromfile_prefix_chars``, ``conflict_handler``, ``add_help``, ``allow_abbrev``, ``exit_on_error``,
121- ``formatter_class``, ``ap_completer_type ``, and on Python >= 3.14 ``suggest_on_error`` / ``color`` --
121+ ``formatter_class``, ``completer_class ``, and on Python >= 3.14 ``suggest_on_error`` / ``color`` --
122122flows straight through; the [`Cmd2ParserKwargs`][cmd2.annotated.Cmd2ParserKwargs] ``TypedDict`` is the single source of truth
123123and gives type-checkers/IDEs autocomplete on the decorator's call site. ``parser_class`` stays as
124124its own explicit kwarg because it selects the class itself, not a value passed to it. Two
@@ -181,8 +181,16 @@ def do_paint(
181181import functools
182182import inspect
183183import types
184- from collections .abc import Callable , Container , Iterable , Sequence
185- from dataclasses import dataclass , field
184+ from collections .abc import (
185+ Callable ,
186+ Container ,
187+ Iterable ,
188+ Sequence ,
189+ )
190+ from dataclasses import (
191+ dataclass ,
192+ field ,
193+ )
186194from pathlib import Path
187195from typing import (
188196 TYPE_CHECKING ,
@@ -205,14 +213,22 @@ def do_paint(
205213from rich .table import Column
206214
207215from . import constants
208- from .argparse_utils import DEFAULT_ARGUMENT_PARSER , Cmd2ArgumentParser , SubcommandSpec
216+ from .argparse_utils import (
217+ ArgparseCommandSpec ,
218+ Cmd2ArgumentParser ,
219+ SubcommandSpec ,
220+ )
209221from .completion import CompletionItem
210222from .decorators import _parse_positionals
211223from .exceptions import Cmd2ArgparseError
212224from .rich_utils import Cmd2HelpFormatter , HelpContent
213- from .types import CmdOrSetT , UnboundChoicesProvider , UnboundCompleter
225+ from .types import (
226+ CmdOrSetT ,
227+ UnboundChoicesProvider ,
228+ UnboundCompleter ,
229+ )
214230
215- if TYPE_CHECKING :
231+ if TYPE_CHECKING : # pragma: no cover
216232 from .argparse_completer import ArgparseCompleter
217233
218234#: ``nargs`` values accepted by cmd2's patched ``add_argument`` (incl. ranged tuples).
@@ -242,7 +258,7 @@ class Cmd2ParserKwargs(TypedDict, total=False):
242258 exit_on_error : bool
243259 suggest_on_error : bool
244260 color : bool
245- ap_completer_type : "type[ArgparseCompleter] | None"
261+ completer_class : "type[ArgparseCompleter] | None"
246262
247263
248264# ---------------------------------------------------------------------------
@@ -1689,7 +1705,7 @@ def _const_mismatches_type(a: _ArgparseArgument) -> bool:
16891705
16901706# Parameters handled specially by the decorator and not added to the parser. The first positional
16911707# parameter (self/cls) is always skipped by position; these cover additional decorator-managed names.
1692- _SKIP_PARAMS = frozenset ({"cmd2_handler" , "cmd2_statement" })
1708+ _SKIP_PARAMS = frozenset ({constants . NS_ATTR_SUBCOMMAND_FUNC , constants . NS_ATTR_STATEMENT })
16931709
16941710
16951711def _link_group_membership (
@@ -1720,16 +1736,30 @@ def _resolve_parameters(
17201736 """Resolve a function signature into a list of argparse-argument builders.
17211737
17221738 ``base_command`` marks each argument's context for the base-command :data:`_CONSTRAINTS` rows and
1723- drives the function-level ``cmd2_handler `` check below. ``groups``/``mutually_exclusive_groups``
1739+ drives the function-level ``cmd2_subcommand_func `` check below. ``groups``/``mutually_exclusive_groups``
17241740 are linked onto each argument as membership facts for the cross-config constraint rows.
17251741 """
17261742 sig = inspect .signature (func )
17271743 # Function-level check (not a per-argument _CONSTRAINTS row): a base command dispatches through
1728- # cmd2_handler, so it must exist. Here so it also fires when the function has zero parameters.
1729- if base_command and "cmd2_handler" not in sig .parameters :
1730- raise TypeError (f"with_annotated(base_command=True) requires a 'cmd2_handler' parameter in { func .__qualname__ } " )
1744+ # cmd2_subcommand_func, so it must exist. Here so it also fires when the function has zero parameters.
1745+ if base_command and constants .NS_ATTR_SUBCOMMAND_FUNC not in sig .parameters :
1746+ raise TypeError (
1747+ f"with_annotated(base_command=True) requires a '{ constants .NS_ATTR_SUBCOMMAND_FUNC } ' "
1748+ f"parameter in { func .__qualname__ } "
1749+ )
1750+ # Resolve hints only for the parameters that become arguments: the bound first parameter
1751+ # (self/cls), the injected skip_params, and the "return" annotation never become arguments
1752+ ignored = {next (iter (sig .parameters ), None ), "return" , * skip_params }
1753+ ignored .discard (None )
1754+ relevant_annotations = {name : ann for name , ann in getattr (func , "__annotations__" , {}).items () if name not in ignored }
1755+ # Forward references resolve against the *original* function's module during functools.wraps wrapper.
1756+ unwrapped = inspect .unwrap (func )
17311757 try :
1732- hints = get_type_hints (func , include_extras = True )
1758+ hints = get_type_hints (
1759+ types .SimpleNamespace (__annotations__ = relevant_annotations ),
1760+ globalns = getattr (unwrapped , "__globals__" , {}),
1761+ include_extras = True ,
1762+ )
17331763 except (NameError , AttributeError , TypeError ) as exc :
17341764 raise TypeError (
17351765 f"Failed to resolve type hints for { func .__qualname__ } . Ensure all annotations use valid, importable types."
@@ -1830,14 +1860,10 @@ def _filtered_namespace_kwargs(
18301860 exclude_subcommand : bool = False ,
18311861) -> dict [str , Any ]:
18321862 """Filter a parsed Namespace down to user-visible kwargs."""
1833- from .constants import NS_ATTR_SUBCMD_HANDLER
1834-
18351863 filtered : dict [str , Any ] = {}
18361864 for key , value in vars (ns ).items ():
18371865 if accepted is not None and key not in accepted :
18381866 continue
1839- if key == NS_ATTR_SUBCMD_HANDLER :
1840- continue
18411867 if exclude_subcommand and key == "subcommand" :
18421868 continue
18431869 filtered [key ] = value
@@ -1954,7 +1980,9 @@ def build_parser_from_function(
19541980 :param parser_kwargs: forwarded [`Cmd2ParserKwargs`][cmd2.annotated.Cmd2ParserKwargs]
19551981 :return: a fully configured ``Cmd2ArgumentParser``
19561982 """
1957- parser_cls = parser_class or DEFAULT_ARGUMENT_PARSER
1983+ from . import argparse_utils
1984+
1985+ parser_cls = parser_class or argparse_utils .DEFAULT_ARGUMENT_PARSER
19581986 if "description" not in parser_kwargs :
19591987 auto_description = _docstring_first_paragraph (func .__doc__ )
19601988 if auto_description is not None :
@@ -1971,25 +1999,16 @@ def build_parser_from_function(
19711999 mutually_exclusive_groups = mutually_exclusive_groups ,
19722000 )
19732001
1974- # ``argument_default=argparse.SUPPRESS`` removes an absent argument from the parsed namespace.
1975- # That is safe only for arguments that are always supplied (required) or carry their own default;
1976- # an *omittable* argument with no default (e.g. a ``T | None`` positional -> nargs='?') would be
1977- # dropped when absent, leaving the function without a keyword argument it expects. ``*args`` is
1978- # exempt: the invocation path substitutes an empty tuple for it. Reject the combination here,
1979- # mirroring the per-argument ``default=argparse.SUPPRESS`` rejection.
2002+ # ``argument_default=argparse.SUPPRESS`` drops an absent argument from the parsed namespace.
2003+ # @with_annotated builds the call from the function signature, so every declared parameter is
2004+ # expected at invocation -- an argument vanishing from the namespace can never be valid here.
2005+ # Reject it outright, mirroring the per-argument ``default=argparse.SUPPRESS`` rejection.
19802006 if parser_kwargs .get ("argument_default" ) is argparse .SUPPRESS :
1981- dropped = [
1982- arg .name
1983- for arg in resolved
1984- if arg .default is _UNSET and arg .omittable and not arg .required and not arg .is_variadic
1985- ]
1986- if dropped :
1987- raise TypeError (
1988- f"argument_default=argparse.SUPPRESS is not supported by @with_annotated for { func .__qualname__ } : "
1989- f"it would drop { dropped !r} from the parsed namespace when absent, but the function expects "
1990- f"{ 'them' if len (dropped ) > 1 else 'it' } as a keyword argument. Give each an explicit default or "
1991- f"make it required, or drop argument_default=argparse.SUPPRESS."
1992- )
2007+ raise TypeError (
2008+ f"argument_default=argparse.SUPPRESS is not supported by @with_annotated for { func .__qualname__ } : "
2009+ f"it drops absent arguments from the parsed namespace, but every parameter built from the "
2010+ f"signature is expected at invocation. Drop argument_default=argparse.SUPPRESS."
2011+ )
19932012
19942013 # Build the group lookup (member references already validated by _resolve_parameters).
19952014 target_for , argument_group_for = _build_argument_group_targets (parser , groups = groups )
@@ -2107,10 +2126,10 @@ def _build_subcommand_handler(
21072126 def handler (self_arg : Any , ns : Any ) -> Any :
21082127 """Unpack Namespace into typed kwargs for the subcommand handler."""
21092128 filtered = _filtered_namespace_kwargs (ns , accepted = _accepted )
2110- if "cmd2_handler" in filtered :
2111- cmd2_h = filtered ["cmd2_handler" ]
2112- if isinstance (cmd2_h , functools .partial ) and cmd2_h .func is handler :
2113- filtered ["cmd2_handler" ] = None
2129+ if constants . NS_ATTR_SUBCOMMAND_FUNC in filtered :
2130+ cmd2_h = filtered [constants . NS_ATTR_SUBCOMMAND_FUNC ]
2131+ if isinstance (cmd2_h , functools .partial ) and getattr ( cmd2_h .func , "__func__" , cmd2_h . func ) is handler :
2132+ filtered [constants . NS_ATTR_SUBCOMMAND_FUNC ] = None
21142133 return _invoke_command_func (
21152134 func , self_arg , filtered , leading_names = _leading_names , var_positional_name = _var_positional_name
21162135 )
@@ -2182,7 +2201,7 @@ def with_annotated(
21822201 :param ns_provider: callable returning a prepopulated Namespace (not with ``subcommand_to``)
21832202 :param preserve_quotes: preserve quotes in arguments (not with ``subcommand_to``)
21842203 :param with_unknown_args: capture unknown args as the ``_unknown`` kwarg (not with ``subcommand_to``)
2185- :param base_command: add ``add_subparsers()``; requires a ``cmd2_handler `` param and no positionals
2204+ :param base_command: add ``add_subparsers()``; requires a ``cmd2_subcommand_func `` param and no positionals
21862205 :param subcommand_to: parent command name; function must be named ``{parent_underscored}_{subcommand}``
21872206 :param help: subcommand help text (only with ``subcommand_to``)
21882207 :param aliases: alternative subcommand names (only with ``subcommand_to``)
@@ -2244,9 +2263,10 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
22442263 if unknown_param .kind is inspect .Parameter .POSITIONAL_ONLY :
22452264 raise TypeError ("Parameter _unknown must be keyword-compatible when with_unknown_args=True" )
22462265
2247- if not base_command and "cmd2_handler" in inspect .signature (fn ).parameters :
2266+ if not base_command and constants . NS_ATTR_SUBCOMMAND_FUNC in inspect .signature (fn ).parameters :
22482267 raise TypeError (
2249- f"Parameter 'cmd2_handler' in { fn .__qualname__ } is only valid when with_annotated(base_command=True) is used."
2268+ f"Parameter '{ constants .NS_ATTR_SUBCOMMAND_FUNC } ' in { fn .__qualname__ } "
2269+ "is only valid when with_annotated(base_command=True) is used."
22502270 )
22512271
22522272 if subcommand_to is not None :
@@ -2256,15 +2276,15 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
22562276 base_command = base_command ,
22572277 options = options ,
22582278 )
2259- spec = SubcommandSpec (
2279+ subcommand_spec = SubcommandSpec (
22602280 name = subcmd_name ,
22612281 command = subcommand_to ,
22622282 help = help ,
22632283 aliases = tuple (aliases ),
22642284 deprecated = deprecated ,
22652285 parser_source = subcmd_parser_builder ,
22662286 )
2267- setattr (handler , constants .SUBCMD_ATTR_SPEC , spec )
2287+ setattr (handler , constants .SUBCOMMAND_ATTR_SPEC , subcommand_spec )
22682288 return handler
22692289
22702290 command_name = fn .__name__ [len (constants .COMMAND_FUNC_PREFIX ) :]
@@ -2308,10 +2328,10 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
23082328 raise Cmd2ArgparseError from exc
23092329
23102330 setattr (ns , constants .NS_ATTR_STATEMENT , statement )
2311- handler = getattr (ns , constants .NS_ATTR_SUBCMD_HANDLER , None )
2331+ handler = getattr (ns , constants .NS_ATTR_SUBCOMMAND_FUNC , None )
23122332 if base_command and handler is not None :
23132333 handler = functools .partial (handler , ns )
2314- ns . cmd2_handler = handler
2334+ setattr ( ns , constants . NS_ATTR_SUBCOMMAND_FUNC , handler )
23152335
23162336 func_kwargs = _filtered_namespace_kwargs (ns , accepted = accepted , exclude_subcommand = base_command )
23172337
@@ -2324,8 +2344,11 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
23242344 )
23252345 return result
23262346
2327- setattr (cmd_wrapper , constants .CMD_ATTR_PARSER_SOURCE , parser_builder )
2328- setattr (cmd_wrapper , constants .CMD_ATTR_PRESERVE_QUOTES , preserve_quotes )
2347+ argparse_command_spec = ArgparseCommandSpec (
2348+ parser_source = parser_builder ,
2349+ preserve_quotes = preserve_quotes ,
2350+ )
2351+ setattr (cmd_wrapper , constants .ARGPARSE_COMMAND_ATTR_SPEC , argparse_command_spec )
23292352
23302353 return cmd_wrapper
23312354
0 commit comments