Skip to content

Commit 209eaff

Browse files
authored
pythongh-137969: Fix double evaluation of ForwardRefs which rely on globals (python#140974)
1 parent 4fa80ce commit 209eaff

File tree

3 files changed

+72
-15
lines changed

3 files changed

+72
-15
lines changed

Lib/annotationlib.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -150,33 +150,42 @@ def evaluate(
150150
if globals is None:
151151
globals = {}
152152

153+
if type_params is None and owner is not None:
154+
type_params = getattr(owner, "__type_params__", None)
155+
153156
if locals is None:
154157
locals = {}
155158
if isinstance(owner, type):
156159
locals.update(vars(owner))
160+
elif (
161+
type_params is not None
162+
or isinstance(self.__cell__, dict)
163+
or self.__extra_names__
164+
):
165+
# Create a new locals dict if necessary,
166+
# to avoid mutating the argument.
167+
locals = dict(locals)
157168

158-
if type_params is None and owner is not None:
159-
# "Inject" type parameters into the local namespace
160-
# (unless they are shadowed by assignments *in* the local namespace),
161-
# as a way of emulating annotation scopes when calling `eval()`
162-
type_params = getattr(owner, "__type_params__", None)
163-
164-
# Type parameters exist in their own scope, which is logically
165-
# between the locals and the globals. We simulate this by adding
166-
# them to the globals. Similar reasoning applies to nonlocals stored in cells.
167-
if type_params is not None or isinstance(self.__cell__, dict):
168-
globals = dict(globals)
169+
# "Inject" type parameters into the local namespace
170+
# (unless they are shadowed by assignments *in* the local namespace),
171+
# as a way of emulating annotation scopes when calling `eval()`
169172
if type_params is not None:
170173
for param in type_params:
171-
globals[param.__name__] = param
174+
locals.setdefault(param.__name__, param)
175+
176+
# Similar logic can be used for nonlocals, which should not
177+
# override locals.
172178
if isinstance(self.__cell__, dict):
173-
for cell_name, cell_value in self.__cell__.items():
179+
for cell_name, cell in self.__cell__.items():
174180
try:
175-
globals[cell_name] = cell_value.cell_contents
181+
cell_value = cell.cell_contents
176182
except ValueError:
177183
pass
184+
else:
185+
locals.setdefault(cell_name, cell_value)
186+
178187
if self.__extra_names__:
179-
locals = {**locals, **self.__extra_names__}
188+
locals.update(self.__extra_names__)
180189

181190
arg = self.__forward_arg__
182191
if arg.isidentifier() and not keyword.iskeyword(arg):

Lib/test/test_annotationlib.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,6 +2149,51 @@ def test_fwdref_invalid_syntax(self):
21492149
with self.assertRaises(SyntaxError):
21502150
fr.evaluate()
21512151

2152+
def test_re_evaluate_generics(self):
2153+
global global_alias
2154+
2155+
# If we've already run this test before,
2156+
# ensure the variable is still undefined
2157+
if "global_alias" in globals():
2158+
del global_alias
2159+
2160+
class C:
2161+
x: global_alias[int]
2162+
2163+
# Evaluate the ForwardRef once
2164+
evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(
2165+
format=Format.FORWARDREF
2166+
)
2167+
2168+
# Now define the global and ensure that the ForwardRef evaluates
2169+
global_alias = list
2170+
self.assertEqual(evaluated.evaluate(), list[int])
2171+
2172+
def test_fwdref_evaluate_argument_mutation(self):
2173+
class C[T]:
2174+
nonlocal alias
2175+
x: alias[T]
2176+
2177+
# Mutable arguments
2178+
globals_ = globals()
2179+
globals_copy = globals_.copy()
2180+
locals_ = locals()
2181+
locals_copy = locals_.copy()
2182+
2183+
# Evaluate the ForwardRef, ensuring we use __cell__ and type params
2184+
get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(
2185+
globals=globals_,
2186+
locals=locals_,
2187+
type_params=C.__type_params__,
2188+
format=Format.FORWARDREF,
2189+
)
2190+
2191+
# Check if the passed in mutable arguments equal the originals
2192+
self.assertEqual(globals_, globals_copy)
2193+
self.assertEqual(locals_, locals_copy)
2194+
2195+
alias = list
2196+
21522197
def test_fwdref_final_class(self):
21532198
with self.assertRaises(TypeError):
21542199
class C(ForwardRef):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :meth:`annotationlib.ForwardRef.evaluate` returning
2+
:class:`~annotationlib.ForwardRef` objects which don't update with new
3+
globals.

0 commit comments

Comments
 (0)