Skip to content

Commit 2e0d445

Browse files
gh-119180: Fix annotationlib.ForwardRef.evaluate with no globals (#124326)
We were sometimes passing None as the globals argument to eval(), which makes it inherit the globals from the calling scope. Instead, ensure that globals is always non-None. The test was passing accidentally because I passed "annotationlib" as a module object; fix that. Also document the parameters to ForwardRef() and remove two unused private ones. Co-authored-by: Alex Waygood <[email protected]>
1 parent e7d465a commit 2e0d445

File tree

2 files changed

+35
-15
lines changed

2 files changed

+35
-15
lines changed

Lib/annotationlib.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@ class Format(enum.IntEnum):
4545

4646

4747
class ForwardRef:
48-
"""Wrapper that holds a forward reference."""
48+
"""Wrapper that holds a forward reference.
49+
50+
Constructor arguments:
51+
* arg: a string representing the code to be evaluated.
52+
* module: the module where the forward reference was created.
53+
Must be a string, not a module object.
54+
* owner: The owning object (module, class, or function).
55+
* is_argument: Does nothing, retained for compatibility.
56+
* is_class: True if the forward reference was created in class scope.
57+
58+
"""
4959

5060
__slots__ = _SLOTS
5161

@@ -57,8 +67,6 @@ def __init__(
5767
owner=None,
5868
is_argument=True,
5969
is_class=False,
60-
_globals=None,
61-
_cell=None,
6270
):
6371
if not isinstance(arg, str):
6472
raise TypeError(f"Forward reference must be a string -- got {arg!r}")
@@ -71,8 +79,8 @@ def __init__(
7179
self.__forward_module__ = module
7280
self.__code__ = None
7381
self.__ast_node__ = None
74-
self.__globals__ = _globals
75-
self.__cell__ = _cell
82+
self.__globals__ = None
83+
self.__cell__ = None
7684
self.__owner__ = owner
7785

7886
def __init_subclass__(cls, /, *args, **kwds):
@@ -115,6 +123,10 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
115123
elif callable(owner):
116124
globals = getattr(owner, "__globals__", None)
117125

126+
# If we pass None to eval() below, the globals of this module are used.
127+
if globals is None:
128+
globals = {}
129+
118130
if locals is None:
119131
locals = {}
120132
if isinstance(owner, type):
@@ -134,14 +146,8 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
134146
# but should in turn be overridden by names in the class scope
135147
# (which here are called `globalns`!)
136148
if type_params is not None:
137-
if globals is None:
138-
globals = {}
139-
else:
140-
globals = dict(globals)
141-
if locals is None:
142-
locals = {}
143-
else:
144-
locals = dict(locals)
149+
globals = dict(globals)
150+
locals = dict(locals)
145151
for param in type_params:
146152
param_name = param.__name__
147153
if not self.__forward_is_class__ or param_name not in globals:

Lib/test/test_annotationlib.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for the annotations module."""
22

33
import annotationlib
4+
import collections
45
import functools
56
import itertools
67
import pickle
@@ -278,11 +279,24 @@ class Gen[T]:
278279
)
279280

280281
def test_fwdref_with_module(self):
281-
self.assertIs(ForwardRef("Format", module=annotationlib).evaluate(), Format)
282+
self.assertIs(ForwardRef("Format", module="annotationlib").evaluate(), Format)
283+
self.assertIs(ForwardRef("Counter", module="collections").evaluate(), collections.Counter)
282284

283285
with self.assertRaises(NameError):
284286
# If globals are passed explicitly, we don't look at the module dict
285-
ForwardRef("Format", module=annotationlib).evaluate(globals={})
287+
ForwardRef("Format", module="annotationlib").evaluate(globals={})
288+
289+
def test_fwdref_to_builtin(self):
290+
self.assertIs(ForwardRef("int").evaluate(), int)
291+
self.assertIs(ForwardRef("int", module="collections").evaluate(), int)
292+
self.assertIs(ForwardRef("int", owner=str).evaluate(), int)
293+
294+
# builtins are still searched with explicit globals
295+
self.assertIs(ForwardRef("int").evaluate(globals={}), int)
296+
297+
# explicit values in globals have precedence
298+
obj = object()
299+
self.assertIs(ForwardRef("int").evaluate(globals={"int": obj}), obj)
286300

287301
def test_fwdref_value_is_cached(self):
288302
fr = ForwardRef("hello")

0 commit comments

Comments
 (0)