Skip to content

Commit 8754d32

Browse files
committed
gh-156924: Try reifying lazy imports in ForwarRef.evaluate()
1 parent 7d71b3e commit 8754d32

3 files changed

Lines changed: 81 additions & 3 deletions

File tree

Lib/annotationlib.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,26 @@ def evaluate(
191191

192192
arg = self.__forward_arg__
193193
if arg.isidentifier() and not keyword.iskeyword(arg):
194+
resolved = _sentinel
194195
if arg in locals:
195-
return locals[arg]
196+
resolved = locals[arg]
196197
elif arg in globals:
197-
return globals[arg]
198+
resolved = globals[arg]
198199
elif hasattr(builtins, arg):
199-
return getattr(builtins, arg)
200+
resolved = getattr(builtins, arg)
201+
202+
if resolved is not _sentinel:
203+
if isinstance(resolved, types.LazyImportType):
204+
# We try reifying the lazy object, and assume it's an error
205+
# if format=VALUE was used:
206+
try:
207+
return resolved.resolve()
208+
except Exception:
209+
if not is_forwardref_format:
210+
raise
211+
return self
212+
else:
213+
return resolved
200214
elif is_forwardref_format:
201215
return self
202216
else:

Lib/test/test_annotationlib.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2179,6 +2179,64 @@ def test_evaluate_forwardref_format(self):
21792179
support.EqualToForwardRef('"a" + 1'),
21802180
)
21812181

2182+
def test_evaluate_lazy_import(self):
2183+
ns = {}
2184+
exec(
2185+
textwrap.dedent(
2186+
"""
2187+
lazy from test.test_lazy_import.data.basic2 import x
2188+
lazy from test.test_lazy_import.data.broken_module import y
2189+
2190+
class A:
2191+
a: x
2192+
2193+
class B:
2194+
b: y
2195+
"""
2196+
),
2197+
ns,
2198+
)
2199+
self.addCleanup(
2200+
import_helper.unload, "test.test_lazy_import.data.basic2"
2201+
)
2202+
self.addCleanup(
2203+
import_helper.unload, "test.test_lazy_import.data.broken_module"
2204+
)
2205+
self.assertIs(type(ns["x"]), types.LazyImportType)
2206+
self.assertIs(type(ns["y"]), types.LazyImportType)
2207+
2208+
# The lazy import resolves successfully:
2209+
for format in (Format.VALUE, Format.FORWARDREF):
2210+
with self.subTest(format=format):
2211+
self.assertEqual(
2212+
ForwardRef("x").evaluate(globals=ns, format=format), 42
2213+
)
2214+
self.assertEqual(
2215+
ForwardRef("x").evaluate(locals=ns, format=format), 42
2216+
)
2217+
self.assertEqual(
2218+
get_annotations(ns["A"], format=Format.FORWARDREF), {"a": 42}
2219+
)
2220+
2221+
# The lazy import fails to resolve:
2222+
fr = ForwardRef("y")
2223+
with self.assertRaisesRegex(ValueError, "always fails to import"):
2224+
fr.evaluate(globals=ns, format=Format.VALUE)
2225+
with self.assertRaisesRegex(ValueError, "always fails to import"):
2226+
fr.evaluate(locals=ns, format=Format.VALUE)
2227+
self.assertIs(fr.evaluate(globals=ns, format=Format.FORWARDREF), fr)
2228+
self.assertIs(fr.evaluate(locals=ns, format=Format.FORWARDREF), fr)
2229+
2230+
annos = get_annotations(ns["B"], format=Format.FORWARDREF)
2231+
self.assertEqual(
2232+
annos,
2233+
{"b": support.EqualToForwardRef("y", is_class=True, owner=ns["B"])},
2234+
)
2235+
with self.assertRaisesRegex(ValueError, "always fails to import"):
2236+
annos["b"].evaluate(format=Format.VALUE)
2237+
with self.assertRaisesRegex(ValueError, "always fails to import"):
2238+
get_annotations(ns["B"], format=Format.VALUE)
2239+
21822240
def test_evaluate_notimplemented_format(self):
21832241
class C:
21842242
x: alias
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
:meth:`annotationlib.ForwardRef.evaluate` now resolves :ref:`lazy import
2+
<lazy-imports>` proxies found in the namespace. With
3+
:attr:`~annotationlib.Format.FORWARDREF`, a lazy import that fails to resolve
4+
now results in a :class:`~annotationlib.ForwardRef` instead of the lazy import
5+
proxy, and with :attr:`~annotationlib.Format.VALUE` the underlying exception is
6+
propagated.

0 commit comments

Comments
 (0)