Skip to content

Raise ValueError for unresolvable annotations in get_types - #1806

Open
Qrytics wants to merge 1 commit into
python-cmd2:mainfrom
Qrytics:pilot/slot-185-fix
Open

Qrytics wants to merge 1 commit into
python-cmd2:mainfrom
Qrytics:pilot/slot-185-fix

Conversation

@Qrytics

@Qrytics Qrytics commented Nov 10, 2026

Copy link
Copy Markdown

What

cmd2.utils.get_types documents that it raises ValueError when its input
can't be processed, but an unresolvable string annotation (e.g. a forward
reference under from __future__ import annotations, or a TYPE_CHECKING
-only import) leaks a raw NameError (or SyntaxError for invalid
annotation strings) out of inspect.get_annotations(..., eval_str=True).
That surfaces as a confusing crash when registering hooks/commands instead
of the documented error type.

Repro

from cmd2.utils import get_types

def foo(x: "NotDefinedAnywhere") -> None: ...
get_types(foo)  # NameError: name 'NotDefinedAnywhere' is not defined

Fix

Catch (NameError, SyntaxError) around the get_annotations call and
re-raise as ValueError naming the offending callable, mirroring the
existing TypeError handling in the same function. No success path
changes.

Tests

Added test_get_types_unresolvable_forward_reference in
tests/test_utils.py (fails before with NameError, passes after);
full test_utils.py get_types selection passes, and
ruff check/ruff format are clean.

@codecov

codecov Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.75%. Comparing base (4e98a38) to head (619a7e2).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1806   +/-   ##
=======================================
  Coverage   99.75%   99.75%           
=======================================
  Files          23       23           
  Lines        6593     6596    +3     
=======================================
+ Hits         6577     6580    +3     
  Misses         16       16           
Flag Coverage Δ
unittests 99.75% <100.00%> (+<0.01%) ⬆️

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 tleonhardt left a comment •

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.

@Qrytics
Thanks for the PR Mario! Wrapping the error and naming the offending callable is the right direction, and much better than leaking a raw NameError. A few changes requested inline. One more that has no line to attach to:

  • CHANGELOG: please add a bug-fix entry to CHANGELOG.md under a new in-progress version heading (4.4.1 is already released).

Comment thread cmd2/utils.py
"""
try:
type_hints = inspect.get_annotations(func_or_method, eval_str=True) # Get dictionary of type hints
except TypeError as exc:

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.

This existing handler also swallows a TypeError raised while evaluating an annotation. For example, def b(x: "int[str]") reports "Argument passed to get_types should be a function or method", which is wrong.

Action: distinguish the two cases, e.g. check callable(func_or_method) up front for the invalid-argument error, then treat any TypeError from get_annotations as a resolution failure.

Comment thread cmd2/utils.py
type_hints = inspect.get_annotations(func_or_method, eval_str=True) # Get dictionary of type hints
except TypeError as exc:
raise ValueError("Argument passed to get_types should be a function or method") from exc
except (NameError, SyntaxError) as exc:

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.

AttributeError still leaks: def a(x: "typing.NoSuch") raises AttributeError: module 'typing' has no attribute 'NoSuch'.

Action: add AttributeError to this tuple. _resolve_func_hints in cmd2/annotated.py catches (NameError, AttributeError, TypeError) for the same failure.

Comment thread cmd2/utils.py
raise ValueError("Argument passed to get_types should be a function or method") from exc
except (NameError, SyntaxError) as exc:
name = getattr(func_or_method, "__name__", func_or_method)
raise ValueError(f"Unable to resolve type hints for {name}: {exc}") from exc

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.

Consider raising TypeError here instead of ValueError. The only callers of get_types are the hook-registration validators in cmd2/cmd2.py, which raise TypeError for every other annotation problem, and _resolve_func_hints in cmd2/annotated.py already raises TypeError("Failed to resolve type hints for ...") for this exact failure.

Action: raise TypeError for unresolvable annotations and update the :raises docstring to match.

Comment thread tests/test_utils.py
assert param_value is int


def test_get_types_unresolvable_forward_reference() -> None:

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.

Only the NameError path is tested.

Action: add cases (parametrizing this test would work well) for:

  • invalid syntax, e.g. "1 +"
  • a bad attribute, e.g. "typing.NoSuch"
  • a TypeError during evaluation, e.g. "int[str]"

Please also add one test that registers a hook with an unresolvable annotation (e.g. via register_postparsing_hook) and asserts the resulting error.

This branch has not been deployed

No deployments
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