Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6837,6 +6837,34 @@ def test_get_type_hints_wrapped_decoratored_func(self):
self.assertEqual(gth(ForRefExample.func), expects)
self.assertEqual(gth(ForRefExample.nested), expects)

def test_get_type_hints_wrapped_cycle_self(self):
# gh-146553: __wrapped__ self-reference must raise ValueError,
# not loop forever.
def f(x: int) -> str: ...
f.__wrapped__ = f
with self.assertRaises(ValueError):
Comment thread
raminfp marked this conversation as resolved.
Outdated
get_type_hints(f)

def test_get_type_hints_wrapped_cycle_mutual(self):
# gh-146553: mutual __wrapped__ cycle (a -> b -> a) must raise
# ValueError, not loop forever.
def a(): ...
def b(): ...
a.__wrapped__ = b
b.__wrapped__ = a
with self.assertRaises(ValueError):
get_type_hints(a)

def test_get_type_hints_wrapped_chain_no_cycle(self):
# gh-146553: a valid (non-cyclic) __wrapped__ chain must still work.
def inner(x: int) -> str: ...
def middle(x: int) -> str: ...
middle.__wrapped__ = inner
def outer(x: int) -> str: ...
outer.__wrapped__ = middle
# No cycle — should return the annotations without raising.
self.assertEqual(get_type_hints(outer), {'x': int, 'return': str})
Comment thread
raminfp marked this conversation as resolved.
Outdated

def test_get_type_hints_annotated(self):
def foobar(x: List['X']): ...
X = Annotated[int, (1, 10)]
Expand Down
10 changes: 10 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2485,8 +2485,18 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
else:
nsobj = obj
# Find globalns for the unwrapped object.
# Use an id-based visited set to detect and break on cycles in the
# __wrapped__ chain (e.g. f.__wrapped__ = f), matching the behavior
# of inspect.unwrap().
Comment thread
raminfp marked this conversation as resolved.
Outdated
_seen_ids = {id(nsobj)}
Comment thread
raminfp marked this conversation as resolved.
Outdated
while hasattr(nsobj, '__wrapped__'):
nsobj = nsobj.__wrapped__
_nsobj_id = id(nsobj)
if _nsobj_id in _seen_ids:
raise ValueError(
f'wrapper loop when unwrapping {obj!r}'
)
_seen_ids.add(_nsobj_id)
globalns = getattr(nsobj, '__globals__', {})
if localns is None:
localns = globalns
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :func:`typing.get_type_hints` hanging indefinitely when a callable has a
circular ``__wrapped__`` chain (e.g. ``f.__wrapped__ = f``). A
:exc:`ValueError` is now raised on cycle detection, matching the behavior of
:func:`inspect.unwrap`.
Loading