Skip to content

Commit ac14d4a

Browse files
gh-129463, gh-128593: Simplify ForwardRef (#129465)
1 parent 231a50f commit ac14d4a

11 files changed

+124
-69
lines changed

Doc/library/annotationlib.rst

-6
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,6 @@ Classes
204204
means may not have any information about their scope, so passing
205205
arguments to this method may be necessary to evaluate them successfully.
206206

207-
.. important::
208-
209-
Once a :class:`~ForwardRef` instance has been evaluated, it caches
210-
the evaluated value, and future calls to :meth:`evaluate` will return
211-
the cached value, regardless of the parameters passed in.
212-
213207
.. versionadded:: 3.14
214208

215209

Doc/library/typing.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -3449,7 +3449,9 @@ Introspection helpers
34493449
.. versionadded:: 3.7.4
34503450

34513451
.. versionchanged:: 3.14
3452-
This is now an alias for :class:`annotationlib.ForwardRef`.
3452+
This is now an alias for :class:`annotationlib.ForwardRef`. Several undocumented
3453+
behaviors of this class have been changed; for example, after a ``ForwardRef`` has
3454+
been evaluated, the evaluated value is no longer cached.
34533455

34543456
.. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=annotationlib.Format.VALUE)
34553457

Doc/whatsnew/3.14.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ This example shows how these formats behave:
209209
...
210210
NameError: name 'Undefined' is not defined
211211
>>> get_annotations(func, format=Format.FORWARDREF)
212-
{'arg': ForwardRef('Undefined')}
212+
{'arg': ForwardRef('Undefined', owner=<function func at 0x...>)}
213213
>>> get_annotations(func, format=Format.STRING)
214214
{'arg': 'Undefined'}
215215

Lib/annotationlib.py

+31-30
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,16 @@ class Format(enum.IntEnum):
3232
# preserved for compatibility with the old typing.ForwardRef class. The remaining
3333
# names are private.
3434
_SLOTS = (
35-
"__forward_evaluated__",
36-
"__forward_value__",
3735
"__forward_is_argument__",
3836
"__forward_is_class__",
3937
"__forward_module__",
4038
"__weakref__",
4139
"__arg__",
42-
"__ast_node__",
43-
"__code__",
4440
"__globals__",
45-
"__owner__",
41+
"__code__",
42+
"__ast_node__",
4643
"__cell__",
44+
"__owner__",
4745
"__stringifier_dict__",
4846
)
4947

@@ -76,14 +74,12 @@ def __init__(
7674
raise TypeError(f"Forward reference must be a string -- got {arg!r}")
7775

7876
self.__arg__ = arg
79-
self.__forward_evaluated__ = False
80-
self.__forward_value__ = None
8177
self.__forward_is_argument__ = is_argument
8278
self.__forward_is_class__ = is_class
8379
self.__forward_module__ = module
80+
self.__globals__ = None
8481
self.__code__ = None
8582
self.__ast_node__ = None
86-
self.__globals__ = None
8783
self.__cell__ = None
8884
self.__owner__ = owner
8985

@@ -95,17 +91,11 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
9591
9692
If the forward reference cannot be evaluated, raise an exception.
9793
"""
98-
if self.__forward_evaluated__:
99-
return self.__forward_value__
10094
if self.__cell__ is not None:
10195
try:
102-
value = self.__cell__.cell_contents
96+
return self.__cell__.cell_contents
10397
except ValueError:
10498
pass
105-
else:
106-
self.__forward_evaluated__ = True
107-
self.__forward_value__ = value
108-
return value
10999
if owner is None:
110100
owner = self.__owner__
111101

@@ -171,8 +161,6 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
171161
else:
172162
code = self.__forward_code__
173163
value = eval(code, globals=globals, locals=locals)
174-
self.__forward_evaluated__ = True
175-
self.__forward_value__ = value
176164
return value
177165

178166
def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard):
@@ -230,18 +218,30 @@ def __forward_code__(self):
230218
def __eq__(self, other):
231219
if not isinstance(other, ForwardRef):
232220
return NotImplemented
233-
if self.__forward_evaluated__ and other.__forward_evaluated__:
234-
return (
235-
self.__forward_arg__ == other.__forward_arg__
236-
and self.__forward_value__ == other.__forward_value__
237-
)
238221
return (
239222
self.__forward_arg__ == other.__forward_arg__
240223
and self.__forward_module__ == other.__forward_module__
224+
# Use "is" here because we use id() for this in __hash__
225+
# because dictionaries are not hashable.
226+
and self.__globals__ is other.__globals__
227+
and self.__forward_is_class__ == other.__forward_is_class__
228+
and self.__code__ == other.__code__
229+
and self.__ast_node__ == other.__ast_node__
230+
and self.__cell__ == other.__cell__
231+
and self.__owner__ == other.__owner__
241232
)
242233

243234
def __hash__(self):
244-
return hash((self.__forward_arg__, self.__forward_module__))
235+
return hash((
236+
self.__forward_arg__,
237+
self.__forward_module__,
238+
id(self.__globals__), # dictionaries are not hashable, so hash by identity
239+
self.__forward_is_class__,
240+
self.__code__,
241+
self.__ast_node__,
242+
self.__cell__,
243+
self.__owner__,
244+
))
245245

246246
def __or__(self, other):
247247
return types.UnionType[self, other]
@@ -250,11 +250,14 @@ def __ror__(self, other):
250250
return types.UnionType[other, self]
251251

252252
def __repr__(self):
253-
if self.__forward_module__ is None:
254-
module_repr = ""
255-
else:
256-
module_repr = f", module={self.__forward_module__!r}"
257-
return f"ForwardRef({self.__forward_arg__!r}{module_repr})"
253+
extra = []
254+
if self.__forward_module__ is not None:
255+
extra.append(f", module={self.__forward_module__!r}")
256+
if self.__forward_is_class__:
257+
extra.append(", is_class=True")
258+
if self.__owner__ is not None:
259+
extra.append(f", owner={self.__owner__!r}")
260+
return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
258261

259262

260263
class _Stringifier:
@@ -276,8 +279,6 @@ def __init__(
276279
# represent a single name).
277280
assert isinstance(node, (ast.AST, str))
278281
self.__arg__ = None
279-
self.__forward_evaluated__ = False
280-
self.__forward_value__ = None
281282
self.__forward_is_argument__ = False
282283
self.__forward_is_class__ = is_class
283284
self.__forward_module__ = None

Lib/test/support/__init__.py

+42
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
if __name__ != 'test.support':
44
raise ImportError('support must be imported from the test package')
55

6+
import annotationlib
67
import contextlib
78
import functools
89
import inspect
@@ -3021,6 +3022,47 @@ def is_libssl_fips_mode():
30213022
return get_fips_mode() != 0
30223023

30233024

3025+
class EqualToForwardRef:
3026+
"""Helper to ease use of annotationlib.ForwardRef in tests.
3027+
3028+
This checks only attributes that can be set using the constructor.
3029+
3030+
"""
3031+
3032+
def __init__(
3033+
self,
3034+
arg,
3035+
*,
3036+
module=None,
3037+
owner=None,
3038+
is_class=False,
3039+
):
3040+
self.__forward_arg__ = arg
3041+
self.__forward_is_class__ = is_class
3042+
self.__forward_module__ = module
3043+
self.__owner__ = owner
3044+
3045+
def __eq__(self, other):
3046+
if not isinstance(other, (EqualToForwardRef, annotationlib.ForwardRef)):
3047+
return NotImplemented
3048+
return (
3049+
self.__forward_arg__ == other.__forward_arg__
3050+
and self.__forward_module__ == other.__forward_module__
3051+
and self.__forward_is_class__ == other.__forward_is_class__
3052+
and self.__owner__ == other.__owner__
3053+
)
3054+
3055+
def __repr__(self):
3056+
extra = []
3057+
if self.__forward_module__ is not None:
3058+
extra.append(f", module={self.__forward_module__!r}")
3059+
if self.__forward_is_class__:
3060+
extra.append(", is_class=True")
3061+
if self.__owner__ is not None:
3062+
extra.append(f", owner={self.__owner__!r}")
3063+
return f"EqualToForwardRef({self.__forward_arg__!r}{''.join(extra)})"
3064+
3065+
30243066
_linked_to_musl = None
30253067
def linked_to_musl():
30263068
"""

Lib/test/test_annotationlib.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -97,27 +97,27 @@ def f(
9797
anno = annotationlib.get_annotations(f, format=Format.FORWARDREF)
9898
x_anno = anno["x"]
9999
self.assertIsInstance(x_anno, ForwardRef)
100-
self.assertEqual(x_anno, ForwardRef("some.module"))
100+
self.assertEqual(x_anno, support.EqualToForwardRef("some.module", owner=f))
101101

102102
y_anno = anno["y"]
103103
self.assertIsInstance(y_anno, ForwardRef)
104-
self.assertEqual(y_anno, ForwardRef("some[module]"))
104+
self.assertEqual(y_anno, support.EqualToForwardRef("some[module]", owner=f))
105105

106106
z_anno = anno["z"]
107107
self.assertIsInstance(z_anno, ForwardRef)
108-
self.assertEqual(z_anno, ForwardRef("some(module)"))
108+
self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f))
109109

110110
alpha_anno = anno["alpha"]
111111
self.assertIsInstance(alpha_anno, ForwardRef)
112-
self.assertEqual(alpha_anno, ForwardRef("some | obj"))
112+
self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f))
113113

114114
beta_anno = anno["beta"]
115115
self.assertIsInstance(beta_anno, ForwardRef)
116-
self.assertEqual(beta_anno, ForwardRef("+some"))
116+
self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f))
117117

118118
gamma_anno = anno["gamma"]
119119
self.assertIsInstance(gamma_anno, ForwardRef)
120-
self.assertEqual(gamma_anno, ForwardRef("some < obj"))
120+
self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f))
121121

122122

123123
class TestSourceFormat(unittest.TestCase):
@@ -362,12 +362,13 @@ def test_fwdref_to_builtin(self):
362362
obj = object()
363363
self.assertIs(ForwardRef("int").evaluate(globals={"int": obj}), obj)
364364

365-
def test_fwdref_value_is_cached(self):
365+
def test_fwdref_value_is_not_cached(self):
366366
fr = ForwardRef("hello")
367367
with self.assertRaises(NameError):
368368
fr.evaluate()
369369
self.assertIs(fr.evaluate(globals={"hello": str}), str)
370-
self.assertIs(fr.evaluate(), str)
370+
with self.assertRaises(NameError):
371+
fr.evaluate()
371372

372373
def test_fwdref_with_owner(self):
373374
self.assertEqual(
@@ -457,7 +458,7 @@ def f2(a: undefined):
457458
)
458459
self.assertEqual(annotationlib.get_annotations(f1, format=1), {"a": int})
459460

460-
fwd = annotationlib.ForwardRef("undefined")
461+
fwd = support.EqualToForwardRef("undefined", owner=f2)
461462
self.assertEqual(
462463
annotationlib.get_annotations(f2, format=Format.FORWARDREF),
463464
{"a": fwd},
@@ -1014,7 +1015,7 @@ def evaluate(format, exc=NotImplementedError):
10141015
annotationlib.call_evaluate_function(evaluate, Format.VALUE)
10151016
self.assertEqual(
10161017
annotationlib.call_evaluate_function(evaluate, Format.FORWARDREF),
1017-
annotationlib.ForwardRef("undefined"),
1018+
support.EqualToForwardRef("undefined"),
10181019
)
10191020
self.assertEqual(
10201021
annotationlib.call_evaluate_function(evaluate, Format.STRING),

Lib/test/test_inspect/test_inspect.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
from test.support import cpython_only, import_helper
3939
from test.support import MISSING_C_DOCSTRINGS, ALWAYS_EQ
40-
from test.support import run_no_yield_async_fn
40+
from test.support import run_no_yield_async_fn, EqualToForwardRef
4141
from test.support.import_helper import DirsOnSysPath, ready_to_import
4242
from test.support.os_helper import TESTFN, temp_cwd
4343
from test.support.script_helper import assert_python_ok, assert_python_failure, kill_python
@@ -4940,9 +4940,12 @@ def test_signature_annotation_format(self):
49404940
signature_func(ida.f, annotation_format=Format.STRING),
49414941
sig([par("x", PORK, annotation="undefined")])
49424942
)
4943+
s1 = signature_func(ida.f, annotation_format=Format.FORWARDREF)
4944+
s2 = sig([par("x", PORK, annotation=EqualToForwardRef("undefined", owner=ida.f))])
4945+
#breakpoint()
49434946
self.assertEqual(
49444947
signature_func(ida.f, annotation_format=Format.FORWARDREF),
4945-
sig([par("x", PORK, annotation=ForwardRef("undefined"))])
4948+
sig([par("x", PORK, annotation=EqualToForwardRef("undefined", owner=ida.f))])
49464949
)
49474950
with self.assertRaisesRegex(NameError, "undefined"):
49484951
signature_func(ida.f, annotation_format=Format.VALUE)

Lib/test/test_types.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from test.support import (
44
run_with_locale, cpython_only, no_rerun,
5-
MISSING_C_DOCSTRINGS,
5+
MISSING_C_DOCSTRINGS, EqualToForwardRef,
66
)
77
import collections.abc
88
from collections import namedtuple, UserDict
@@ -1089,7 +1089,13 @@ def test_instantiation(self):
10891089
self.assertIs(int, types.UnionType[int])
10901090
self.assertIs(int, types.UnionType[int, int])
10911091
self.assertEqual(int | str, types.UnionType[int, str])
1092-
self.assertEqual(int | typing.ForwardRef("str"), types.UnionType[int, "str"])
1092+
1093+
for obj in (
1094+
int | typing.ForwardRef("str"),
1095+
typing.Union[int, "str"],
1096+
):
1097+
self.assertIsInstance(obj, types.UnionType)
1098+
self.assertEqual(obj.__args__, (int, EqualToForwardRef("str")))
10931099

10941100

10951101
class MappingProxyTests(unittest.TestCase):

0 commit comments

Comments
 (0)