diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 8204c762cce8a2..26121e3af31cb9 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 530114161701b5..4557d3d6cf322b 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -2179,6 +2179,64 @@ 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_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 00000000000000..345de4add820f3 --- /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 +` 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 lazy import +proxy, and with :attr:`~annotationlib.Format.VALUE` the underlying exception is +propagated.