]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-137969: Fix double evaluation of `ForwardRef`s which rely on globals (GH...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Thu, 13 Nov 2025 21:26:58 +0000 (22:26 +0100)
committerGitHub <noreply@github.com>
Thu, 13 Nov 2025 21:26:58 +0000 (13:26 -0800)
gh-137969: Fix double evaluation of `ForwardRef`s which rely on globals (GH-140974)
(cherry picked from commit 209eaff68c3b241c01aece14182cb9ced51526fc)

Co-authored-by: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
Lib/annotationlib.py
Lib/test/test_annotationlib.py
Misc/NEWS.d/next/Library/2025-11-04-15-40-35.gh-issue-137969.9VZQVt.rst [new file with mode: 0644]

index 2166dbff0ee70cb5f8ed5773363dece5d7c3b581..33907b1fc2a53a5c2bb8337b3710219eb36acc28 100644 (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):
index 9f3275d50714844409159698a97e542971e970eb..8208d0e9c94819719948674352c64b119b3d18d2 100644 (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):
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 (file)
index 0000000..dfa582b
--- /dev/null
@@ -0,0 +1,3 @@
+Fix :meth:`annotationlib.ForwardRef.evaluate` returning
+:class:`~annotationlib.ForwardRef` objects which don't update with new
+globals.