https://github.com/python/cpython/commit/b1027d4762435b97546c122dd94290d707b3ff39
commit: b1027d4762435b97546c122dd94290d707b3ff39
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-11-03T07:22:32-08:00
summary:
gh-138151: Fix annotationlib handling of multiple nonlocals (#138164)
files:
A Misc/NEWS.d/next/Library/2025-08-26-08-17-56.gh-issue-138151.I6CdAk.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index 16dbb128bc9293..2166dbff0ee70c 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -85,6 +85,9 @@ def __init__(
# 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 @@ def evaluate(
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 @@ def evaluate(
# 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 @@ def evaluate(
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 @@ def __hash__(self):
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 @@ def __missing__(self, key):
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 7b08f58bfb8ba2..fd5d43b09b9702 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -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):
# 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 00000000000000..de29f536afc95e
--- /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.
_______________________________________________
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]