Skip to content
6 changes: 0 additions & 6 deletions Doc/library/annotationlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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'}

Expand Down
53 changes: 27 additions & 26 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ 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__",
Expand Down Expand Up @@ -76,8 +74,6 @@ 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
Expand All @@ -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__

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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__
and self.__forward_is_class__ == other.__forward_is_class__
and self.__code__ == other.__code__
and self.__ast_node__ == other.__ast_node__
# Use "is" here because we use id() for this in __hash__
# because dictionaries are not hashable.
and self.__globals__ is other.__globals__
and self.__cell__ == other.__cell__
and self.__owner__ == other.__owner__
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could possibly micro-optimise this slightly by moving some of the cheaper comparisons up (e.g. the self.__globals__ is other.__globals__ comparison should be very cheap since it's just an identity check).

It took me a while to wrap my head around the self.__globals__ is other.__globals__ comparison but I think it's correct to do it that way...!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reordered them a bit. I think the performance effects are quite hard to predict though because it matters not just how expensive the check is, but also how likely it is to short-circuit the comparison. For example, maybe since we already checked the module, the globals check actually never makes a difference, because if the module doesn't match the globals also won't match.

If somebody wants to micro-optimize this I won't stop them but I'd rather just land this; I doubt it will make a huge difference either way.


def __hash__(self):
return hash((self.__forward_arg__, self.__forward_module__))
return hash((
self.__forward_arg__,
self.__forward_module__,
self.__forward_is_class__,
self.__code__,
self.__ast_node__,
id(self.__globals__), # dictionaries are not hashable, so hash by identity
self.__cell__,
self.__owner__,
))

def __or__(self, other):
return types.UnionType[self, other]
Expand All @@ -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:
Expand All @@ -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
Expand Down
42 changes: 42 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
"""
Expand Down
21 changes: 11 additions & 10 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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),
Expand Down
7 changes: 5 additions & 2 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 8 additions & 2 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Loading
Loading