Skip to content

Commit 060ca69

Browse files
committed
pythongh-129463: pythongh-128593: Simplify ForwardRef
1 parent a472244 commit 060ca69

5 files changed

+52
-147
lines changed

Lib/annotationlib.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ class Format(enum.IntEnum):
3434
# preserved for compatibility with the old typing.ForwardRef class. The remaining
3535
# names are private.
3636
_SLOTS = (
37-
"__forward_evaluated__",
38-
"__forward_value__",
3937
"__forward_is_argument__",
4038
"__forward_is_class__",
4139
"__forward_module__",
@@ -78,8 +76,6 @@ def __init__(
7876
raise TypeError(f"Forward reference must be a string -- got {arg!r}")
7977

8078
self.__arg__ = arg
81-
self.__forward_evaluated__ = False
82-
self.__forward_value__ = None
8379
self.__forward_is_argument__ = is_argument
8480
self.__forward_is_class__ = is_class
8581
self.__forward_module__ = module
@@ -97,16 +93,12 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
9793
9894
If the forward reference cannot be evaluated, raise an exception.
9995
"""
100-
if self.__forward_evaluated__:
101-
return self.__forward_value__
10296
if self.__cell__ is not None:
10397
try:
10498
value = self.__cell__.cell_contents
10599
except ValueError:
106100
pass
107101
else:
108-
self.__forward_evaluated__ = True
109-
self.__forward_value__ = value
110102
return value
111103
if owner is None:
112104
owner = self.__owner__
@@ -173,8 +165,6 @@ def evaluate(self, *, globals=None, locals=None, type_params=None, owner=None):
173165
else:
174166
code = self.__forward_code__
175167
value = eval(code, globals=globals, locals=locals)
176-
self.__forward_evaluated__ = True
177-
self.__forward_value__ = value
178168
return value
179169

180170
def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard):
@@ -229,22 +219,6 @@ def __forward_code__(self):
229219
raise SyntaxError(f"Forward reference must be an expression -- got {arg!r}")
230220
return self.__code__
231221

232-
def __eq__(self, other):
233-
if not isinstance(other, ForwardRef):
234-
return NotImplemented
235-
if self.__forward_evaluated__ and other.__forward_evaluated__:
236-
return (
237-
self.__forward_arg__ == other.__forward_arg__
238-
and self.__forward_value__ == other.__forward_value__
239-
)
240-
return (
241-
self.__forward_arg__ == other.__forward_arg__
242-
and self.__forward_module__ == other.__forward_module__
243-
)
244-
245-
def __hash__(self):
246-
return hash((self.__forward_arg__, self.__forward_module__))
247-
248222
def __or__(self, other):
249223
global _Union
250224
if _Union is None:
@@ -284,8 +258,6 @@ def __init__(
284258
# represent a single name).
285259
assert isinstance(node, (ast.AST, str))
286260
self.__arg__ = None
287-
self.__forward_evaluated__ = False
288-
self.__forward_value__ = None
289261
self.__forward_is_argument__ = False
290262
self.__forward_is_class__ = is_class
291263
self.__forward_module__ = None

Lib/test/test_annotationlib.py

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ def wrapper(a, b):
3232
return wrapper
3333

3434

35+
def assert_is_fwdref(case, obj, value):
36+
case.assertIsInstance(obj, annotationlib.ForwardRef)
37+
case.assertEqual(obj.__forward_arg__, value)
38+
39+
3540
class MyClass:
3641
def __repr__(self):
3742
return "my repr"
@@ -59,8 +64,7 @@ def inner(arg: x):
5964

6065
anno = annotationlib.get_annotations(inner, format=Format.FORWARDREF)
6166
fwdref = anno["arg"]
62-
self.assertIsInstance(fwdref, annotationlib.ForwardRef)
63-
self.assertEqual(fwdref.__forward_arg__, "x")
67+
assert_is_fwdref(self, fwdref, "x")
6468
with self.assertRaises(NameError):
6569
fwdref.evaluate()
6670

@@ -77,8 +81,7 @@ def f(x: int, y: doesntexist):
7781
anno = annotationlib.get_annotations(f, format=Format.FORWARDREF)
7882
self.assertIs(anno["x"], int)
7983
fwdref = anno["y"]
80-
self.assertIsInstance(fwdref, annotationlib.ForwardRef)
81-
self.assertEqual(fwdref.__forward_arg__, "doesntexist")
84+
assert_is_fwdref(self, fwdref, "doesntexist")
8285
with self.assertRaises(NameError):
8386
fwdref.evaluate()
8487
self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1)
@@ -96,28 +99,22 @@ def f(
9699

97100
anno = annotationlib.get_annotations(f, format=Format.FORWARDREF)
98101
x_anno = anno["x"]
99-
self.assertIsInstance(x_anno, ForwardRef)
100-
self.assertEqual(x_anno, ForwardRef("some.module"))
102+
assert_is_fwdref(self, x_anno, "some.module")
101103

102104
y_anno = anno["y"]
103-
self.assertIsInstance(y_anno, ForwardRef)
104-
self.assertEqual(y_anno, ForwardRef("some[module]"))
105+
assert_is_fwdref(self, y_anno, "some[module]")
105106

106107
z_anno = anno["z"]
107-
self.assertIsInstance(z_anno, ForwardRef)
108-
self.assertEqual(z_anno, ForwardRef("some(module)"))
108+
assert_is_fwdref(self, z_anno, "some(module)")
109109

110110
alpha_anno = anno["alpha"]
111-
self.assertIsInstance(alpha_anno, ForwardRef)
112-
self.assertEqual(alpha_anno, ForwardRef("some | obj"))
111+
assert_is_fwdref(self, alpha_anno, "some | obj")
113112

114113
beta_anno = anno["beta"]
115-
self.assertIsInstance(beta_anno, ForwardRef)
116-
self.assertEqual(beta_anno, ForwardRef("+some"))
114+
assert_is_fwdref(self, beta_anno, "+some")
117115

118116
gamma_anno = anno["gamma"]
119-
self.assertIsInstance(gamma_anno, ForwardRef)
120-
self.assertEqual(gamma_anno, ForwardRef("some < obj"))
117+
assert_is_fwdref(self, gamma_anno, "some < obj")
121118

122119

123120
class TestSourceFormat(unittest.TestCase):
@@ -362,13 +359,6 @@ def test_fwdref_to_builtin(self):
362359
obj = object()
363360
self.assertIs(ForwardRef("int").evaluate(globals={"int": obj}), obj)
364361

365-
def test_fwdref_value_is_cached(self):
366-
fr = ForwardRef("hello")
367-
with self.assertRaises(NameError):
368-
fr.evaluate()
369-
self.assertIs(fr.evaluate(globals={"hello": str}), str)
370-
self.assertIs(fr.evaluate(), str)
371-
372362
def test_fwdref_with_owner(self):
373363
self.assertEqual(
374364
ForwardRef("Counter[int]", owner=collections).evaluate(),
@@ -457,12 +447,10 @@ def f2(a: undefined):
457447
)
458448
self.assertEqual(annotationlib.get_annotations(f1, format=1), {"a": int})
459449

460-
fwd = annotationlib.ForwardRef("undefined")
461-
self.assertEqual(
462-
annotationlib.get_annotations(f2, format=Format.FORWARDREF),
463-
{"a": fwd},
464-
)
465-
self.assertEqual(annotationlib.get_annotations(f2, format=3), {"a": fwd})
450+
for fmt in (Format.FORWARDREF, 3):
451+
annos = annotationlib.get_annotations(f2, format=fmt)
452+
self.assertEqual(list(annos), ["a"])
453+
assert_is_fwdref(self, annos["a"], "undefined")
466454

467455
self.assertEqual(
468456
annotationlib.get_annotations(f1, format=Format.STRING),
@@ -1012,10 +1000,10 @@ def evaluate(format, exc=NotImplementedError):
10121000

10131001
with self.assertRaises(NameError):
10141002
annotationlib.call_evaluate_function(evaluate, Format.VALUE)
1015-
self.assertEqual(
1016-
annotationlib.call_evaluate_function(evaluate, Format.FORWARDREF),
1017-
annotationlib.ForwardRef("undefined"),
1018-
)
1003+
1004+
fwdref = annotationlib.call_evaluate_function(evaluate, Format.FORWARDREF)
1005+
assert_is_fwdref(self, fwdref, "undefined")
1006+
10191007
self.assertEqual(
10201008
annotationlib.call_evaluate_function(evaluate, Format.STRING),
10211009
"undefined",

Lib/test/test_typing.py

Lines changed: 24 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ def wrapper(self):
7676
return wrapper
7777

7878

79+
class EqualToForwardRef:
80+
def __init__(self, arg, module=None):
81+
self.arg = arg
82+
self.module = module
83+
84+
def __eq__(self, other):
85+
if not isinstance(other, ForwardRef):
86+
return NotImplemented
87+
return self.arg == other.__forward_arg__ and self.module == other.__forward_module__
88+
89+
7990
class Employee:
8091
pass
8192

@@ -467,8 +478,8 @@ def test_or(self):
467478
self.assertEqual(X | "x", Union[X, "x"])
468479
self.assertEqual("x" | X, Union["x", X])
469480
# make sure the order is correct
470-
self.assertEqual(get_args(X | "x"), (X, ForwardRef("x")))
471-
self.assertEqual(get_args("x" | X), (ForwardRef("x"), X))
481+
self.assertEqual(get_args(X | "x"), (X, EqualToForwardRef("x")))
482+
self.assertEqual(get_args("x" | X), (EqualToForwardRef("x"), X))
472483

473484
def test_union_constrained(self):
474485
A = TypeVar('A', str, bytes)
@@ -4965,7 +4976,7 @@ class C3:
49654976
def f(x: X): ...
49664977
self.assertEqual(
49674978
get_type_hints(f, globals(), locals()),
4968-
{'x': list[list[ForwardRef('X')]]}
4979+
{'x': list[list[EqualToForwardRef('X')]]}
49694980
)
49704981

49714982
def test_pep695_generic_class_with_future_annotations(self):
@@ -5183,12 +5194,15 @@ class Node(Generic[T]): ...
51835194
Callable[..., T], Callable[[int], int],
51845195
Tuple[Any, Any], Node[T], Node[int], Node[Any], typing.Iterable[T],
51855196
typing.Iterable[Any], typing.Iterable[int], typing.Dict[int, str],
5186-
typing.Dict[T, Any], ClassVar[int], ClassVar[List[T]], Tuple['T', 'T'],
5187-
Union['T', int], List['T'], typing.Mapping['T', int]]
5197+
typing.Dict[T, Any], ClassVar[int], ClassVar[List[T]]]
51885198
for t in things + [Any]:
51895199
self.assertEqual(t, copy(t))
51905200
self.assertEqual(t, deepcopy(t))
51915201

5202+
shallow_things = [Tuple['T', 'T'], Union['T', int], List['T'], typing.Mapping['T', int]]
5203+
for t in things + [Any]:
5204+
self.assertEqual(t, copy(t))
5205+
51925206
def test_immutability_by_copy_and_pickle(self):
51935207
# Special forms like Union, Any, etc., generic aliases to containers like List,
51945208
# Mapping, etc., and type variabcles are considered immutable by copy and pickle.
@@ -6087,82 +6101,6 @@ def test_forwardref_only_str_arg(self):
60876101
with self.assertRaises(TypeError):
60886102
typing.ForwardRef(1) # only `str` type is allowed
60896103

6090-
def test_forward_equality(self):
6091-
fr = typing.ForwardRef('int')
6092-
self.assertEqual(fr, typing.ForwardRef('int'))
6093-
self.assertNotEqual(List['int'], List[int])
6094-
self.assertNotEqual(fr, typing.ForwardRef('int', module=__name__))
6095-
frm = typing.ForwardRef('int', module=__name__)
6096-
self.assertEqual(frm, typing.ForwardRef('int', module=__name__))
6097-
self.assertNotEqual(frm, typing.ForwardRef('int', module='__other_name__'))
6098-
6099-
def test_forward_equality_gth(self):
6100-
c1 = typing.ForwardRef('C')
6101-
c1_gth = typing.ForwardRef('C')
6102-
c2 = typing.ForwardRef('C')
6103-
c2_gth = typing.ForwardRef('C')
6104-
6105-
class C:
6106-
pass
6107-
def foo(a: c1_gth, b: c2_gth):
6108-
pass
6109-
6110-
self.assertEqual(get_type_hints(foo, globals(), locals()), {'a': C, 'b': C})
6111-
self.assertEqual(c1, c2)
6112-
self.assertEqual(c1, c1_gth)
6113-
self.assertEqual(c1_gth, c2_gth)
6114-
self.assertEqual(List[c1], List[c1_gth])
6115-
self.assertNotEqual(List[c1], List[C])
6116-
self.assertNotEqual(List[c1_gth], List[C])
6117-
self.assertEqual(Union[c1, c1_gth], Union[c1])
6118-
self.assertEqual(Union[c1, c1_gth, int], Union[c1, int])
6119-
6120-
def test_forward_equality_hash(self):
6121-
c1 = typing.ForwardRef('int')
6122-
c1_gth = typing.ForwardRef('int')
6123-
c2 = typing.ForwardRef('int')
6124-
c2_gth = typing.ForwardRef('int')
6125-
6126-
def foo(a: c1_gth, b: c2_gth):
6127-
pass
6128-
get_type_hints(foo, globals(), locals())
6129-
6130-
self.assertEqual(hash(c1), hash(c2))
6131-
self.assertEqual(hash(c1_gth), hash(c2_gth))
6132-
self.assertEqual(hash(c1), hash(c1_gth))
6133-
6134-
c3 = typing.ForwardRef('int', module=__name__)
6135-
c4 = typing.ForwardRef('int', module='__other_name__')
6136-
6137-
self.assertNotEqual(hash(c3), hash(c1))
6138-
self.assertNotEqual(hash(c3), hash(c1_gth))
6139-
self.assertNotEqual(hash(c3), hash(c4))
6140-
self.assertEqual(hash(c3), hash(typing.ForwardRef('int', module=__name__)))
6141-
6142-
def test_forward_equality_namespace(self):
6143-
class A:
6144-
pass
6145-
def namespace1():
6146-
a = typing.ForwardRef('A')
6147-
def fun(x: a):
6148-
pass
6149-
get_type_hints(fun, globals(), locals())
6150-
return a
6151-
6152-
def namespace2():
6153-
a = typing.ForwardRef('A')
6154-
6155-
class A:
6156-
pass
6157-
def fun(x: a):
6158-
pass
6159-
6160-
get_type_hints(fun, globals(), locals())
6161-
return a
6162-
6163-
self.assertEqual(namespace1(), namespace1())
6164-
self.assertNotEqual(namespace1(), namespace2())
6165-
61666104
def test_forward_repr(self):
61676105
self.assertEqual(repr(List['int']), "typing.List[ForwardRef('int')]")
61686106
self.assertEqual(repr(List[ForwardRef('int', module='mod')]),
@@ -6226,7 +6164,7 @@ def cmp(o1, o2):
62266164
r1 = namespace1()
62276165
r2 = namespace2()
62286166
self.assertIsNot(r1, r2)
6229-
self.assertRaises(RecursionError, cmp, r1, r2)
6167+
self.assertNotEqual(r1, r2)
62306168

62316169
def test_union_forward_recursion(self):
62326170
ValueList = List['Value']
@@ -7146,7 +7084,7 @@ def func(x: undefined) -> undefined: ...
71467084
# FORWARDREF
71477085
self.assertEqual(
71487086
get_type_hints(func, format=annotationlib.Format.FORWARDREF),
7149-
{'x': ForwardRef('undefined'), 'return': ForwardRef('undefined')},
7087+
{'x': EqualToForwardRef('undefined'), 'return': EqualToForwardRef('undefined')},
71507088
)
71517089

71527090
# STRING
@@ -8030,7 +7968,7 @@ class Y(NamedTuple):
80307968
class Z(NamedTuple):
80317969
a: None
80327970
b: "str"
8033-
annos = {'a': type(None), 'b': ForwardRef("str")}
7971+
annos = {'a': type(None), 'b': EqualToForwardRef("str")}
80347972
self.assertEqual(Z.__annotations__, annos)
80357973
self.assertEqual(Z.__annotate__(annotationlib.Format.VALUE), annos)
80367974
self.assertEqual(Z.__annotate__(annotationlib.Format.FORWARDREF), annos)
@@ -8046,7 +7984,7 @@ class X(NamedTuple):
80467984
"""
80477985
ns = run_code(textwrap.dedent(code))
80487986
X = ns['X']
8049-
self.assertEqual(X.__annotations__, {'a': ForwardRef("int"), 'b': ForwardRef("None")})
7987+
self.assertEqual(X.__annotations__, {'a': EqualToForwardRef("int"), 'b': EqualToForwardRef("None")})
80507988

80517989
def test_deferred_annotations(self):
80527990
class X(NamedTuple):
@@ -9032,7 +8970,7 @@ class X(TypedDict):
90328970
class Y(TypedDict):
90338971
a: None
90348972
b: "int"
9035-
fwdref = ForwardRef('int', module=__name__)
8973+
fwdref = EqualToForwardRef('int', module=__name__)
90368974
self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref})
90378975
self.assertEqual(Y.__annotate__(annotationlib.Format.FORWARDREF), {'a': type(None), 'b': fwdref})
90388976

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:class:`annotationlib.ForwardRef` objects no longer cache their value when
2+
they are successfully evaluated. Successive calls to
3+
:meth:`annotationlib.ForwardRef.evaluate` may return different values.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:class:`annotationlib.ForwardRef` objects no longer compare or hash equal
2+
when they refer to the same string. The implementation of equality was
3+
error-prone because it did not take all attributes of the
4+
:class:`!ForwardRef` object into account.

0 commit comments

Comments
 (0)