Skip to content

Commit d791036

Browse files
ViicosJelleZijlstraencukoujohnslavik
authored andcommitted
gh-156924: Try reifying lazy imports in ForwarRef.evaluate() (GH-156940)
(cherry picked from commit 52ffffe) Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com> Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com> Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
1 parent cf312cf commit d791036

3 files changed

Lines changed: 136 additions & 3 deletions

File tree

Lib/annotationlib.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,27 @@ 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. If this fails, we propagate
205+
# the error in the VALUE format and leave the
206+
# ForwardRef unresolved in the FORWARDREF format.
207+
try:
208+
return resolved.resolve()
209+
except Exception:
210+
if not is_forwardref_format:
211+
raise
212+
return self
213+
else:
214+
return resolved
200215
elif is_forwardref_format:
201216
return self
202217
else:

Lib/test/test_annotationlib.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2179,6 +2179,118 @@ 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+
2240+
def test_get_annotations_lazy_import(self):
2241+
ns = {}
2242+
exec(
2243+
textwrap.dedent(
2244+
"""
2245+
lazy from test.test_lazy_import.data.basic2 import x
2246+
lazy from test.test_lazy_import.data.broken_module import y
2247+
2248+
class A:
2249+
a: object.fail
2250+
b: x
2251+
2252+
class B:
2253+
a: object.fail
2254+
b: y
2255+
"""
2256+
),
2257+
ns,
2258+
)
2259+
self.addCleanup(
2260+
import_helper.unload, "test.test_lazy_import.data.basic2"
2261+
)
2262+
self.addCleanup(
2263+
import_helper.unload, "test.test_lazy_import.data.broken_module"
2264+
)
2265+
self.assertIs(type(ns["x"]), types.LazyImportType)
2266+
self.assertIs(type(ns["y"]), types.LazyImportType)
2267+
2268+
annos = get_annotations(ns["A"], format=Format.FORWARDREF)
2269+
self.assertEqual(
2270+
annos,
2271+
{
2272+
"a": support.EqualToForwardRef(
2273+
"object.fail", is_class=True, owner=ns["A"]
2274+
),
2275+
"b": 42,
2276+
},
2277+
)
2278+
2279+
annos = get_annotations(ns["B"], format=Format.FORWARDREF)
2280+
self.assertEqual(
2281+
annos,
2282+
{
2283+
"a": support.EqualToForwardRef(
2284+
"object.fail", is_class=True, owner=ns["B"]
2285+
),
2286+
"b": support.EqualToForwardRef(
2287+
"y", is_class=True, owner=ns["B"]
2288+
),
2289+
},
2290+
)
2291+
with self.assertRaisesRegex(ValueError, "always fails to import"):
2292+
annos["b"].evaluate(format=Format.VALUE)
2293+
21822294
def test_evaluate_notimplemented_format(self):
21832295
class C:
21842296
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
5+
:class:`lazy import proxy <types.LazyImportType>`, and with :attr:`~annotationlib.Format.VALUE` the underlying exception is
6+
propagated.

0 commit comments

Comments
 (0)