Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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__}

Expand Down Expand Up @@ -199,7 +209,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):
Expand Down Expand Up @@ -278,7 +288,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,
))
Expand Down Expand Up @@ -608,13 +618,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}__"
Expand Down Expand Up @@ -666,7 +678,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
# original source.
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(
Expand Down Expand Up @@ -710,7 +722,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(
Expand All @@ -725,7 +737,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
Expand All @@ -737,7 +749,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(
Expand All @@ -748,7 +760,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)
Expand All @@ -773,14 +785,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:
Expand All @@ -801,7 +815,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):
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,20 @@ 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])

class TestCallEvaluateFunction(unittest.TestCase):
def test_evaluation(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -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