https://github.com/python/cpython/commit/ac14d4a23f58c1f3e1909753aa17c1c302ea771d
commit: ac14d4a23f58c1f3e1909753aa17c1c302ea771d
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-04-05T04:36:34Z
summary:
gh-129463, gh-128593: Simplify ForwardRef (#129465)
files:
A Misc/NEWS.d/next/Library/2025-01-29-21-27-45.gh-issue-128593.r3j4l-.rst
A Misc/NEWS.d/next/Library/2025-01-29-21-29-46.gh-issue-129463.qePexX.rst
M Doc/library/annotationlib.rst
M Doc/library/typing.rst
M Doc/whatsnew/3.14.rst
M Lib/annotationlib.py
M Lib/test/support/__init__.py
M Lib/test/test_annotationlib.py
M Lib/test/test_inspect/test_inspect.py
M Lib/test/test_types.py
M Lib/test/test_typing.py
diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst
index ab9393e8cd8ab7..e07081e3c5dd7a 100644
--- a/Doc/library/annotationlib.rst
+++ b/Doc/library/annotationlib.rst
@@ -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
diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index d5870498fa35b9..84f77e8f206438 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -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)
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index d97874fe7a88d4..81c7969e8af245 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -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'}
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index c42dad3503bff7..d6243c8863610e 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -32,18 +32,16 @@ class Format(enum.IntEnum):
# 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 @@ def __init__(
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 @@ def evaluate(self, *, globals=None, locals=None,
type_params=None, owner=None):
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 @@ def evaluate(self, *, globals=None, locals=None,
type_params=None, owner=None):
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 @@ def __forward_code__(self):
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 @@ def __ror__(self, other):
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 @@ def __init__(
# 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
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 1ae4e0b756f007..6d670a575b0c4f 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -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():
"""
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index 495606b48ed2e8..f10282042c7430 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -97,27 +97,27 @@ def f(
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 @@ def test_fwdref_to_builtin(self):
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 @@ def f2(a: undefined):
)
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 @@ def evaluate(format, exc=NotImplementedError):
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),
diff --git a/Lib/test/test_inspect/test_inspect.py
b/Lib/test/test_inspect/test_inspect.py
index 73cf5ac64eece6..daae990458d708 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -37,7 +37,7 @@
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 @@ def test_signature_annotation_format(self):
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)
diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py
index f014f7e9ee08c9..350567ec7e990d 100644
--- a/Lib/test/test_types.py
+++ b/Lib/test/test_types.py
@@ -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 @@ def test_instantiation(self):
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):
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index ead589fef6cd11..edf3cf9d4a3658 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -49,7 +49,10 @@
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 @@ def test_or(self):
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 C3:
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 @@ def fun(x: a):
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 @@ def fun(x: a): pass
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 @@ def func(x: undefined) -> undefined: ...
# 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 Y(NamedTuple):
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 X(NamedTuple):
"""
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 X(TypedDict):
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})
diff --git
a/Misc/NEWS.d/next/Library/2025-01-29-21-27-45.gh-issue-128593.r3j4l-.rst
b/Misc/NEWS.d/next/Library/2025-01-29-21-27-45.gh-issue-128593.r3j4l-.rst
new file mode 100644
index 00000000000000..03fb5a5413a4a8
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-01-29-21-27-45.gh-issue-128593.r3j4l-.rst
@@ -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.
diff --git
a/Misc/NEWS.d/next/Library/2025-01-29-21-29-46.gh-issue-129463.qePexX.rst
b/Misc/NEWS.d/next/Library/2025-01-29-21-29-46.gh-issue-129463.qePexX.rst
new file mode 100644
index 00000000000000..2dea03d2384578
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-01-29-21-29-46.gh-issue-129463.qePexX.rst
@@ -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.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]