annotationlib.get_annotations() infinite loop on __wrapped__ cycle (eval_str=True)
Bug report
Bug description:
annotationlib.get_annotations() hangs indefinitely when called with eval_str=True on a callable that has a circular __wrapped__ reference chain.
Reproducer
import annotationlib def f(x: 'int') -> 'str': pass f.__wrapped__ = f # self-referential cycle # Hangs forever, never returns annotationlib.get_annotations(f, eval_str=True)
Two-node cycle also triggers it:
def g(): pass f.__wrapped__ = g g.__wrapped__ = f annotationlib.get_annotations(f, eval_str=True) # hangs
Root Cause
In Lib/annotationlib.py, get_annotations() has an early-return guard at line 1010:
if not eval_str: return dict(ann) # fast path, skips unwrap entirely for default eval_str=False
When eval_str=True the code falls through to a while True: loop (lines 1039โ1048)
that unwraps __wrapped__ chains with no cycle detection:
if unwrap is not None: while True: if hasattr(unwrap, "__wrapped__"): unwrap = unwrap.__wrapped__ # no cycle detection continue if functools := sys.modules.get("functools"): if isinstance(unwrap, functools.partial): unwrap = unwrap.func # also no cycle detection continue break
Fix
Apply the same cycle-detection pattern used by inspect.unwrap() (visited id-set):
if unwrap is not None: seen = {id(unwrap)} while True: if hasattr(unwrap, "__wrapped__"): candidate = unwrap.__wrapped__ if id(candidate) in seen: break seen.add(id(candidate)) unwrap = candidate continue if functools := sys.modules.get("functools"): if isinstance(unwrap, functools.partial): candidate = unwrap.func if id(candidate) in seen: break seen.add(id(candidate)) unwrap = candidate continue break if hasattr(unwrap, "__globals__"): obj_globals = unwrap.__globals__
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux