https://github.com/python/cpython/commit/7317e9d761b89aa34a21ba0e7f7a8f1462d5297a
commit: 7317e9d761b89aa34a21ba0e7f7a8f1462d5297a
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-03-29T22:30:14-07:00
summary:

[3.14] gh-146556: Fix infinite loop in annotationlib.get_annotations() on 
circular __wrapped__ (GH-146557) (#146622)

gh-146556: Fix infinite loop in annotationlib.get_annotations() on circular 
__wrapped__ (GH-146557)
(cherry picked from commit 2cf6a68f028da164bdb9b0ce8ad2cc9bf8f72750)

Co-authored-by: Ramin Farajpour Cami <[email protected]>

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]

Reply via email to