[3.14] gh-137969: Fix double evaluation of ForwardRefs which rely on globals (GH-140974) (#141527)
Some checks failed
Tests / (push) Blocked by required conditions
Tests / Sanitizers (push) Blocked by required conditions
Tests / Change detection (push) Waiting to run
Tests / Docs (push) Blocked by required conditions
Tests / Windows MSI (push) Blocked by required conditions
Tests / Check if the ABI has changed (push) Blocked by required conditions
Tests / Check if Autoconf files are up to date (push) Blocked by required conditions
Tests / Check if generated files are up to date (push) Blocked by required conditions
Tests / Ubuntu SSL tests with OpenSSL (push) Blocked by required conditions
Tests / Android (aarch64) (push) Blocked by required conditions
Tests / Android (x86_64) (push) Blocked by required conditions
Tests / iOS (push) Blocked by required conditions
Tests / WASI (push) Blocked by required conditions
Tests / Hypothesis tests on Ubuntu (push) Blocked by required conditions
Tests / Address sanitizer (push) Blocked by required conditions
Tests / Cross build Linux (push) Blocked by required conditions
Tests / CIFuzz (push) Blocked by required conditions
Tests / All required checks pass (push) Blocked by required conditions
Lint / lint (push) Waiting to run
JIT / Interpreter (Debug) (push) Has been cancelled
Tail calling interpreter / aarch64-apple-darwin/clang (push) Has been cancelled
Tail calling interpreter / aarch64-unknown-linux-gnu/gcc (push) Has been cancelled
Tail calling interpreter / x86_64-pc-windows-msvc/msvc (push) Has been cancelled
Tail calling interpreter / x86_64-apple-darwin/clang (push) Has been cancelled
Tail calling interpreter / free-threading (push) Has been cancelled
Tail calling interpreter / x86_64-unknown-linux-gnu/gcc (push) Has been cancelled
JIT / aarch64-pc-windows-msvc/msvc (Release) (push) Has been cancelled
JIT / aarch64-pc-windows-msvc/msvc (Debug) (push) Has been cancelled
JIT / i686-pc-windows-msvc/msvc (Release) (push) Has been cancelled
JIT / i686-pc-windows-msvc/msvc (Debug) (push) Has been cancelled
JIT / aarch64-apple-darwin/clang (Release) (push) Has been cancelled
JIT / aarch64-unknown-linux-gnu/gcc (Release) (push) Has been cancelled
JIT / aarch64-apple-darwin/clang (Debug) (push) Has been cancelled
JIT / aarch64-unknown-linux-gnu/gcc (Debug) (push) Has been cancelled
JIT / x86_64-pc-windows-msvc/msvc (Release) (push) Has been cancelled
JIT / x86_64-pc-windows-msvc/msvc (Debug) (push) Has been cancelled
JIT / x86_64-apple-darwin/clang (Release) (push) Has been cancelled
JIT / x86_64-unknown-linux-gnu/gcc (Release) (push) Has been cancelled
JIT / x86_64-apple-darwin/clang (Debug) (push) Has been cancelled
JIT / x86_64-unknown-linux-gnu/gcc (Debug) (push) Has been cancelled
JIT / JIT with tail calling interpreter (push) Has been cancelled

gh-137969: Fix double evaluation of `ForwardRef`s which rely on globals (GH-140974)
(cherry picked from commit 209eaff68c)

Co-authored-by: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
This commit is contained in:
Miss Islington (bot) 2025-11-13 22:26:58 +01:00 committed by GitHub
parent af20b880fc
commit dd28db664b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 72 additions and 15 deletions

View file

@ -150,33 +150,42 @@ class ForwardRef:
if globals is None:
globals = {}
if type_params is None and owner is not None:
type_params = getattr(owner, "__type_params__", None)
if locals is None:
locals = {}
if isinstance(owner, type):
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
# (unless they are shadowed by assignments *in* the local namespace),
# 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)
# "Inject" type parameters into the local namespace
# (unless they are shadowed by assignments *in* the local namespace),
# as a way of emulating annotation scopes when calling `eval()`
if type_params is not None:
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):
for cell_name, cell_value in self.__cell__.items():
for cell_name, cell in self.__cell__.items():
try:
globals[cell_name] = cell_value.cell_contents
cell_value = cell.cell_contents
except ValueError:
pass
else:
locals.setdefault(cell_name, cell_value)
if self.__extra_names__:
locals = {**locals, **self.__extra_names__}
locals.update(self.__extra_names__)
arg = self.__forward_arg__
if arg.isidentifier() and not keyword.iskeyword(arg):

View file

@ -2149,6 +2149,51 @@ class TestForwardRefClass(unittest.TestCase):
with self.assertRaises(SyntaxError):
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):
with self.assertRaises(TypeError):
class C(ForwardRef):

View file

@ -0,0 +1,3 @@
Fix :meth:`annotationlib.ForwardRef.evaluate` returning
:class:`~annotationlib.ForwardRef` objects which don't update with new
globals.