Skip to content

fix: fix edge case on forward unresolveable type#1673

Open
KelvinChung2000 wants to merge 1 commit into
mainfrom
unuse-param-edge-case
Open

fix: fix edge case on forward unresolveable type#1673
KelvinChung2000 wants to merge 1 commit into
mainfrom
unuse-param-edge-case

Conversation

@KelvinChung2000
Copy link
Copy Markdown
Contributor

Fix an edge case when doing forward declaration on types:

  if TYPE_CHECKING:
      from myapp.cli import MyCmd

  @with_annotated
  def do_greet(self: "MyCmd", name: str, count: int = 1):
      ...

which happens when trying to declare the command in another file, while still wanting to keep the type hinting for self.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.40%. Comparing base (31d0461) to head (5a643fd).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1673      +/-   ##
==========================================
- Coverage   99.50%   99.40%   -0.11%     
==========================================
  Files          23       23              
  Lines        5689     5692       +3     
==========================================
- Hits         5661     5658       -3     
- Misses         28       34       +6     
Flag Coverage Δ
unittests 99.40% <100.00%> (-0.11%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@tleonhardt
Copy link
Copy Markdown
Member

The one test failure is due to release of Python 3.15.0b2 and is fixed in PR #1674

@KelvinChung2000
Copy link
Copy Markdown
Contributor Author

I will rebase once that is landed. I found another edge case related to the function return type that also needs to be covered

Comment thread cmd2/annotated.py
hints = get_type_hints(func, include_extras=True)
hints = get_type_hints(
types.SimpleNamespace(__annotations__=relevant_annotations),
globalns=getattr(func, "__globals__", {}),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If func is wrapped by another decorator using @functools.wraps, its __globals__ attribute points to the decorator's module, not the original function's module. Because globalns is explicitly provided here, get_type_hints() won't traverse the __wrapped__ chain to find the correct globals. This will cause forward references (e.g., string annotations) to fail with NameError or resolve to the wrong type.

Suggested change:

ignored = {next(iter(sig.parameters), None), *skip_params}
ignored.discard(None)
relevant_annotations = {name: ann for name, ann in getattr(func, "__annotations__", {}).items() if name not in ignored}
unwrapped = inspect.unwrap(func)
try:
    hints = get_type_hints(
        types.SimpleNamespace(__annotations__=relevant_annotations),
        globalns=getattr(unwrapped, "__globals__", {}),
        include_extras=True,
    )

The key change is created unrwapped = inspect.unwrap(func) before the try and then using unwrapped instead of func in the call to getattr.

@tleonhardt
Copy link
Copy Markdown
Member

@KelvinChung2000 I merged the fix for Python 3.15.0b2. You are free to rebase when you see fit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants