]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-138151: Fix annotationlib handling of multiple nonlocals (#138164)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Mon, 3 Nov 2025 15:22:32 +0000 (07:22 -0800)
committerGitHub <noreply@github.com>
Mon, 3 Nov 2025 15:22:32 +0000 (07:22 -0800)
Lib/annotationlib.py
Lib/test/test_annotationlib.py
Misc/NEWS.d/next/Library/2025-08-26-08-17-56.gh-issue-138151.I6CdAk.rst [new file with mode: 0644]

index 16dbb128bc9293e06989e0c62da52d8f0019df4f..2166dbff0ee70cb5f8ed5773363dece5d7c3b581 100644 (file)
@@ -85,6 +85,9 @@ class ForwardRef:
         # These are always set to None here but may be non-None if a ForwardRef
         # is created through __class__ assignment on a _Stringifier object.
         self.__globals__ = None
+        # This may be either a cell object (for a ForwardRef referring to a single name)
+        # or a dict mapping cell names to cell objects (for a ForwardRef containing references
+        # to multiple names).
         self.__cell__ = None
         self.__extra_names__ = None
         # These are initially None but serve as a cache and may be set to a non-None
@@ -117,7 +120,7 @@ class ForwardRef:
                 is_forwardref_format = True
             case _:
                 raise NotImplementedError(format)
-        if self.__cell__ is not None:
+        if isinstance(self.__cell__, types.CellType):
             try:
                 return self.__cell__.cell_contents
             except ValueError:
@@ -160,11 +163,18 @@ class ForwardRef:
 
         # 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.
-        if type_params is not None:
+        # 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:
             for param in type_params:
                 globals[param.__name__] = param
+        if isinstance(self.__cell__, dict):
+            for cell_name, cell_value in self.__cell__.items():
+                try:
+                    globals[cell_name] = cell_value.cell_contents
+                except ValueError:
+                    pass
         if self.__extra_names__:
             locals = {**locals, **self.__extra_names__}
 
@@ -202,7 +212,7 @@ class ForwardRef:
             except Exception:
                 return self
             else:
-                new_locals.transmogrify()
+                new_locals.transmogrify(self.__cell__)
                 return result
 
     def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard):
@@ -274,7 +284,7 @@ class ForwardRef:
             self.__forward_module__,
             id(self.__globals__),  # dictionaries are not hashable, so hash by identity
             self.__forward_is_class__,
-            self.__cell__,
+            tuple(sorted(self.__cell__.items())) if isinstance(self.__cell__, dict) else self.__cell__,
             self.__owner__,
             tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None,
         ))
@@ -642,13 +652,15 @@ class _StringifierDict(dict):
         self.stringifiers.append(fwdref)
         return fwdref
 
-    def transmogrify(self):
+    def transmogrify(self, cell_dict):
         for obj in self.stringifiers:
             obj.__class__ = ForwardRef
             obj.__stringifier_dict__ = None  # not needed for ForwardRef
             if isinstance(obj.__ast_node__, str):
                 obj.__arg__ = obj.__ast_node__
                 obj.__ast_node__ = None
+            if cell_dict is not None and obj.__cell__ is None:
+                obj.__cell__ = cell_dict
 
     def create_unique_name(self):
         name = f"__annotationlib_name_{self.next_id}__"
@@ -712,7 +724,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
 
         globals = _StringifierDict({}, format=format)
         is_class = isinstance(owner, type)
-        closure = _build_closure(
+        closure, _ = _build_closure(
             annotate, owner, is_class, globals, allow_evaluation=False
         )
         func = types.FunctionType(
@@ -756,7 +768,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
             is_class=is_class,
             format=format,
         )
-        closure = _build_closure(
+        closure, cell_dict = _build_closure(
             annotate, owner, is_class, globals, allow_evaluation=True
         )
         func = types.FunctionType(
@@ -774,7 +786,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
         except Exception:
             pass
         else:
-            globals.transmogrify()
+            globals.transmogrify(cell_dict)
             return result
 
         # Try again, but do not provide any globals. This allows us to return
@@ -786,7 +798,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
             is_class=is_class,
             format=format,
         )
-        closure = _build_closure(
+        closure, cell_dict = _build_closure(
             annotate, owner, is_class, globals, allow_evaluation=False
         )
         func = types.FunctionType(
@@ -797,7 +809,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
             kwdefaults=annotate.__kwdefaults__,
         )
         result = func(Format.VALUE_WITH_FAKE_GLOBALS)
-        globals.transmogrify()
+        globals.transmogrify(cell_dict)
         if _is_evaluate:
             if isinstance(result, ForwardRef):
                 return result.evaluate(format=Format.FORWARDREF)
@@ -822,14 +834,16 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
 
 def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluation):
     if not annotate.__closure__:
-        return None
+        return None, None
     freevars = annotate.__code__.co_freevars
     new_closure = []
+    cell_dict = {}
     for i, cell in enumerate(annotate.__closure__):
         if i < len(freevars):
             name = freevars[i]
         else:
             name = "__cell__"
+        cell_dict[name] = cell
         new_cell = None
         if allow_evaluation:
             try:
@@ -850,7 +864,7 @@ def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluat
             stringifier_dict.stringifiers.append(fwdref)
             new_cell = types.CellType(fwdref)
         new_closure.append(new_cell)
-    return tuple(new_closure)
+    return tuple(new_closure), cell_dict
 
 
 def _stringify_single(anno):
index 7b08f58bfb8ba210be90cec9a29b092509935fb9..fd5d43b09b9702a797566cd11681f79f991a8495 100644 (file)
@@ -1194,6 +1194,21 @@ class TestGetAnnotations(unittest.TestCase):
             },
         )
 
+    def test_nonlocal_in_annotation_scope(self):
+        class Demo:
+            nonlocal sequence_b
+            x: sequence_b
+            y: sequence_b[int]
+
+        fwdrefs = get_annotations(Demo, format=Format.FORWARDREF)
+
+        self.assertIsInstance(fwdrefs["x"], ForwardRef)
+        self.assertIsInstance(fwdrefs["y"], ForwardRef)
+
+        sequence_b = list
+        self.assertIs(fwdrefs["x"].evaluate(), list)
+        self.assertEqual(fwdrefs["y"].evaluate(), list[int])
+
     def test_raises_error_from_value(self):
         # test that if VALUE is the only supported format, but raises an error
         # that error is propagated from get_annotations
diff --git a/Misc/NEWS.d/next/Library/2025-08-26-08-17-56.gh-issue-138151.I6CdAk.rst b/Misc/NEWS.d/next/Library/2025-08-26-08-17-56.gh-issue-138151.I6CdAk.rst
new file mode 100644 (file)
index 0000000..de29f53
--- /dev/null
@@ -0,0 +1,3 @@
+In :mod:`annotationlib`, improve evaluation of forward references to
+nonlocal variables that are not yet defined when the annotations are
+initially evaluated.