Skip to content

Commit 2a35236

Browse files
authored
Merge branch 'main' into claude/fix-deprecated-git-protocol-url
2 parents 3dfb201 + 0786a4e commit 2a35236

3 files changed

Lines changed: 275 additions & 68 deletions

File tree

cmd2/annotated.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def do_paint(
226226
UnboundCompleter,
227227
)
228228

229-
if TYPE_CHECKING:
229+
if TYPE_CHECKING: # pragma: no cover
230230
from .argparse_completer import ArgparseCompleter
231231

232232
#: ``nargs`` values accepted by cmd2's patched ``add_argument`` (incl. ranged tuples).
@@ -1745,8 +1745,19 @@ def _resolve_parameters(
17451745
f"with_annotated(base_command=True) requires a '{constants.NS_ATTR_SUBCOMMAND_FUNC}' "
17461746
f"parameter in {func.__qualname__}"
17471747
)
1748+
# Resolve hints only for the parameters that become arguments: the bound first parameter
1749+
# (self/cls), the injected skip_params, and the "return" annotation never become arguments
1750+
ignored = {next(iter(sig.parameters), None), "return", *skip_params}
1751+
ignored.discard(None)
1752+
relevant_annotations = {name: ann for name, ann in getattr(func, "__annotations__", {}).items() if name not in ignored}
1753+
# Forward references resolve against the *original* function's module during functools.wraps wrapper.
1754+
unwrapped = inspect.unwrap(func)
17481755
try:
1749-
hints = get_type_hints(func, include_extras=True)
1756+
hints = get_type_hints(
1757+
types.SimpleNamespace(__annotations__=relevant_annotations),
1758+
globalns=getattr(unwrapped, "__globals__", {}),
1759+
include_extras=True,
1760+
)
17501761
except (NameError, AttributeError, TypeError) as exc:
17511762
raise TypeError(
17521763
f"Failed to resolve type hints for {func.__qualname__}. Ensure all annotations use valid, importable types."
@@ -1986,25 +1997,16 @@ def build_parser_from_function(
19861997
mutually_exclusive_groups=mutually_exclusive_groups,
19871998
)
19881999

1989-
# ``argument_default=argparse.SUPPRESS`` removes an absent argument from the parsed namespace.
1990-
# That is safe only for arguments that are always supplied (required) or carry their own default;
1991-
# an *omittable* argument with no default (e.g. a ``T | None`` positional -> nargs='?') would be
1992-
# dropped when absent, leaving the function without a keyword argument it expects. ``*args`` is
1993-
# exempt: the invocation path substitutes an empty tuple for it. Reject the combination here,
1994-
# mirroring the per-argument ``default=argparse.SUPPRESS`` rejection.
2000+
# ``argument_default=argparse.SUPPRESS`` drops an absent argument from the parsed namespace.
2001+
# @with_annotated builds the call from the function signature, so every declared parameter is
2002+
# expected at invocation -- an argument vanishing from the namespace can never be valid here.
2003+
# Reject it outright, mirroring the per-argument ``default=argparse.SUPPRESS`` rejection.
19952004
if parser_kwargs.get("argument_default") is argparse.SUPPRESS:
1996-
dropped = [
1997-
arg.name
1998-
for arg in resolved
1999-
if arg.default is _UNSET and arg.omittable and not arg.required and not arg.is_variadic
2000-
]
2001-
if dropped:
2002-
raise TypeError(
2003-
f"argument_default=argparse.SUPPRESS is not supported by @with_annotated for {func.__qualname__}: "
2004-
f"it would drop {dropped!r} from the parsed namespace when absent, but the function expects "
2005-
f"{'them' if len(dropped) > 1 else 'it'} as a keyword argument. Give each an explicit default or "
2006-
f"make it required, or drop argument_default=argparse.SUPPRESS."
2007-
)
2005+
raise TypeError(
2006+
f"argument_default=argparse.SUPPRESS is not supported by @with_annotated for {func.__qualname__}: "
2007+
f"it drops absent arguments from the parsed namespace, but every parameter built from the "
2008+
f"signature is expected at invocation. Drop argument_default=argparse.SUPPRESS."
2009+
)
20082010

20092011
# Build the group lookup (member references already validated by _resolve_parameters).
20102012
target_for, argument_group_for = _build_argument_group_targets(parser, groups=groups)
@@ -2124,7 +2126,7 @@ def handler(self_arg: Any, ns: Any) -> Any:
21242126
filtered = _filtered_namespace_kwargs(ns, accepted=_accepted)
21252127
if constants.NS_ATTR_SUBCOMMAND_FUNC in filtered:
21262128
cmd2_h = filtered[constants.NS_ATTR_SUBCOMMAND_FUNC]
2127-
if isinstance(cmd2_h, functools.partial) and cmd2_h.func is handler:
2129+
if isinstance(cmd2_h, functools.partial) and getattr(cmd2_h.func, "__func__", cmd2_h.func) is handler:
21282130
filtered[constants.NS_ATTR_SUBCOMMAND_FUNC] = None
21292131
return _invoke_command_func(
21302132
func, self_arg, filtered, leading_names=_leading_names, var_positional_name=_var_positional_name

docs/features/annotated.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -363,14 +363,11 @@ def do_run(self, verbose: bool = False, quiet: bool = False): ...
363363
```
364364

365365
`parents=` mirrors argparse's standard parents mechanism for sharing argument definitions across
366-
parsers. `argument_default=argparse.SUPPRESS` is accepted only when no argument could be stranded by
367-
it: it removes an absent argument from the parsed namespace, which is safe for an argument that is
368-
always supplied (a required option, a mandatory positional) or that carries its own default, but not
369-
for an _omittable_ argument with no default (for example a `T | None` positional, which becomes
370-
`nargs='?'`). If any such argument is present, `@with_annotated` raises `TypeError` rather than let
371-
the function be called missing a keyword argument it expects (mirroring the per-argument
372-
`default=argparse.SUPPRESS` rejection). `*args` is exempt, since the invocation path substitutes an
373-
empty tuple for it.
366+
parsers. `argument_default=argparse.SUPPRESS` is not supported and raises `TypeError`. It removes an
367+
absent argument from the parsed namespace, but `@with_annotated` builds the call from the function
368+
signature, so every declared parameter is expected at invocation; an argument vanishing from the
369+
namespace can never be valid here (mirroring the per-argument `default=argparse.SUPPRESS`
370+
rejection). Any other `argument_default` value is forwarded to the parser unchanged.
374371

375372
The remaining argparse kwargs cover less-common needs but are wired through unchanged:
376373

0 commit comments

Comments
 (0)