mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
gh-129463, gh-128593: Simplify ForwardRef (#129465)
This commit is contained in:
parent
231a50fa9a
commit
ac14d4a23f
11 changed files with 124 additions and 69 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
"""
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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})
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
Loading…
Add table
Add a link
Reference in a new issue