Skip to content

Commit 5a643fd

Browse files
fix: fix edge case on forward unresolveable type
1 parent 31d0461 commit 5a643fd

2 files changed

Lines changed: 27 additions & 1 deletion

File tree

cmd2/annotated.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1726,8 +1726,16 @@ def _resolve_parameters(
17261726
# cmd2_handler, so it must exist. Here so it also fires when the function has zero parameters.
17271727
if base_command and "cmd2_handler" not in sig.parameters:
17281728
raise TypeError(f"with_annotated(base_command=True) requires a 'cmd2_handler' parameter in {func.__qualname__}")
1729+
# Resolve hints only for the parameters that become arguments
1730+
ignored = {next(iter(sig.parameters), None), *skip_params}
1731+
ignored.discard(None)
1732+
relevant_annotations = {name: ann for name, ann in getattr(func, "__annotations__", {}).items() if name not in ignored}
17291733
try:
1730-
hints = get_type_hints(func, include_extras=True)
1734+
hints = get_type_hints(
1735+
types.SimpleNamespace(__annotations__=relevant_annotations),
1736+
globalns=getattr(func, "__globals__", {}),
1737+
include_extras=True,
1738+
)
17311739
except (NameError, AttributeError, TypeError) as exc:
17321740
raise TypeError(
17331741
f"Failed to resolve type hints for {func.__qualname__}. Ensure all annotations use valid, importable types."

tests/test_annotated.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,24 @@ def do_broken(self, name: "NonExistentType"): # noqa: F821
631631
with pytest.raises(TypeError, match="Failed to resolve type hints"):
632632
build_parser_from_function(do_broken)
633633

634+
def test_unresolvable_hint_on_ignored_self_is_tolerated(self) -> None:
635+
"""An unresolvable annotation on the bound parameter (self/cls) must not abort
636+
parser generation, since it is never turned into an argument. This happens when
637+
``self`` is annotated with a forward reference that is only importable under
638+
``TYPE_CHECKING``.
639+
"""
640+
641+
def do_cmd(self: "UnimportableCmd", name: str, count: int = 1): # noqa: F821
642+
pass
643+
644+
# Without the lenient retry this raises "Failed to resolve type hints".
645+
parser = build_parser_from_function(do_cmd)
646+
dests = {action.dest for action in parser._actions if action.dest != "help"}
647+
assert dests == {"name", "count"}
648+
ns = parser.parse_args(["alice"])
649+
assert ns.name == "alice"
650+
assert ns.count == 1
651+
634652
def test_validate_base_command_type_hints_failure_raises(self) -> None:
635653
"""Base-command validation should raise, not swallow, type hint failures."""
636654
from cmd2.annotated import _resolve_parameters

0 commit comments

Comments
 (0)