https://github.com/python/cpython/commit/013c4ae5d036a3be65633b8dd73e042a4845cc4d
commit: 013c4ae5d036a3be65633b8dd73e042a4845cc4d
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: hugovk <[email protected]>
date: 2026-09-10T13:17:08+03:00
summary:

[3.15] gh-156924: Try reifying lazy imports in `ForwarRef.evaluate()` 
(GH-156940) (#157224)

Co-authored-by: Victorien <[email protected]>
Co-authored-by: Jelle Zijlstra <[email protected]>
Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: Bartosz SÅ‚awecki <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py

diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index 5c9a0812646f814..9ae1e3a06e91767 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -191,12 +191,27 @@ def evaluate(
 
         arg = self.__forward_arg__
         if arg.isidentifier() and not keyword.iskeyword(arg):
+            resolved = _sentinel
             if arg in locals:
-                return locals[arg]
+                resolved = locals[arg]
             elif arg in globals:
-                return globals[arg]
+                resolved = globals[arg]
             elif hasattr(builtins, arg):
-                return getattr(builtins, arg)
+                resolved = getattr(builtins, arg)
+
+            if resolved is not _sentinel:
+                if isinstance(resolved, types.LazyImportType):
+                    # We try reifying the lazy object. If this fails, we 
propagate
+                    # the error in the VALUE format and leave the
+                    # ForwardRef unresolved in the FORWARDREF format.
+                    try:
+                        return resolved.resolve()
+                    except Exception:
+                        if not is_forwardref_format:
+                            raise
+                        return self
+                else:
+                    return resolved
             elif is_forwardref_format:
                 return self
             else:
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index 530114161701b5a..7bdb4a9993f4e33 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -2179,6 +2179,118 @@ def test_evaluate_forwardref_format(self):
             support.EqualToForwardRef('"a" + 1'),
         )
 
+    def test_evaluate_lazy_import(self):
+        ns = {}
+        exec(
+            textwrap.dedent(
+                """
+                lazy from test.test_lazy_import.data.basic2 import x
+                lazy from test.test_lazy_import.data.broken_module import y
+
+                class A:
+                    a: x
+
+                class B:
+                    b: y
+                """
+            ),
+            ns,
+        )
+        self.addCleanup(
+            import_helper.unload, "test.test_lazy_import.data.basic2"
+        )
+        self.addCleanup(
+            import_helper.unload, "test.test_lazy_import.data.broken_module"
+        )
+        self.assertIs(type(ns["x"]), types.LazyImportType)
+        self.assertIs(type(ns["y"]), types.LazyImportType)
+
+        # The lazy import resolves successfully:
+        for format in (Format.VALUE, Format.FORWARDREF):
+            with self.subTest(format=format):
+                self.assertEqual(
+                    ForwardRef("x").evaluate(globals=ns, format=format), 42
+                )
+                self.assertEqual(
+                    ForwardRef("x").evaluate(locals=ns, format=format), 42
+                )
+        self.assertEqual(
+            get_annotations(ns["A"], format=Format.FORWARDREF), {"a": 42}
+        )
+
+        # The lazy import fails to resolve:
+        fr = ForwardRef("y")
+        with self.assertRaisesRegex(ValueError, "always fails to import"):
+            fr.evaluate(globals=ns, format=Format.VALUE)
+        with self.assertRaisesRegex(ValueError, "always fails to import"):
+            fr.evaluate(locals=ns, format=Format.VALUE)
+        self.assertIs(fr.evaluate(globals=ns, format=Format.FORWARDREF), fr)
+        self.assertIs(fr.evaluate(locals=ns, format=Format.FORWARDREF), fr)
+
+        annos = get_annotations(ns["B"], format=Format.FORWARDREF)
+        self.assertEqual(
+            annos,
+            {"b": support.EqualToForwardRef("y", is_class=True, 
owner=ns["B"])},
+        )
+        with self.assertRaisesRegex(ValueError, "always fails to import"):
+            annos["b"].evaluate(format=Format.VALUE)
+        with self.assertRaisesRegex(ValueError, "always fails to import"):
+            get_annotations(ns["B"], format=Format.VALUE)
+
+    def test_get_annotations_lazy_import(self):
+        ns = {}
+        exec(
+            textwrap.dedent(
+                """
+                lazy from test.test_lazy_import.data.basic2 import x
+                lazy from test.test_lazy_import.data.broken_module import y
+
+                class A:
+                    a: object.fail
+                    b: x
+
+                class B:
+                    a: object.fail
+                    b: y
+                """
+            ),
+            ns,
+        )
+        self.addCleanup(
+            import_helper.unload, "test.test_lazy_import.data.basic2"
+        )
+        self.addCleanup(
+            import_helper.unload, "test.test_lazy_import.data.broken_module"
+        )
+        self.assertIs(type(ns["x"]), types.LazyImportType)
+        self.assertIs(type(ns["y"]), types.LazyImportType)
+
+        annos = get_annotations(ns["A"], format=Format.FORWARDREF)
+        self.assertEqual(
+            annos,
+            {
+                "a": support.EqualToForwardRef(
+                    "object.fail", is_class=True, owner=ns["A"]
+                ),
+                "b": 42,
+            },
+        )
+
+        annos = get_annotations(ns["B"], format=Format.FORWARDREF)
+        self.assertEqual(
+            annos,
+            {
+                "a": support.EqualToForwardRef(
+                    "object.fail", is_class=True, owner=ns["B"]
+                ),
+                "b": support.EqualToForwardRef(
+                    "y", is_class=True, owner=ns["B"]
+                ),
+            },
+        )
+        with self.assertRaisesRegex(ValueError, "always fails to import"):
+            annos["b"].evaluate(format=Format.VALUE)
+
     def test_evaluate_notimplemented_format(self):
         class C:
             x: alias
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst 
b/Misc/NEWS.d/next/Library/2026-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst
new file mode 100644
index 000000000000000..61d0ed69857f3fd
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst
@@ -0,0 +1,6 @@
+:meth:`annotationlib.ForwardRef.evaluate` now resolves :ref:`lazy import
+<lazy-imports>` proxies found in the namespace. With
+:attr:`~annotationlib.Format.FORWARDREF`, a lazy import that fails to resolve
+now results in a :class:`~annotationlib.ForwardRef` instead of the
+:class:`lazy import proxy <types.LazyImportType>`, and with 
:attr:`~annotationlib.Format.VALUE` the underlying exception is
+propagated.

_______________________________________________
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