From 8754d322628ad177693c24617084419cd9daa6e2 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:22:37 +0200 Subject: [PATCH 1/2] gh-156924: Try reifying lazy imports in `ForwarRef.evaluate()` --- Lib/annotationlib.py | 20 ++++++- Lib/test/test_annotationlib.py | 58 +++++++++++++++++++ ...-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst | 6 ++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-04-14-15-31.gh-issue-156924.3Ux7IJ.rst diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 8204c762cce8a2..61ea56f6a16fdc 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -191,12 +191,26 @@ 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, and assume it's an error + # if format=VALUE was used: + 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. From a4904055a59d77dac2bcd747dca4ba567dcf17fe Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:00:18 +0200 Subject: [PATCH 2/2] Update Lib/annotationlib.py Co-authored-by: Jelle Zijlstra --- Lib/annotationlib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 61ea56f6a16fdc..26121e3af31cb9 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -201,8 +201,9 @@ def evaluate( if resolved is not _sentinel: if isinstance(resolved, types.LazyImportType): - # We try reifying the lazy object, and assume it's an error - # if format=VALUE was used: + # 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: