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
24 changes: 21 additions & 3 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,16 @@ def evaluate(
is_forwardref_format = True
case _:
raise NotImplementedError(format)
if isinstance(self.__cell__, types.CellType):
if (
globals is None
and locals is None
and isinstance(self.__cell__, types.CellType)
):
# The cell holds the value of the name from the enclosing scope.
# Take this shortcut only if no namespaces were provided;
# otherwise, the resolution below gives an explicitly passed
# locals precedence over the cell, and the cell precedence
# over globals.
try:
return self.__cell__.cell_contents
except ValueError:
Expand Down Expand Up @@ -194,9 +203,18 @@ def evaluate(
resolved = _sentinel
if arg in locals:
resolved = locals[arg]
elif arg in globals:
elif isinstance(self.__cell__, types.CellType):
# The cell holds the value of the name from the enclosing
# scope. Like the dict-of-cells path above, which merges
# cells into locals with setdefault, it takes precedence
# over globals, but not over locals.
try:
resolved = self.__cell__.cell_contents
except ValueError:
pass
if resolved is _sentinel and arg in globals:
resolved = globals[arg]
elif hasattr(builtins, arg):
elif resolved is _sentinel and hasattr(builtins, arg):
resolved = getattr(builtins, arg)

if resolved is not _sentinel:
Expand Down
57 changes: 57 additions & 0 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2525,6 +2525,63 @@ class C[T]:

alias = list

def test_evaluate_cell_with_explicit_namespaces(self):
def inner(arg: x):
pass

fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"]
self.assertIsInstance(fwdref, ForwardRef)
self.assertIsInstance(fwdref.__cell__, types.CellType)

x = 1
# With no explicit namespaces, the value from the enclosing
# scope, held in the cell, is used.
self.assertEqual(fwdref.evaluate(), 1)
# An explicitly passed locals takes precedence over the cell,
# but globals does not: the cell holds a nonlocal binding, which
# sits between locals and globals in the lookup order (as in
# the dict-of-cells path, where the cells are merged into the
# locals with setdefault).
self.assertEqual(fwdref.evaluate(locals={"x": 2}), 2)
self.assertEqual(fwdref.evaluate(globals={"x": 3}), 1)
self.assertEqual(fwdref.evaluate(globals={"x": 3}, locals={"x": 2}), 2)
self.assertEqual(
fwdref.evaluate(locals={"x": 2}, format=Format.FORWARDREF), 2
)
self.assertEqual(
fwdref.evaluate(globals={"x": 3}, format=Format.FORWARDREF), 1
)
# If the name is not in the explicit namespaces, the cell
# value is used as a fallback.
self.assertEqual(fwdref.evaluate(locals={}), 1)
self.assertEqual(fwdref.evaluate(globals={}), 1)
self.assertEqual(fwdref.evaluate(locals={"y": 2}), 1)

def test_evaluate_empty_cell_with_explicit_namespaces(self):
def inner(arg: x):
pass

fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"]
self.assertIsInstance(fwdref, ForwardRef)
self.assertIsInstance(fwdref.__cell__, types.CellType)

x = 1
del x
# The cell is empty, so the name cannot be resolved without
# explicit namespaces.
with self.assertRaises(NameError):
fwdref.evaluate()
# Explicitly passed namespaces are still consulted.
self.assertEqual(fwdref.evaluate(locals={"x": 2}), 2)
self.assertEqual(fwdref.evaluate(globals={"x": 3}), 3)

# Refilling the cell restores it: an explicitly passed locals
# still takes precedence over it, but globals does not.
x = 7
self.assertEqual(fwdref.evaluate(), 7)
self.assertEqual(fwdref.evaluate(locals={"x": 2}), 2)
self.assertEqual(fwdref.evaluate(globals={"x": 3}), 7)

def test_fwdref_final_class(self):
with self.assertRaises(TypeError):
class C(ForwardRef):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fixed :meth:`annotationlib.ForwardRef.evaluate` silently ignoring an
explicitly passed *locals* when the reference holds a single closure
cell. An explicit *locals* now takes precedence over the cell, as on
the other resolution paths; the cell still takes precedence over
*globals*, since it holds a nonlocal binding.
Loading