Skip to content

Commit 61a821b

Browse files
committed
pythongh-146553: Fix infinite loop in typing.get_type_hints() on circular __wrapped__
python#146553
1 parent 1efe441 commit 61a821b

3 files changed

Lines changed: 42 additions & 0 deletions

File tree

Lib/test/test_typing.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6837,6 +6837,34 @@ def test_get_type_hints_wrapped_decoratored_func(self):
68376837
self.assertEqual(gth(ForRefExample.func), expects)
68386838
self.assertEqual(gth(ForRefExample.nested), expects)
68396839

6840+
def test_get_type_hints_wrapped_cycle_self(self):
6841+
# gh-146553: __wrapped__ self-reference must raise ValueError,
6842+
# not loop forever.
6843+
def f(x: int) -> str: ...
6844+
f.__wrapped__ = f
6845+
with self.assertRaises(ValueError):
6846+
get_type_hints(f)
6847+
6848+
def test_get_type_hints_wrapped_cycle_mutual(self):
6849+
# gh-146553: mutual __wrapped__ cycle (a -> b -> a) must raise
6850+
# ValueError, not loop forever.
6851+
def a(): ...
6852+
def b(): ...
6853+
a.__wrapped__ = b
6854+
b.__wrapped__ = a
6855+
with self.assertRaises(ValueError):
6856+
get_type_hints(a)
6857+
6858+
def test_get_type_hints_wrapped_chain_no_cycle(self):
6859+
# gh-146553: a valid (non-cyclic) __wrapped__ chain must still work.
6860+
def inner(x: int) -> str: ...
6861+
def middle(x: int) -> str: ...
6862+
middle.__wrapped__ = inner
6863+
def outer(x: int) -> str: ...
6864+
outer.__wrapped__ = middle
6865+
# No cycle — should return the annotations without raising.
6866+
self.assertEqual(get_type_hints(outer), {'x': int, 'return': str})
6867+
68406868
def test_get_type_hints_annotated(self):
68416869
def foobar(x: List['X']): ...
68426870
X = Annotated[int, (1, 10)]

Lib/typing.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2485,8 +2485,18 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False,
24852485
else:
24862486
nsobj = obj
24872487
# Find globalns for the unwrapped object.
2488+
# Use an id-based visited set to detect and break on cycles in the
2489+
# __wrapped__ chain (e.g. f.__wrapped__ = f), matching the behavior
2490+
# of inspect.unwrap().
2491+
_seen_ids = {id(nsobj)}
24882492
while hasattr(nsobj, '__wrapped__'):
24892493
nsobj = nsobj.__wrapped__
2494+
_nsobj_id = id(nsobj)
2495+
if _nsobj_id in _seen_ids:
2496+
raise ValueError(
2497+
f'wrapper loop when unwrapping {obj!r}'
2498+
)
2499+
_seen_ids.add(_nsobj_id)
24902500
globalns = getattr(nsobj, '__globals__', {})
24912501
if localns is None:
24922502
localns = globalns
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`typing.get_type_hints` hanging indefinitely when a callable has a
2+
circular ``__wrapped__`` chain (e.g. ``f.__wrapped__ = f``). A
3+
:exc:`ValueError` is now raised on cycle detection, matching the behavior of
4+
:func:`inspect.unwrap`.

0 commit comments

Comments
 (0)