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
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

View file

@ -3449,7 +3449,9 @@ Introspection helpers
.. versionadded:: 3.7.4
.. 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)

View file

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

View file

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

View file

@ -3,6 +3,7 @@
if __name__ != 'test.support':
raise ImportError('support must be imported from the test package')
import annotationlib
import contextlib
import functools
import inspect
@ -3021,6 +3022,47 @@ def is_libssl_fips_mode():
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
def linked_to_musl():
"""

View file

@ -97,27 +97,27 @@ class TestForwardRefFormat(unittest.TestCase):
anno = annotationlib.get_annotations(f, format=Format.FORWARDREF)
x_anno = anno["x"]
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"]
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"]
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"]
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"]
self.assertIsInstance(beta_anno, ForwardRef)
self.assertEqual(beta_anno, ForwardRef("+some"))
self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f))
gamma_anno = anno["gamma"]
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):
@ -362,12 +362,13 @@ class TestForwardRefClass(unittest.TestCase):
obj = object()
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")
with self.assertRaises(NameError):
fr.evaluate()
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):
self.assertEqual(
@ -457,7 +458,7 @@ class TestGetAnnotations(unittest.TestCase):
)
self.assertEqual(annotationlib.get_annotations(f1, format=1), {"a": int})
fwd = annotationlib.ForwardRef("undefined")
fwd = support.EqualToForwardRef("undefined", owner=f2)
self.assertEqual(
annotationlib.get_annotations(f2, format=Format.FORWARDREF),
{"a": fwd},
@ -1014,7 +1015,7 @@ class TestCallEvaluateFunction(unittest.TestCase):
annotationlib.call_evaluate_function(evaluate, Format.VALUE)
self.assertEqual(
annotationlib.call_evaluate_function(evaluate, Format.FORWARDREF),
annotationlib.ForwardRef("undefined"),
support.EqualToForwardRef("undefined"),
)
self.assertEqual(
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 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.os_helper import TESTFN, temp_cwd
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),
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(
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"):
signature_func(ida.f, annotation_format=Format.VALUE)

View file

@ -2,7 +2,7 @@
from test.support import (
run_with_locale, cpython_only, no_rerun,
MISSING_C_DOCSTRINGS,
MISSING_C_DOCSTRINGS, EqualToForwardRef,
)
import collections.abc
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, int])
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):

View file

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