Skip to content

Commit 73556fa

Browse files
gh-160948: Only an explicitly passed locals takes precedence over the closure cell
1 parent 42ae372 commit 73556fa

3 files changed

Lines changed: 28 additions & 16 deletions

File tree

‎Lib/annotationlib.py‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,10 @@ def evaluate(
128128
and isinstance(self.__cell__, types.CellType)
129129
):
130130
# The cell holds the value of the name from the enclosing scope.
131-
# Explicitly passed namespaces take precedence over it, so use
132-
# this shortcut only if neither was provided; otherwise, the
133-
# cell value is used as a fallback during name resolution below.
131+
# Take this shortcut only if no namespaces were provided;
132+
# otherwise, the resolution below gives an explicitly passed
133+
# locals precedence over the cell, and the cell precedence
134+
# over globals.
134135
try:
135136
return self.__cell__.cell_contents
136137
except ValueError:
@@ -202,16 +203,18 @@ def evaluate(
202203
resolved = _sentinel
203204
if arg in locals:
204205
resolved = locals[arg]
205-
elif arg in globals:
206-
resolved = globals[arg]
207206
elif isinstance(self.__cell__, types.CellType):
208-
# Fall back to the value from the enclosing scope if the
209-
# name is not in either of the provided namespaces.
207+
# The cell holds the value of the name from the enclosing
208+
# scope. Like the dict-of-cells path above, which merges
209+
# cells into locals with setdefault, it takes precedence
210+
# over globals, but not over locals.
210211
try:
211212
resolved = self.__cell__.cell_contents
212213
except ValueError:
213214
pass
214-
if resolved is _sentinel and hasattr(builtins, arg):
215+
if resolved is _sentinel and arg in globals:
216+
resolved = globals[arg]
217+
elif resolved is _sentinel and hasattr(builtins, arg):
215218
resolved = getattr(builtins, arg)
216219

217220
if resolved is not _sentinel:

‎Lib/test/test_annotationlib.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2537,13 +2537,20 @@ def inner(arg: x):
25372537
# With no explicit namespaces, the value from the enclosing
25382538
# scope, held in the cell, is used.
25392539
self.assertEqual(fwdref.evaluate(), 1)
2540-
# Explicitly passed namespaces take precedence over the cell.
2540+
# An explicitly passed locals takes precedence over the cell,
2541+
# but globals does not: the cell holds a nonlocal binding, which
2542+
# sits between locals and globals in the lookup order (as in
2543+
# the dict-of-cells path, where the cells are merged into the
2544+
# locals with setdefault).
25412545
self.assertEqual(fwdref.evaluate(locals={"x": 2}), 2)
2542-
self.assertEqual(fwdref.evaluate(globals={"x": 3}), 3)
2546+
self.assertEqual(fwdref.evaluate(globals={"x": 3}), 1)
25432547
self.assertEqual(fwdref.evaluate(globals={"x": 3}, locals={"x": 2}), 2)
25442548
self.assertEqual(
25452549
fwdref.evaluate(locals={"x": 2}, format=Format.FORWARDREF), 2
25462550
)
2551+
self.assertEqual(
2552+
fwdref.evaluate(globals={"x": 3}, format=Format.FORWARDREF), 1
2553+
)
25472554
# If the name is not in the explicit namespaces, the cell
25482555
# value is used as a fallback.
25492556
self.assertEqual(fwdref.evaluate(locals={}), 1)
@@ -2568,11 +2575,12 @@ def inner(arg: x):
25682575
self.assertEqual(fwdref.evaluate(locals={"x": 2}), 2)
25692576
self.assertEqual(fwdref.evaluate(globals={"x": 3}), 3)
25702577

2571-
# Refilling the cell restores it as the fallback, but explicit
2572-
# namespaces still take precedence over it.
2578+
# Refilling the cell restores it: an explicitly passed locals
2579+
# still takes precedence over it, but globals does not.
25732580
x = 7
25742581
self.assertEqual(fwdref.evaluate(), 7)
25752582
self.assertEqual(fwdref.evaluate(locals={"x": 2}), 2)
2583+
self.assertEqual(fwdref.evaluate(globals={"x": 3}), 7)
25762584

25772585
def test_fwdref_final_class(self):
25782586
with self.assertRaises(TypeError):
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
Fixed :meth:`annotationlib.ForwardRef.evaluate` silently ignoring
2-
explicitly passed *globals* and *locals* when the reference holds a
3-
single closure cell. Explicit namespaces now take precedence, as on the
4-
other resolution paths, and the cell value is used as a fallback.
1+
Fixed :meth:`annotationlib.ForwardRef.evaluate` silently ignoring an
2+
explicitly passed *locals* when the reference holds a single closure
3+
cell. An explicit *locals* now takes precedence over the cell, as on
4+
the other resolution paths; the cell still takes precedence over
5+
*globals*, since it holds a nonlocal binding.

0 commit comments

Comments
 (0)