From: Jelle Zijlstra Date: Mon, 3 Nov 2025 15:22:32 +0000 (-0800) Subject: gh-138151: Fix annotationlib handling of multiple nonlocals (#138164) X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=b1027d4762435b97546c122dd94290d707b3ff39;p=thirdparty%2FPython%2Fcpython.git gh-138151: Fix annotationlib handling of multiple nonlocals (#138164) --- diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 16dbb128bc92..2166dbff0ee7 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -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): diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 7b08f58bfb8b..fd5d43b09b97 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -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 index 000000000000..de29f536afc9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-26-08-17-56.gh-issue-138151.I6CdAk.rst @@ -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.