gh-129463, gh-128593: Simplify ForwardRef (#129465)

This commit is contained in:
Jelle Zijlstra 2025-04-04 21:36:34 -07:00 committed by GitHub
parent 231a50fa9a
commit ac14d4a23f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 124 additions and 69 deletions

View file

@ -204,12 +204,6 @@ Classes
means may not have any information about their scope, so passing means may not have any information about their scope, so passing
arguments to this method may be necessary to evaluate them successfully. arguments to this method may be necessary to evaluate them successfully.
.. important::
Once a :class:`~ForwardRef` instance has been evaluated, it caches
the evaluated value, and future calls to :meth:`evaluate` will return
the cached value, regardless of the parameters passed in.
.. versionadded:: 3.14 .. versionadded:: 3.14

View file

@ -3449,7 +3449,9 @@ Introspection helpers
.. versionadded:: 3.7.4 .. versionadded:: 3.7.4
.. versionchanged:: 3.14 .. versionchanged:: 3.14
This is now an alias for :class:`annotationlib.ForwardRef`. This is now an alias for :class:`annotationlib.ForwardRef`. Several undocumented
behaviors of this class have been changed; for example, after a ``ForwardRef`` has
been evaluated, the evaluated value is no longer cached.
.. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=annotationlib.Format.VALUE) .. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=annotationlib.Format.VALUE)

View file

@ -209,7 +209,7 @@ This example shows how these formats behave:
... ...
NameError: name 'Undefined' is not defined NameError: name 'Undefined' is not defined
>>> get_annotations(func, format=Format.FORWARDREF) >>> get_annotations(func, format=Format.FORWARDREF)
{'arg': ForwardRef('Undefined')} {'arg': ForwardRef('Undefined', owner=<function func at 0x...>)}
>>> get_annotations(func, format=Format.STRING) >>> get_annotations(func, format=Format.STRING)
{'arg': 'Undefined'} {'arg': 'Undefined'}

View file

@ -32,18 +32,16 @@ _sentinel = object()
# preserved for compatibility with the old typing.ForwardRef class. The remaining # preserved for compatibility with the old typing.ForwardRef class. The remaining
# names are private. # names are private.
_SLOTS = ( _SLOTS = (
"__forward_evaluated__",
"__forward_value__",
"__forward_is_argument__", "__forward_is_argument__",
"__forward_is_class__", "__forward_is_class__",
"__forward_module__", "__forward_module__",
"__weakref__", "__weakref__",
"__arg__", "__arg__",
"__ast_node__",
"__code__",
"__globals__", "__globals__",
"__owner__", "__code__",
"__ast_node__",
"__cell__", "__cell__",
"__owner__",
"__stringifier_dict__", "__stringifier_dict__",
) )
@ -76,14 +74,12 @@ class ForwardRef:
raise TypeError(f"Forward reference must be a string -- got {arg!r}") raise TypeError(f"Forward reference must be a string -- got {arg!r}")
self.__arg__ = arg self.__arg__ = arg
self.__forward_evaluated__ = False
self.__forward_value__ = None
self.__forward_is_argument__ = is_argument self.__forward_is_argument__ = is_argument
self.__forward_is_class__ = is_class self.__forward_is_class__ = is_class
self.__forward_module__ = module self.__forward_module__ = module
self.__globals__ = None
self.__code__ = None self.__code__ = None
self.__ast_node__ = None self.__ast_node__ = None
self.__globals__ = None
self.__cell__ = None self.__cell__ = None
self.__owner__ = owner self.__owner__ = owner
@ -95,17 +91,11 @@ class ForwardRef:
If the forward reference cannot be evaluated, raise an exception. If the forward reference cannot be evaluated, raise an exception.
""" """
if self.__forward_evaluated__:
return self.__forward_value__
if self.__cell__ is not None: if self.__cell__ is not None:
try: try:
value = self.__cell__.cell_contents return self.__cell__.cell_contents
except ValueError: except ValueError:
pass pass
else:
self.__forward_evaluated__ = True
self.__forward_value__ = value
return value
if owner is None: if owner is None:
owner = self.__owner__ owner = self.__owner__
@ -171,8 +161,6 @@ class ForwardRef:
else: else:
code = self.__forward_code__ code = self.__forward_code__
value = eval(code, globals=globals, locals=locals) value = eval(code, globals=globals, locals=locals)
self.__forward_evaluated__ = True
self.__forward_value__ = value
return value return value
def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard): def _evaluate(self, globalns, localns, type_params=_sentinel, *, recursive_guard):
@ -230,18 +218,30 @@ class ForwardRef:
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, ForwardRef): if not isinstance(other, ForwardRef):
return NotImplemented return NotImplemented
if self.__forward_evaluated__ and other.__forward_evaluated__:
return (
self.__forward_arg__ == other.__forward_arg__
and self.__forward_value__ == other.__forward_value__
)
return ( return (
self.__forward_arg__ == other.__forward_arg__ self.__forward_arg__ == other.__forward_arg__
and self.__forward_module__ == other.__forward_module__ and self.__forward_module__ == other.__forward_module__
# Use "is" here because we use id() for this in __hash__
# because dictionaries are not hashable.
and self.__globals__ is other.__globals__
and self.__forward_is_class__ == other.__forward_is_class__
and self.__code__ == other.__code__
and self.__ast_node__ == other.__ast_node__
and self.__cell__ == other.__cell__
and self.__owner__ == other.__owner__
) )
def __hash__(self): def __hash__(self):
return hash((self.__forward_arg__, self.__forward_module__)) return hash((
self.__forward_arg__,
self.__forward_module__,
id(self.__globals__), # dictionaries are not hashable, so hash by identity
self.__forward_is_class__,
self.__code__,
self.__ast_node__,
self.__cell__,
self.__owner__,
))
def __or__(self, other): def __or__(self, other):
return types.UnionType[self, other] return types.UnionType[self, other]
@ -250,11 +250,14 @@ class ForwardRef:
return types.UnionType[other, self] return types.UnionType[other, self]
def __repr__(self): def __repr__(self):
if self.__forward_module__ is None: extra = []
module_repr = "" if self.__forward_module__ is not None:
else: extra.append(f", module={self.__forward_module__!r}")
module_repr = f", module={self.__forward_module__!r}" if self.__forward_is_class__:
return f"ForwardRef({self.__forward_arg__!r}{module_repr})" extra.append(", is_class=True")
if self.__owner__ is not None:
extra.append(f", owner={self.__owner__!r}")
return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
class _Stringifier: class _Stringifier:
@ -276,8 +279,6 @@ class _Stringifier:
# represent a single name). # represent a single name).
assert isinstance(node, (ast.AST, str)) assert isinstance(node, (ast.AST, str))
self.__arg__ = None self.__arg__ = None
self.__forward_evaluated__ = False
self.__forward_value__ = None
self.__forward_is_argument__ = False self.__forward_is_argument__ = False
self.__forward_is_class__ = is_class self.__forward_is_class__ = is_class
self.__forward_module__ = None self.__forward_module__ = None

View file

@ -3,6 +3,7 @@
if __name__ != 'test.support': if __name__ != 'test.support':
raise ImportError('support must be imported from the test package') raise ImportError('support must be imported from the test package')
import annotationlib
import contextlib import contextlib
import functools import functools
import inspect import inspect
@ -3021,6 +3022,47 @@ def is_libssl_fips_mode():
return get_fips_mode() != 0 return get_fips_mode() != 0
class EqualToForwardRef:
"""Helper to ease use of annotationlib.ForwardRef in tests.
This checks only attributes that can be set using the constructor.
"""
def __init__(
self,
arg,
*,
module=None,
owner=None,
is_class=False,
):
self.__forward_arg__ = arg
self.__forward_is_class__ = is_class
self.__forward_module__ = module
self.__owner__ = owner
def __eq__(self, other):
if not isinstance(other, (EqualToForwardRef, annotationlib.ForwardRef)):
return NotImplemented
return (
self.__forward_arg__ == other.__forward_arg__
and self.__forward_module__ == other.__forward_module__
and self.__forward_is_class__ == other.__forward_is_class__
and self.__owner__ == other.__owner__
)
def __repr__(self):
extra = []
if self.__forward_module__ is not None:
extra.append(f", module={self.__forward_module__!r}")
if self.__forward_is_class__:
extra.append(", is_class=True")
if self.__owner__ is not None:
extra.append(f", owner={self.__owner__!r}")
return f"EqualToForwardRef({self.__forward_arg__!r}{''.join(extra)})"
_linked_to_musl = None _linked_to_musl = None
def linked_to_musl(): def linked_to_musl():
""" """

View file

@ -97,27 +97,27 @@ class TestForwardRefFormat(unittest.TestCase):
anno = annotationlib.get_annotations(f, format=Format.FORWARDREF) anno = annotationlib.get_annotations(f, format=Format.FORWARDREF)
x_anno = anno["x"] x_anno = anno["x"]
self.assertIsInstance(x_anno, ForwardRef) self.assertIsInstance(x_anno, ForwardRef)
self.assertEqual(x_anno, ForwardRef("some.module")) self.assertEqual(x_anno, support.EqualToForwardRef("some.module", owner=f))
y_anno = anno["y"] y_anno = anno["y"]
self.assertIsInstance(y_anno, ForwardRef) self.assertIsInstance(y_anno, ForwardRef)
self.assertEqual(y_anno, ForwardRef("some[module]")) self.assertEqual(y_anno, support.EqualToForwardRef("some[module]", owner=f))
z_anno = anno["z"] z_anno = anno["z"]
self.assertIsInstance(z_anno, ForwardRef) self.assertIsInstance(z_anno, ForwardRef)
self.assertEqual(z_anno, ForwardRef("some(module)")) self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f))
alpha_anno = anno["alpha"] alpha_anno = anno["alpha"]
self.assertIsInstance(alpha_anno, ForwardRef) self.assertIsInstance(alpha_anno, ForwardRef)
self.assertEqual(alpha_anno, ForwardRef("some | obj")) self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f))
beta_anno = anno["beta"] beta_anno = anno["beta"]
self.assertIsInstance(beta_anno, ForwardRef) self.assertIsInstance(beta_anno, ForwardRef)
self.assertEqual(beta_anno, ForwardRef("+some")) self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f))
gamma_anno = anno["gamma"] gamma_anno = anno["gamma"]
self.assertIsInstance(gamma_anno, ForwardRef) self.assertIsInstance(gamma_anno, ForwardRef)
self.assertEqual(gamma_anno, ForwardRef("some < obj")) self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f))
class TestSourceFormat(unittest.TestCase): class TestSourceFormat(unittest.TestCase):
@ -362,12 +362,13 @@ class TestForwardRefClass(unittest.TestCase):
obj = object() obj = object()
self.assertIs(ForwardRef("int").evaluate(globals={"int": obj}), obj) self.assertIs(ForwardRef("int").evaluate(globals={"int": obj}), obj)
def test_fwdref_value_is_cached(self): def test_fwdref_value_is_not_cached(self):
fr = ForwardRef("hello") fr = ForwardRef("hello")
with self.assertRaises(NameError): with self.assertRaises(NameError):
fr.evaluate() fr.evaluate()
self.assertIs(fr.evaluate(globals={"hello": str}), str) self.assertIs(fr.evaluate(globals={"hello": str}), str)
self.assertIs(fr.evaluate(), str) with self.assertRaises(NameError):
fr.evaluate()
def test_fwdref_with_owner(self): def test_fwdref_with_owner(self):
self.assertEqual( self.assertEqual(
@ -457,7 +458,7 @@ class TestGetAnnotations(unittest.TestCase):
) )
self.assertEqual(annotationlib.get_annotations(f1, format=1), {"a": int}) self.assertEqual(annotationlib.get_annotations(f1, format=1), {"a": int})
fwd = annotationlib.ForwardRef("undefined") fwd = support.EqualToForwardRef("undefined", owner=f2)
self.assertEqual( self.assertEqual(
annotationlib.get_annotations(f2, format=Format.FORWARDREF), annotationlib.get_annotations(f2, format=Format.FORWARDREF),
{"a": fwd}, {"a": fwd},
@ -1014,7 +1015,7 @@ class TestCallEvaluateFunction(unittest.TestCase):
annotationlib.call_evaluate_function(evaluate, Format.VALUE) annotationlib.call_evaluate_function(evaluate, Format.VALUE)
self.assertEqual( self.assertEqual(
annotationlib.call_evaluate_function(evaluate, Format.FORWARDREF), annotationlib.call_evaluate_function(evaluate, Format.FORWARDREF),
annotationlib.ForwardRef("undefined"), support.EqualToForwardRef("undefined"),
) )
self.assertEqual( self.assertEqual(
annotationlib.call_evaluate_function(evaluate, Format.STRING), annotationlib.call_evaluate_function(evaluate, Format.STRING),

View file

@ -37,7 +37,7 @@ except ImportError:
from test.support import cpython_only, import_helper from test.support import cpython_only, import_helper
from test.support import MISSING_C_DOCSTRINGS, ALWAYS_EQ from test.support import MISSING_C_DOCSTRINGS, ALWAYS_EQ
from test.support import run_no_yield_async_fn from test.support import run_no_yield_async_fn, EqualToForwardRef
from test.support.import_helper import DirsOnSysPath, ready_to_import from test.support.import_helper import DirsOnSysPath, ready_to_import
from test.support.os_helper import TESTFN, temp_cwd from test.support.os_helper import TESTFN, temp_cwd
from test.support.script_helper import assert_python_ok, assert_python_failure, kill_python from test.support.script_helper import assert_python_ok, assert_python_failure, kill_python
@ -4940,9 +4940,12 @@ class TestSignatureObject(unittest.TestCase):
signature_func(ida.f, annotation_format=Format.STRING), signature_func(ida.f, annotation_format=Format.STRING),
sig([par("x", PORK, annotation="undefined")]) sig([par("x", PORK, annotation="undefined")])
) )
s1 = signature_func(ida.f, annotation_format=Format.FORWARDREF)
s2 = sig([par("x", PORK, annotation=EqualToForwardRef("undefined", owner=ida.f))])
#breakpoint()
self.assertEqual( self.assertEqual(
signature_func(ida.f, annotation_format=Format.FORWARDREF), signature_func(ida.f, annotation_format=Format.FORWARDREF),
sig([par("x", PORK, annotation=ForwardRef("undefined"))]) sig([par("x", PORK, annotation=EqualToForwardRef("undefined", owner=ida.f))])
) )
with self.assertRaisesRegex(NameError, "undefined"): with self.assertRaisesRegex(NameError, "undefined"):
signature_func(ida.f, annotation_format=Format.VALUE) signature_func(ida.f, annotation_format=Format.VALUE)

View file

@ -2,7 +2,7 @@
from test.support import ( from test.support import (
run_with_locale, cpython_only, no_rerun, run_with_locale, cpython_only, no_rerun,
MISSING_C_DOCSTRINGS, MISSING_C_DOCSTRINGS, EqualToForwardRef,
) )
import collections.abc import collections.abc
from collections import namedtuple, UserDict from collections import namedtuple, UserDict
@ -1089,7 +1089,13 @@ class UnionTests(unittest.TestCase):
self.assertIs(int, types.UnionType[int]) self.assertIs(int, types.UnionType[int])
self.assertIs(int, types.UnionType[int, int]) self.assertIs(int, types.UnionType[int, int])
self.assertEqual(int | str, types.UnionType[int, str]) self.assertEqual(int | str, types.UnionType[int, str])
self.assertEqual(int | typing.ForwardRef("str"), types.UnionType[int, "str"])
for obj in (
int | typing.ForwardRef("str"),
typing.Union[int, "str"],
):
self.assertIsInstance(obj, types.UnionType)
self.assertEqual(obj.__args__, (int, EqualToForwardRef("str")))
class MappingProxyTests(unittest.TestCase): class MappingProxyTests(unittest.TestCase):

View file

@ -49,7 +49,10 @@ import weakref
import warnings import warnings
import types import types
from test.support import captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper, run_code from test.support import (
captured_stderr, cpython_only, infinite_recursion, requires_docstrings, import_helper, run_code,
EqualToForwardRef,
)
from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper from test.typinganndata import ann_module695, mod_generics_cache, _typed_dict_helper
@ -468,8 +471,8 @@ class TypeVarTests(BaseTestCase):
self.assertEqual(X | "x", Union[X, "x"]) self.assertEqual(X | "x", Union[X, "x"])
self.assertEqual("x" | X, Union["x", X]) self.assertEqual("x" | X, Union["x", X])
# make sure the order is correct # make sure the order is correct
self.assertEqual(get_args(X | "x"), (X, ForwardRef("x"))) self.assertEqual(get_args(X | "x"), (X, EqualToForwardRef("x")))
self.assertEqual(get_args("x" | X), (ForwardRef("x"), X)) self.assertEqual(get_args("x" | X), (EqualToForwardRef("x"), X))
def test_union_constrained(self): def test_union_constrained(self):
A = TypeVar('A', str, bytes) A = TypeVar('A', str, bytes)
@ -4992,7 +4995,7 @@ class GenericTests(BaseTestCase):
def f(x: X): ... def f(x: X): ...
self.assertEqual( self.assertEqual(
get_type_hints(f, globals(), locals()), get_type_hints(f, globals(), locals()),
{'x': list[list[ForwardRef('X')]]} {'x': list[list[EqualToForwardRef('X')]]}
) )
def test_pep695_generic_class_with_future_annotations(self): def test_pep695_generic_class_with_future_annotations(self):
@ -6186,7 +6189,7 @@ class ForwardRefTests(BaseTestCase):
return a return a
self.assertEqual(namespace1(), namespace1()) self.assertEqual(namespace1(), namespace1())
self.assertNotEqual(namespace1(), namespace2()) self.assertEqual(namespace1(), namespace2())
def test_forward_repr(self): def test_forward_repr(self):
self.assertEqual(repr(List['int']), "typing.List[ForwardRef('int')]") self.assertEqual(repr(List['int']), "typing.List[ForwardRef('int')]")
@ -6244,14 +6247,10 @@ class ForwardRefTests(BaseTestCase):
ret = get_type_hints(fun, globals(), locals()) ret = get_type_hints(fun, globals(), locals())
return a return a
def cmp(o1, o2): r1 = namespace1()
return o1 == o2 r2 = namespace2()
self.assertIsNot(r1, r2)
with infinite_recursion(25): self.assertEqual(r1, r2)
r1 = namespace1()
r2 = namespace2()
self.assertIsNot(r1, r2)
self.assertRaises(RecursionError, cmp, r1, r2)
def test_union_forward_recursion(self): def test_union_forward_recursion(self):
ValueList = List['Value'] ValueList = List['Value']
@ -7173,7 +7172,8 @@ class GetTypeHintTests(BaseTestCase):
# FORWARDREF # FORWARDREF
self.assertEqual( self.assertEqual(
get_type_hints(func, format=annotationlib.Format.FORWARDREF), get_type_hints(func, format=annotationlib.Format.FORWARDREF),
{'x': ForwardRef('undefined'), 'return': ForwardRef('undefined')}, {'x': EqualToForwardRef('undefined', owner=func),
'return': EqualToForwardRef('undefined', owner=func)},
) )
# STRING # STRING
@ -8044,7 +8044,7 @@ class NamedTupleTests(BaseTestCase):
class Z(NamedTuple): class Z(NamedTuple):
a: None a: None
b: "str" b: "str"
annos = {'a': type(None), 'b': ForwardRef("str")} annos = {'a': type(None), 'b': EqualToForwardRef("str")}
self.assertEqual(Z.__annotations__, annos) self.assertEqual(Z.__annotations__, annos)
self.assertEqual(Z.__annotate__(annotationlib.Format.VALUE), annos) self.assertEqual(Z.__annotate__(annotationlib.Format.VALUE), annos)
self.assertEqual(Z.__annotate__(annotationlib.Format.FORWARDREF), annos) self.assertEqual(Z.__annotate__(annotationlib.Format.FORWARDREF), annos)
@ -8060,7 +8060,7 @@ class NamedTupleTests(BaseTestCase):
""" """
ns = run_code(textwrap.dedent(code)) ns = run_code(textwrap.dedent(code))
X = ns['X'] X = ns['X']
self.assertEqual(X.__annotations__, {'a': ForwardRef("int"), 'b': ForwardRef("None")}) self.assertEqual(X.__annotations__, {'a': EqualToForwardRef("int"), 'b': EqualToForwardRef("None")})
def test_deferred_annotations(self): def test_deferred_annotations(self):
class X(NamedTuple): class X(NamedTuple):
@ -9079,7 +9079,7 @@ class TypedDictTests(BaseTestCase):
class Y(TypedDict): class Y(TypedDict):
a: None a: None
b: "int" b: "int"
fwdref = ForwardRef('int', module=__name__) fwdref = EqualToForwardRef('int', module=__name__)
self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref}) self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref})
self.assertEqual(Y.__annotate__(annotationlib.Format.FORWARDREF), {'a': type(None), 'b': fwdref}) self.assertEqual(Y.__annotate__(annotationlib.Format.FORWARDREF), {'a': type(None), 'b': fwdref})

View file

@ -0,0 +1,3 @@
:class:`annotationlib.ForwardRef` objects no longer cache their value when
they are successfully evaluated. Successive calls to
:meth:`annotationlib.ForwardRef.evaluate` may return different values.

View file

@ -0,0 +1,3 @@
The implementations of equality and hashing for :class:`annotationlib.ForwardRef`
now use all attributes on the object. Two :class:`!ForwardRef` objects
are equal only if all attributes are equal.