https://github.com/python/cpython/commit/2cf6a68f028da164bdb9b0ce8ad2cc9bf8f72750
commit: 2cf6a68f028da164bdb9b0ce8ad2cc9bf8f72750
branch: main
author: Ramin Farajpour Cami <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-03-30T03:08:18Z
summary:
gh-146556: Fix infinite loop in annotationlib.get_annotations() on circular
__wrapped__ (#146557)
files:
A Misc/NEWS.d/next/Library/2026-03-28-12-20-19.gh-issue-146556.Y8Eson.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index df8fb5e4c62079..9fee2564114339 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -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__"):
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index e89d6c0b1613ba..50cf8fcb6b4ed6 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -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
diff --git
a/Misc/NEWS.d/next/Library/2026-03-28-12-20-19.gh-issue-146556.Y8Eson.rst
b/Misc/NEWS.d/next/Library/2026-03-28-12-20-19.gh-issue-146556.Y8Eson.rst
new file mode 100644
index 00000000000000..71f84593edb522
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-03-28-12-20-19.gh-issue-146556.Y8Eson.rst
@@ -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`.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]