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
Linked PRs
Bug report
Bug description:
annotationlib.get_annotations()hangs indefinitely when called witheval_str=Trueon a callable that has a circular__wrapped__reference chain.Reproducer
Two-node cycle also triggers it:
Root Cause
In
Lib/annotationlib.py,get_annotations()has an early-return guard at line 1010:When
eval_str=Truethe code falls through to awhile True:loop (lines 1039–1048)that unwraps
__wrapped__chains with no cycle detection:Fix
Apply the same cycle-detection pattern used by
inspect.unwrap()(visitedid-set):CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs