obj_globals = obj_locals = unwrap = None
if unwrap is not None:
+ # Use an id-based visited set to detect cycles in the __wrapped__
+ # and functools.partial.func chain (e.g. f.__wrapped__ = f).
+ # On cycle detection we stop and use whatever __globals__ we have
+ # found so far, mirroring the approach of inspect.unwrap().
+ _seen_ids = {id(unwrap)}
while True:
if hasattr(unwrap, "__wrapped__"):
- unwrap = unwrap.__wrapped__
+ candidate = unwrap.__wrapped__
+ if id(candidate) in _seen_ids:
+ break
+ _seen_ids.add(id(candidate))
+ unwrap = candidate
continue
if functools := sys.modules.get("functools"):
if isinstance(unwrap, functools.partial):
- unwrap = unwrap.func
+ candidate = unwrap.func
+ if id(candidate) in _seen_ids:
+ break
+ _seen_ids.add(id(candidate))
+ unwrap = candidate
continue
break
if hasattr(unwrap, "__globals__"):
get_annotations(foo, format=Format.FORWARDREF, eval_str=True)
get_annotations(foo, format=Format.STRING, eval_str=True)
+ def test_eval_str_wrapped_cycle_self(self):
+ # gh-146556: self-referential __wrapped__ cycle must not hang.
+ def f(x: 'int') -> 'str': ...
+ f.__wrapped__ = f
+ # Cycle is detected and broken; globals from f itself are used.
+ result = get_annotations(f, eval_str=True)
+ self.assertEqual(result, {'x': int, 'return': str})
+
+ def test_eval_str_wrapped_cycle_mutual(self):
+ # gh-146556: mutual __wrapped__ cycle (a -> b -> a) must not hang.
+ def a(x: 'int'): ...
+ def b(): ...
+ a.__wrapped__ = b
+ b.__wrapped__ = a
+ result = get_annotations(a, eval_str=True)
+ self.assertEqual(result, {'x': int})
+
+ def test_eval_str_wrapped_chain_no_cycle(self):
+ # gh-146556: a valid (non-cyclic) __wrapped__ chain must still work.
+ def inner(x: 'int'): ...
+ def outer(x: 'int'): ...
+ outer.__wrapped__ = inner
+ result = get_annotations(outer, eval_str=True)
+ self.assertEqual(result, {'x': int})
+
def test_stock_annotations(self):
def foo(a: int, b: str):
pass
--- /dev/null
+Fix :func:`annotationlib.get_annotations` hanging indefinitely when called
+with ``eval_str=True`` on a callable that has a circular ``__wrapped__``
+chain (e.g. ``f.__wrapped__ = f``). Cycle detection using an id-based
+visited set now stops the traversal and falls back to the globals found
+so far, mirroring the approach of :func:`inspect.unwrap`.