mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
[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:
parent
788104633c
commit
08012a93a8
3 changed files with 45 additions and 13 deletions
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue