gh-138151: Fix annotationlib handling of multiple nonlocals (#138164)

This commit is contained in:
Jelle Zijlstra 2025-11-03 07:22:32 -08:00 committed by GitHub
parent d590685297
commit b1027d4762
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
# 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):