[3.14] gh-138151: Fix annotationlib handling of multiple nonlocals (GH-138164) (#140949)

gh-138151: Fix annotationlib handling of multiple nonlocals (GH-138164)
(cherry picked from commit b1027d4762)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
Miss Islington (bot) 2025-11-03 16:52:01 +01:00 committed by GitHub
parent 788104633c
commit 08012a93a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 45 additions and 13 deletions

View file

@ -85,6 +85,9 @@ def __init__(
# These are always set to None here but may be non-None if a 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. # is created through __class__ assignment on a _Stringifier object.
self.__globals__ = None 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.__cell__ = None
self.__extra_names__ = None self.__extra_names__ = None
# These are initially None but serve as a cache and may be set to a non-None # These are initially None but serve as a cache and may be set to a non-None
@ -117,7 +120,7 @@ def evaluate(
is_forwardref_format = True is_forwardref_format = True
case _: case _:
raise NotImplementedError(format) raise NotImplementedError(format)
if self.__cell__ is not None: if isinstance(self.__cell__, types.CellType):
try: try:
return self.__cell__.cell_contents return self.__cell__.cell_contents
except ValueError: except ValueError:
@ -160,11 +163,18 @@ def evaluate(
# Type parameters exist in their own scope, which is logically # Type parameters exist in their own scope, which is logically
# between the locals and the globals. We simulate this by adding # between the locals and the globals. We simulate this by adding
# them to the globals. # them to the globals. Similar reasoning applies to nonlocals stored in cells.
if type_params is not None: if type_params is not None or isinstance(self.__cell__, dict):
globals = dict(globals) globals = dict(globals)
if type_params is not None:
for param in type_params: for param in type_params:
globals[param.__name__] = param 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__: if self.__extra_names__:
locals = {**locals, **self.__extra_names__} locals = {**locals, **self.__extra_names__}
@ -202,7 +212,7 @@ def evaluate(
except Exception: except Exception:
return self return self
else: else:
new_locals.transmogrify() new_locals.transmogrify(self.__cell__)
return result return result
def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard): def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard):
@ -274,7 +284,7 @@ def __hash__(self):
self.__forward_module__, self.__forward_module__,
id(self.__globals__), # dictionaries are not hashable, so hash by identity id(self.__globals__), # dictionaries are not hashable, so hash by identity
self.__forward_is_class__, self.__forward_is_class__,
self.__cell__, tuple(sorted(self.__cell__.items())) if isinstance(self.__cell__, dict) else self.__cell__,
self.__owner__, self.__owner__,
tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None, tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None,
)) ))
@ -642,13 +652,15 @@ def __missing__(self, key):
self.stringifiers.append(fwdref) self.stringifiers.append(fwdref)
return fwdref return fwdref
def transmogrify(self): def transmogrify(self, cell_dict):
for obj in self.stringifiers: for obj in self.stringifiers:
obj.__class__ = ForwardRef obj.__class__ = ForwardRef
obj.__stringifier_dict__ = None # not needed for ForwardRef obj.__stringifier_dict__ = None # not needed for ForwardRef
if isinstance(obj.__ast_node__, str): if isinstance(obj.__ast_node__, str):
obj.__arg__ = obj.__ast_node__ obj.__arg__ = obj.__ast_node__
obj.__ast_node__ = None obj.__ast_node__ = None
if cell_dict is not None and obj.__cell__ is None:
obj.__cell__ = cell_dict
def create_unique_name(self): def create_unique_name(self):
name = f"__annotationlib_name_{self.next_id}__" 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) globals = _StringifierDict({}, format=format)
is_class = isinstance(owner, type) is_class = isinstance(owner, type)
closure = _build_closure( closure, _ = _build_closure(
annotate, owner, is_class, globals, allow_evaluation=False annotate, owner, is_class, globals, allow_evaluation=False
) )
func = types.FunctionType( func = types.FunctionType(
@ -756,7 +768,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
is_class=is_class, is_class=is_class,
format=format, format=format,
) )
closure = _build_closure( closure, cell_dict = _build_closure(
annotate, owner, is_class, globals, allow_evaluation=True annotate, owner, is_class, globals, allow_evaluation=True
) )
func = types.FunctionType( func = types.FunctionType(
@ -774,7 +786,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
except Exception: except Exception:
pass pass
else: else:
globals.transmogrify() globals.transmogrify(cell_dict)
return result return result
# Try again, but do not provide any globals. This allows us to return # 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, is_class=is_class,
format=format, format=format,
) )
closure = _build_closure( closure, cell_dict = _build_closure(
annotate, owner, is_class, globals, allow_evaluation=False annotate, owner, is_class, globals, allow_evaluation=False
) )
func = types.FunctionType( func = types.FunctionType(
@ -797,7 +809,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
kwdefaults=annotate.__kwdefaults__, kwdefaults=annotate.__kwdefaults__,
) )
result = func(Format.VALUE_WITH_FAKE_GLOBALS) result = func(Format.VALUE_WITH_FAKE_GLOBALS)
globals.transmogrify() globals.transmogrify(cell_dict)
if _is_evaluate: if _is_evaluate:
if isinstance(result, ForwardRef): if isinstance(result, ForwardRef):
return result.evaluate(format=Format.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): def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluation):
if not annotate.__closure__: if not annotate.__closure__:
return None return None, None
freevars = annotate.__code__.co_freevars freevars = annotate.__code__.co_freevars
new_closure = [] new_closure = []
cell_dict = {}
for i, cell in enumerate(annotate.__closure__): for i, cell in enumerate(annotate.__closure__):
if i < len(freevars): if i < len(freevars):
name = freevars[i] name = freevars[i]
else: else:
name = "__cell__" name = "__cell__"
cell_dict[name] = cell
new_cell = None new_cell = None
if allow_evaluation: if allow_evaluation:
try: try:
@ -850,7 +864,7 @@ def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluat
stringifier_dict.stringifiers.append(fwdref) stringifier_dict.stringifiers.append(fwdref)
new_cell = types.CellType(fwdref) new_cell = types.CellType(fwdref)
new_closure.append(new_cell) new_closure.append(new_cell)
return tuple(new_closure) return tuple(new_closure), cell_dict
def _stringify_single(anno): def _stringify_single(anno):

View file

@ -1194,6 +1194,21 @@ class RaisesAttributeError:
}, },
) )
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): def test_raises_error_from_value(self):
# test that if VALUE is the only supported format, but raises an error # test that if VALUE is the only supported format, but raises an error
# that error is propagated from get_annotations # that error is propagated from get_annotations

View file

@ -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.