mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
gh-137969: Fix double evaluation of ForwardRefs which rely on globals (#140974)
This commit is contained in:
parent
4fa80ce74c
commit
209eaff68c
3 changed files with 72 additions and 15 deletions
|
|
@ -150,33 +150,42 @@ def evaluate(
|
||||||
if globals is None:
|
if globals is None:
|
||||||
globals = {}
|
globals = {}
|
||||||
|
|
||||||
|
if type_params is None and owner is not None:
|
||||||
|
type_params = getattr(owner, "__type_params__", None)
|
||||||
|
|
||||||
if locals is None:
|
if locals is None:
|
||||||
locals = {}
|
locals = {}
|
||||||
if isinstance(owner, type):
|
if isinstance(owner, type):
|
||||||
locals.update(vars(owner))
|
locals.update(vars(owner))
|
||||||
|
elif (
|
||||||
|
type_params is not None
|
||||||
|
or isinstance(self.__cell__, dict)
|
||||||
|
or self.__extra_names__
|
||||||
|
):
|
||||||
|
# Create a new locals dict if necessary,
|
||||||
|
# to avoid mutating the argument.
|
||||||
|
locals = dict(locals)
|
||||||
|
|
||||||
if type_params is None and owner is not None:
|
|
||||||
# "Inject" type parameters into the local namespace
|
# "Inject" type parameters into the local namespace
|
||||||
# (unless they are shadowed by assignments *in* the local namespace),
|
# (unless they are shadowed by assignments *in* the local namespace),
|
||||||
# as a way of emulating annotation scopes when calling `eval()`
|
# as a way of emulating annotation scopes when calling `eval()`
|
||||||
type_params = getattr(owner, "__type_params__", None)
|
|
||||||
|
|
||||||
# Type parameters exist in their own scope, which is logically
|
|
||||||
# between the locals and the globals. We simulate this by adding
|
|
||||||
# them to the globals. Similar reasoning applies to nonlocals stored in cells.
|
|
||||||
if type_params is not None or isinstance(self.__cell__, dict):
|
|
||||||
globals = dict(globals)
|
|
||||||
if type_params is not None:
|
if type_params is not None:
|
||||||
for param in type_params:
|
for param in type_params:
|
||||||
globals[param.__name__] = param
|
locals.setdefault(param.__name__, param)
|
||||||
|
|
||||||
|
# Similar logic can be used for nonlocals, which should not
|
||||||
|
# override locals.
|
||||||
if isinstance(self.__cell__, dict):
|
if isinstance(self.__cell__, dict):
|
||||||
for cell_name, cell_value in self.__cell__.items():
|
for cell_name, cell in self.__cell__.items():
|
||||||
try:
|
try:
|
||||||
globals[cell_name] = cell_value.cell_contents
|
cell_value = cell.cell_contents
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
locals.setdefault(cell_name, cell_value)
|
||||||
|
|
||||||
if self.__extra_names__:
|
if self.__extra_names__:
|
||||||
locals = {**locals, **self.__extra_names__}
|
locals.update(self.__extra_names__)
|
||||||
|
|
||||||
arg = self.__forward_arg__
|
arg = self.__forward_arg__
|
||||||
if arg.isidentifier() and not keyword.iskeyword(arg):
|
if arg.isidentifier() and not keyword.iskeyword(arg):
|
||||||
|
|
|
||||||
|
|
@ -2149,6 +2149,51 @@ def test_fwdref_invalid_syntax(self):
|
||||||
with self.assertRaises(SyntaxError):
|
with self.assertRaises(SyntaxError):
|
||||||
fr.evaluate()
|
fr.evaluate()
|
||||||
|
|
||||||
|
def test_re_evaluate_generics(self):
|
||||||
|
global global_alias
|
||||||
|
|
||||||
|
# If we've already run this test before,
|
||||||
|
# ensure the variable is still undefined
|
||||||
|
if "global_alias" in globals():
|
||||||
|
del global_alias
|
||||||
|
|
||||||
|
class C:
|
||||||
|
x: global_alias[int]
|
||||||
|
|
||||||
|
# Evaluate the ForwardRef once
|
||||||
|
evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(
|
||||||
|
format=Format.FORWARDREF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now define the global and ensure that the ForwardRef evaluates
|
||||||
|
global_alias = list
|
||||||
|
self.assertEqual(evaluated.evaluate(), list[int])
|
||||||
|
|
||||||
|
def test_fwdref_evaluate_argument_mutation(self):
|
||||||
|
class C[T]:
|
||||||
|
nonlocal alias
|
||||||
|
x: alias[T]
|
||||||
|
|
||||||
|
# Mutable arguments
|
||||||
|
globals_ = globals()
|
||||||
|
globals_copy = globals_.copy()
|
||||||
|
locals_ = locals()
|
||||||
|
locals_copy = locals_.copy()
|
||||||
|
|
||||||
|
# Evaluate the ForwardRef, ensuring we use __cell__ and type params
|
||||||
|
get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(
|
||||||
|
globals=globals_,
|
||||||
|
locals=locals_,
|
||||||
|
type_params=C.__type_params__,
|
||||||
|
format=Format.FORWARDREF,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the passed in mutable arguments equal the originals
|
||||||
|
self.assertEqual(globals_, globals_copy)
|
||||||
|
self.assertEqual(locals_, locals_copy)
|
||||||
|
|
||||||
|
alias = list
|
||||||
|
|
||||||
def test_fwdref_final_class(self):
|
def test_fwdref_final_class(self):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
class C(ForwardRef):
|
class C(ForwardRef):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
Fix :meth:`annotationlib.ForwardRef.evaluate` returning
|
||||||
|
:class:`~annotationlib.ForwardRef` objects which don't update with new
|
||||||
|
globals.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue