8000 gh-119180: Improvements to ForwardRef.evaluate (#122210) · python/cpython@016f4b5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 016f4b5

Browse files
gh-119180: Improvements to ForwardRef.evaluate (#122210)
Noticed some issues while writing documentation for this method.
1 parent a6644d4 commit 016f4b5

File tree

3 files changed

+60
-11
lines changed

3 files changed

+60
-11
lines changed

Lib/annotationlib.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def __init_subclass__(cls, /, *args, **kwds):
7474
def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
7575
"""Evaluate the forward reference and return the value.
7676
77-
If the forward reference is not evaluatable, raise an exception.
77+
If the forward reference cannot be evaluated, raise an exception.
7878
"""
7979
if self.__forward_evaluated__:
8080
return self.__forward_value__
@@ -89,12 +89,10 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
8989
return value
9090
if owner is None:
9191
owner = self.__owner__
92-
if type_params is None and owner is None:
93-
raise TypeError("Either 'type_params' or 'owner' must be provided")
9492

95-
if self.__forward_module__ is not None:
93+
if globals is None and self.__forward_module__ is not None:
9694
globals = getattr(
97-
sys.modules.get(self.__forward_module__, None), "__dict__", globals
95+
sys.modules.get(self.__forward_module__, None), "__dict__", None
9896
)
9997
if globals is None:
10098
globals = self.__globals__
@@ -112,14 +110,14 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
112110

113111
if locals is None:
114112
locals = {}
115-
if isinstance(self.__owner__, type):
116-
locals.update(vars(self.__owner__))
113+
if isinstance(owner, type):
114+
locals.update(vars(owner))
117115

118-
if type_params is None and self.__owner__ is not None:
116+
if type_params is None and owner is not None:
119117
# "Inject" type parameters into the local namespace
120118
# (unless they are shadowed by assignments *in* the local namespace),
121119
# as a way of emulating annotation scopes when calling `eval()`
122-
type_params = getattr(self.__owner__, "__type_params__", None)
120+
type_params = getattr(owner, "__type_params__", None)
123121

124122
# type parameters require some special handling,
125123
# as they exist in their own scope
@@ -129,7 +127,14 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
129127
# but should in turn be overridden by names in the class scope
130128
# (which here are called `globalns`!)
131129
if type_params is not None:
132-
globals, locals = dict(globals), dict(locals)
130+
if globals is None:
131+
globals = {}
132+
else:
133+
globals = dict(globals)
134+
if locals is None:
135+
locals = {}
136+
else:
137+
locals = dict(locals)
133138
for param in type_params:
134139
param_name = param.__name__
135140
if not self.__forward_is_class__ or param_name not in globals:

Lib/test/test_annotationlib.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import itertools
66
import pickle
77
import unittest
8-
from annotationlib import Format, get_annotations, get_annotate_function
8+
from annotationlib import Format, ForwardRef, get_annotations, get_annotate_function
99
from typing import Unpack
1010

1111
from test.test_inspect import inspect_stock_annotations
@@ -250,6 +250,46 @@ def test_special_attrs(self):
250250
with self.assertRaises(TypeError):
251251
pickle.dumps(fr, proto)
252252

253+
def test_evaluate_with_type_params(self):
254+
class Gen[T]:
255+
alias = int
256+
257+
with self.assertRaises(NameError):
258+
ForwardRef("T").evaluate()
259+
with self.assertRaises(NameError):
260+
ForwardRef("T").evaluate(type_params=())
261+
with self.assertRaises(NameError):
262+
ForwardRef("T").evaluate(owner=int)
263+
264+
T, = Gen.__type_params__
265+
self.assertIs(ForwardRef("T").evaluate(type_params=Gen.__type_params__), T)
266+
self.assertIs(ForwardRef("T").evaluate(owner=Gen), T)
267+
268+
with self.assertRaises(NameError):
269+
ForwardRef("alias").evaluate(type_params=Gen.__type_params__)
270+
self.assertIs(ForwardRef("alias").evaluate(owner=Gen), int)
271+
# If you pass custom locals, we don't look at the owner's locals
272+
with self.assertRaises(NameError):
273+
ForwardRef("alias").evaluate(owne 9E7A r=Gen, locals={})
274+
# But if the name exists in the locals, it works
275+
self.assertIs(
276+
ForwardRef("alias").evaluate(owner=Gen, locals={"alias": str}), str
277+
)
278+
279+
def test_fwdref_with_module(self):
280+
self.assertIs(ForwardRef("Format", module=annotationlib).evaluate(), Format)
281+
282+
with self.assertRaises(NameError):
283+
# If globals are passed explicitly, we don't look at the module dict
284+
ForwardRef("Format", module=annotationlib).evaluate(globals={})
285+
286+
def test_fwdref_value_is_cached(self):
287+
fr = ForwardRef("hello")
288+
with self.assertRaises(NameError):
289+
fr.evaluate()
290+
self.assertIs(fr.evaluate(globals={"hello": str}), str)
291+
self.assertIs(fr.evaluate(), str)
292+
253293

254294
class TestGetAnnotations(unittest.TestCase):
255295
def test_builtin_type(self):

Lib/typing.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,10 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, *, recursive_guard=f
474474
_deprecation_warning_for_no_type_params_passed("typing._eval_type")
475475
type_params = ()
476476
if isinstance(t, ForwardRef):
477+
# If the forward_ref has __forward_module__ set, evaluate() infers the globals
478+
# from the module, and it will probably pick better than the globals we have here.
479+
if t.__forward_module__ is not None:
480+
globalns = None
477481
return evaluate_forward_ref(t, globals=globalns, locals=localns,
478482
type_params=type_params, owner=owner,
479483
_recursive_guard=recursive_guard, format=format)

0 commit comments

Comments
 (0)
0