mirror of
https://github.com/python/cpython.git
synced 2026-04-17 01:10:46 +00:00
gh-146556: Fix infinite loop in annotationlib.get_annotations() on circular __wrapped__ (#146557)
This commit is contained in:
parent
4d0e8ee649
commit
2cf6a68f02
3 changed files with 45 additions and 2 deletions
|
|
@ -1037,13 +1037,26 @@ def get_annotations(
|
|||
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__"):
|
||||
|
|
|
|||
|
|
@ -646,6 +646,31 @@ def foo():
|
|||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue