https://github.com/python/cpython/commit/209eaff68c3b241c01aece14182cb9ced51526fc
commit: 209eaff68c3b241c01aece14182cb9ced51526fc
branch: main
author: dr-carlos <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-11-13T18:17:17Z
summary:
gh-137969: Fix double evaluation of `ForwardRef`s which rely on globals
(#140974)
files:
A Misc/NEWS.d/next/Library/2025-11-04-15-40-35.gh-issue-137969.9VZQVt.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index 2166dbff0ee70c..33907b1fc2a53a 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -150,33 +150,42 @@ def evaluate(
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):
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index 9f3275d5071484..8208d0e9c94819 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -2149,6 +2149,51 @@ def test_fwdref_invalid_syntax(self):
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):
diff --git
a/Misc/NEWS.d/next/Library/2025-11-04-15-40-35.gh-issue-137969.9VZQVt.rst
b/Misc/NEWS.d/next/Library/2025-11-04-15-40-35.gh-issue-137969.9VZQVt.rst
new file mode 100644
index 00000000000000..dfa582bdbc8825
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-04-15-40-35.gh-issue-137969.9VZQVt.rst
@@ -0,0 +1,3 @@
+Fix :meth:`annotationlib.ForwardRef.evaluate` returning
+:class:`~annotationlib.ForwardRef` objects which don't update with new
+globals.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]