From 8184ac61398c187203dad819eb5b9d34005a96ae Mon Sep 17 00:00:00 2001 From: Daraan Date: Fri, 17 Jan 2025 05:58:06 +0100 Subject: [PATCH] Add backport of `evaluate_forward_ref` (#497) Co-authored-by: Jelle Zijlstra --- CHANGELOG.md | 3 + doc/index.rst | 35 ++++- src/test_typing_extensions.py | 228 +++++++++++++++++++++++++++-- src/typing_extensions.py | 267 +++++++++++++++++++++++++++++++++- 4 files changed, 513 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c11f96b..139d92c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ aliases that have a `Concatenate` special form as their argument. - Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795): fix `TypeAliasType` not raising an error on non-tuple inputs for `type_params`. Patch by [Daraan](https://github.com/Daraan). +- Backport `evaluate_forward_ref` from CPython PR + [#119891](https://github.com/python/cpython/pull/119891) to evaluate `ForwardRef`s. + Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. - Fix that lists and ... could not be used for parameter expressions for `TypeAliasType` instances before Python 3.11. Patch by [Daraan](https://github.com/Daraan). diff --git a/doc/index.rst b/doc/index.rst index d321ce04..ea5d776d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -753,6 +753,37 @@ Functions .. versionadded:: 4.2.0 +.. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE) + + Evaluate an :py:class:`typing.ForwardRef` as a :py:term:`type hint`. + + This is similar to calling :py:meth:`annotationlib.ForwardRef.evaluate`, + but unlike that method, :func:`!evaluate_forward_ref` also: + + * Recursively evaluates forward references nested within the type hint. + However, the amount of recursion is limited in Python 3.8 and 3.10. + * Raises :exc:`TypeError` when it encounters certain objects that are + not valid type hints. + * Replaces type hints that evaluate to :const:`!None` with + :class:`types.NoneType`. + * Supports the :attr:`Format.FORWARDREF` and + :attr:`Format.STRING` formats. + + *forward_ref* must be an instance of :py:class:`typing.ForwardRef`. + *owner*, if given, should be the object that holds the annotations that + the forward reference derived from, such as a module, class object, or function. + It is used to infer the namespaces to use for looking up names. + *globals* and *locals* can also be explicitly given to provide + the global and local namespaces. + *type_params* is a tuple of :py:ref:`type parameters ` that + are in scope when evaluating the forward reference. + This parameter must be provided (though it may be an empty tuple) if *owner* + is not given and the forward reference does not already have an owner set. + *format* specifies the format of the annotation and is a member of + the :class:`Format` enum. + + .. versionadded:: 4.13.0 + .. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE) See :py:func:`inspect.get_annotations`. In the standard library since Python 3.10. @@ -764,7 +795,7 @@ Functions of the :pep:`649` behavior on versions of Python that do not support it. The purpose of this backport is to allow users who would like to use - :attr:`Format.FORWARDREF` or :attr:`Format.SOURCE` semantics once + :attr:`Format.FORWARDREF` or :attr:`Format.STRING` semantics once :pep:`649` is implemented, but who also want to support earlier Python versions, to simply write:: @@ -911,7 +942,7 @@ Enums ``typing_extensions`` emulates this value on versions of Python which do not support :pep:`649` by returning the same value as for ``VALUE`` semantics. - .. attribute:: SOURCE + .. attribute:: STRING Equal to 3. When :pep:`649` is implemented, this format will produce an annotation dictionary where the values have been replaced by strings containing diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 2a3a800e..10efcd24 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -28,6 +28,7 @@ import typing_extensions from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated from typing_extensions import ( + _FORWARD_REF_HAS_CLASS, _PEP_649_OR_749_IMPLEMENTED, Annotated, Any, @@ -82,6 +83,7 @@ clear_overloads, dataclass_transform, deprecated, + evaluate_forward_ref, final, get_annotations, get_args, @@ -7948,7 +7950,7 @@ def f2(a: "undefined"): # noqa: F821 self.assertEqual(get_annotations(f2, format=2), {"a": "undefined"}) self.assertEqual( - get_annotations(f1, format=Format.SOURCE), + get_annotations(f1, format=Format.STRING), {"a": "int"}, ) self.assertEqual(get_annotations(f1, format=3), {"a": "int"}) @@ -7975,7 +7977,7 @@ def foo(): foo, format=Format.FORWARDREF, eval_str=True ) get_annotations( - foo, format=Format.SOURCE, eval_str=True + foo, format=Format.STRING, eval_str=True ) def test_stock_annotations(self): @@ -7989,7 +7991,7 @@ def foo(a: int, b: str): {"a": int, "b": str}, ) self.assertEqual( - get_annotations(foo, format=Format.SOURCE), + get_annotations(foo, format=Format.STRING), {"a": "int", "b": "str"}, ) @@ -8084,43 +8086,43 @@ def test_stock_annotations_in_module(self): ) self.assertEqual( - get_annotations(isa, format=Format.SOURCE), + get_annotations(isa, format=Format.STRING), {"a": "int", "b": "str"}, ) self.assertEqual( - get_annotations(isa.MyClass, format=Format.SOURCE), + get_annotations(isa.MyClass, format=Format.STRING), {"a": "int", "b": "str"}, ) mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass" self.assertEqual( - get_annotations(isa.function, format=Format.SOURCE), + get_annotations(isa.function, format=Format.STRING), {"a": "int", "b": "str", "return": mycls}, ) self.assertEqual( get_annotations( - isa.function2, format=Format.SOURCE + isa.function2, format=Format.STRING ), {"a": "int", "b": "str", "c": mycls, "return": mycls}, ) self.assertEqual( get_annotations( - isa.function3, format=Format.SOURCE + isa.function3, format=Format.STRING ), {"a": "int", "b": "str", "c": "MyClass"}, ) self.assertEqual( - get_annotations(inspect, format=Format.SOURCE), + get_annotations(inspect, format=Format.STRING), {}, ) self.assertEqual( get_annotations( - isa.UnannotatedClass, format=Format.SOURCE + isa.UnannotatedClass, format=Format.STRING ), {}, ) self.assertEqual( get_annotations( - isa.unannotated_function, format=Format.SOURCE + isa.unannotated_function, format=Format.STRING ), {}, ) @@ -8141,7 +8143,7 @@ def test_stock_annotations_on_wrapper(self): ) mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass" self.assertEqual( - get_annotations(wrapped, format=Format.SOURCE), + get_annotations(wrapped, format=Format.STRING), {"a": "int", "b": "str", "return": mycls}, ) self.assertEqual( @@ -8160,10 +8162,10 @@ def test_stringized_annotations_in_module(self): {"eval_str": False}, {"format": Format.VALUE}, {"format": Format.FORWARDREF}, - {"format": Format.SOURCE}, + {"format": Format.STRING}, {"format": Format.VALUE, "eval_str": False}, {"format": Format.FORWARDREF, "eval_str": False}, - {"format": Format.SOURCE, "eval_str": False}, + {"format": Format.STRING, "eval_str": False}, ]: with self.subTest(**kwargs): self.assertEqual( @@ -8466,6 +8468,204 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self): set(results.generic_func.__type_params__) ) +class TestEvaluateForwardRefs(BaseTestCase): + def test_global_constant(self): + if sys.version_info[:3] > (3, 10, 0): + self.assertTrue(_FORWARD_REF_HAS_CLASS) + + def test_forward_ref_fallback(self): + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("doesntexist")) + ref = typing.ForwardRef("doesntexist") + self.assertIs(evaluate_forward_ref(ref, format=Format.FORWARDREF), ref) + + class X: + unresolvable = "doesnotexist2" + + evaluated_ref = evaluate_forward_ref( + typing.ForwardRef("X.unresolvable"), + locals={"X": X}, + type_params=None, + format=Format.FORWARDREF, + ) + self.assertEqual(evaluated_ref, typing.ForwardRef("doesnotexist2")) + + def test_evaluate_with_type_params(self): + # Use a T name that is not in globals + self.assertNotIn("Tx", globals()) + if not TYPING_3_12_0: + Tx = TypeVar("Tx") + class Gen(Generic[Tx]): + alias = int + if not hasattr(Gen, "__type_params__"): + Gen.__type_params__ = (Tx,) + self.assertEqual(Gen.__type_params__, (Tx,)) + del Tx + else: + ns = {} + exec(textwrap.dedent(""" + class Gen[Tx]: + alias = int + """), None, ns) + Gen = ns["Gen"] + + # owner=None, type_params=None + # NOTE: The behavior of owner=None might change in the future when ForwardRef.__owner__ is available + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("Tx")) + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=()) + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("Tx"), owner=int) + + (Tx,) = Gen.__type_params__ + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=Gen.__type_params__), Tx) + + # For this test its important that Tx is not a global variable, i.e. do not use "T" here + self.assertNotIn("Tx", globals()) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), owner=Gen), Tx) + + # Different type_params take precedence + not_Tx = TypeVar("Tx") # different TypeVar with same name + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=(not_Tx,), owner=Gen), not_Tx) + + # globals can take higher precedence + if _FORWARD_REF_HAS_CLASS: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx", is_class=True), owner=Gen, globals={"Tx": str}), str) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx", is_class=True), owner=Gen, type_params=(not_Tx,), globals={"Tx": str}), str) + + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("alias"), type_params=Gen.__type_params__) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen), int) + # If you pass custom locals, we don't look at the owner's locals + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen, locals={}) + # But if the name exists in the locals, it works + self.assertIs( + evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen, locals={"alias": str}), str + ) + + @skipUnless( + HAS_FORWARD_MODULE, "Needs module 'forward' to test forward references" + ) + def test_fwdref_with_module(self): + self.assertIs( + evaluate_forward_ref(typing.ForwardRef("Counter", module="collections")), collections.Counter + ) + self.assertEqual( + evaluate_forward_ref(typing.ForwardRef("Counter[int]", module="collections")), + collections.Counter[int], + ) + + with self.assertRaises(NameError): + # If globals are passed explicitly, we don't look at the module dict + evaluate_forward_ref(typing.ForwardRef("Format", module="annotationlib"), globals={}) + + def test_fwdref_to_builtin(self): + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), int) + if HAS_FORWARD_MODULE: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int", module="collections")), int) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), owner=str), int) + + # builtins are still searched with explicit globals + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={}), int) + + def test_fwdref_with_globals(self): + # explicit values in globals have precedence + obj = object() + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": obj}), obj) + + def test_fwdref_value_is_cached(self): + fr = typing.ForwardRef("hello") + with self.assertRaises(NameError): + evaluate_forward_ref(fr) + self.assertIs(evaluate_forward_ref(fr, globals={"hello": str}), str) + self.assertIs(evaluate_forward_ref(fr), str) + + @skipUnless(TYPING_3_9_0, "Needs PEP 585 support") + def test_fwdref_with_owner(self): + self.assertEqual( + evaluate_forward_ref(typing.ForwardRef("Counter[int]"), owner=collections), + collections.Counter[int], + ) + + def test_name_lookup_without_eval(self): + # test the codepath where we look up simple names directly in the + # namespaces without going through eval() + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), int) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), locals={"int": str}), str) + self.assertIs( + evaluate_forward_ref(typing.ForwardRef("int"), locals={"int": float}, globals={"int": str}), + float, + ) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": str}), str) + import builtins + + from test import support + with support.swap_attr(builtins, "int", dict): + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), dict) + + def test_nested_strings(self): + # This variable must have a different name TypeVar + Tx = TypeVar("Tx") + + class Y(Generic[Tx]): + a = "X" + bT = "Y[T_nonlocal]" + + Z = TypeAliasType("Z", Y[Tx], type_params=(Tx,)) + + evaluated_ref1a = evaluate_forward_ref(typing.ForwardRef("Y[Y['Tx']]"), locals={"Y": Y, "Tx": Tx}) + self.assertEqual(get_origin(evaluated_ref1a), Y) + self.assertEqual(get_args(evaluated_ref1a), (Y[Tx],)) + + evaluated_ref1b = evaluate_forward_ref( + typing.ForwardRef("Y[Y['Tx']]"), locals={"Y": Y}, type_params=(Tx,) + ) + self.assertEqual(get_origin(evaluated_ref1b), Y) + self.assertEqual(get_args(evaluated_ref1b), (Y[Tx],)) + + with self.subTest("nested string of TypeVar"): + evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y}) + self.assertEqual(get_origin(evaluated_ref2), Y) + if not TYPING_3_9_0: + self.skipTest("Nested string 'Tx' stays ForwardRef in 3.8") + self.assertEqual(get_args(evaluated_ref2), (Y[Tx],)) + + with self.subTest("nested string of TypeAliasType and alias"): + # NOTE: Using Y here works for 3.10 + evaluated_ref3 = evaluate_forward_ref(typing.ForwardRef("""Y['Z["StrAlias"]']"""), locals={"Y": Y, "Z": Z, "StrAlias": str}) + self.assertEqual(get_origin(evaluated_ref3), Y) + if sys.version_info[:2] in ((3,8), (3, 10)): + self.skipTest("Nested string 'StrAlias' is not resolved in 3.8 and 3.10") + self.assertEqual(get_args(evaluated_ref3), (Z[str],)) + + def test_invalid_special_forms(self): + # tests _lax_type_check to raise errors the same way as the typing module. + # Regex capture "< class 'module.name'> and "module.name" + with self.assertRaisesRegex( + TypeError, r"Plain .*Protocol('>)? is not valid as type argument" + ): + evaluate_forward_ref(typing.ForwardRef("Protocol"), globals=vars(typing)) + with self.assertRaisesRegex( + TypeError, r"Plain .*Generic('>)? is not valid as type argument" + ): + evaluate_forward_ref(typing.ForwardRef("Generic"), globals=vars(typing)) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("Final"), globals=vars(typing)) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("ClassVar"), globals=vars(typing)) + if _FORWARD_REF_HAS_CLASS: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_class=True), globals=vars(typing)), Final) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_class=True), globals=vars(typing)), ClassVar) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)) + else: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar) + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 993a284d..ded403fe 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1,10 +1,12 @@ import abc +import builtins import collections import collections.abc import contextlib import enum import functools import inspect +import keyword import operator import sys import types as _types @@ -63,6 +65,7 @@ 'dataclass_transform', 'deprecated', 'Doc', + 'evaluate_forward_ref', 'get_overloads', 'final', 'Format', @@ -142,6 +145,9 @@ GenericMeta = type _PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta") +# Added with bpo-45166 to 3.10.1+ and some 3.9 versions +_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__ + # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. @@ -4005,7 +4011,7 @@ def __eq__(self, other: object) -> bool: class Format(enum.IntEnum): VALUE = 1 FORWARDREF = 2 - SOURCE = 3 + STRING = 3 if _PEP_649_OR_749_IMPLEMENTED: @@ -4036,13 +4042,13 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, undefined names with ForwardRef objects. The implementation proposed by PEP 649 relies on language changes that cannot be backported; the typing-extensions implementation simply returns the same result as VALUE. - * SOURCE: return annotations as strings, in a format close to the original + * STRING: return annotations as strings, in a format close to the original source. Again, this behavior cannot be replicated directly in a backport. As an approximation, typing-extensions retrieves the annotations under VALUE semantics and then stringifies them. The purpose of this backport is to allow users who would like to use - FORWARDREF or SOURCE semantics once PEP 649 is implemented, but who also + FORWARDREF or STRING semantics once PEP 649 is implemented, but who also want to support earlier Python versions, to simply write: typing_extensions.get_annotations(obj, format=Format.FORWARDREF) @@ -4101,7 +4107,7 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, return {} if not eval_str: - if format is Format.SOURCE: + if format is Format.STRING: return { key: value if isinstance(value, str) else typing._type_repr(value) for key, value in ann.items() @@ -4136,6 +4142,259 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, for key, value in ann.items() } return return_value + +if hasattr(typing, "evaluate_forward_ref"): + evaluate_forward_ref = typing.evaluate_forward_ref +else: + # Implements annotationlib.ForwardRef.evaluate + def _eval_with_owner( + forward_ref, *, owner=None, globals=None, locals=None, type_params=None + ): + if forward_ref.__forward_evaluated__: + return forward_ref.__forward_value__ + if getattr(forward_ref, "__cell__", None) is not None: + try: + value = forward_ref.__cell__.cell_contents + except ValueError: + pass + else: + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + if owner is None: + owner = getattr(forward_ref, "__owner__", None) + + if ( + globals is None + and getattr(forward_ref, "__forward_module__", None) is not None + ): + globals = getattr( + sys.modules.get(forward_ref.__forward_module__, None), "__dict__", None + ) + if globals is None: + globals = getattr(forward_ref, "__globals__", None) + if globals is None: + if isinstance(owner, type): + module_name = getattr(owner, "__module__", None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + globals = getattr(module, "__dict__", None) + elif isinstance(owner, _types.ModuleType): + globals = getattr(owner, "__dict__", None) + elif callable(owner): + globals = getattr(owner, "__globals__", None) + + # If we pass None to eval() below, the globals of this module are used. + if globals is None: + globals = {} + + if locals is None: + locals = {} + if isinstance(owner, type): + locals.update(vars(owner)) + + if type_params is None and owner is not None: + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + type_params = getattr(owner, "__type_params__", None) + + # type parameters require some special handling, + # as they exist in their own scope + # but `eval()` does not have a dedicated parameter for that scope. + # For classes, names in type parameter scopes should override + # names in the global scope (which here are called `localns`!), + # but should in turn be overridden by names in the class scope + # (which here are called `globalns`!) + if type_params is not None: + globals = dict(globals) + locals = dict(locals) + for param in type_params: + param_name = param.__name__ + if ( + _FORWARD_REF_HAS_CLASS and not forward_ref.__forward_is_class__ + ) or param_name not in globals: + globals[param_name] = param + locals.pop(param_name, None) + + arg = forward_ref.__forward_arg__ + if arg.isidentifier() and not keyword.iskeyword(arg): + if arg in locals: + value = locals[arg] + elif arg in globals: + value = globals[arg] + elif hasattr(builtins, arg): + return getattr(builtins, arg) + else: + raise NameError(arg) + else: + code = forward_ref.__forward_code__ + value = eval(code, globals, locals) + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + + def _lax_type_check( + value, msg, is_argument=True, *, module=None, allow_special_forms=False + ): + """ + A lax Python 3.11+ like version of typing._type_check + """ + if hasattr(typing, "_type_convert"): + if _FORWARD_REF_HAS_CLASS: + type_ = typing._type_convert( + value, + module=module, + allow_special_forms=allow_special_forms, + ) + # module was added with bpo-41249 before is_class (bpo-46539) + elif "__forward_module__" in typing.ForwardRef.__slots__: + type_ = typing._type_convert(value, module=module) + else: + type_ = typing._type_convert(value) + else: + if value is None: + return type(None) + if isinstance(value, str): + return ForwardRef(value) + type_ = value + invalid_generic_forms = (Generic, Protocol) + if not allow_special_forms: + invalid_generic_forms += (ClassVar,) + if is_argument: + invalid_generic_forms += (Final,) + if ( + isinstance(type_, typing._GenericAlias) + and get_origin(type_) in invalid_generic_forms + ): + raise TypeError(f"{type_} is not valid as type argument") from None + if type_ in (Any, LiteralString, NoReturn, Never, Self, TypeAlias): + return type_ + if allow_special_forms and type_ in (ClassVar, Final): + return type_ + if ( + isinstance(type_, (_SpecialForm, typing._SpecialForm)) + or type_ in (Generic, Protocol) + ): + raise TypeError(f"Plain {type_} is not valid as type argument") from None + if type(type_) is tuple: # lax version with tuple instead of callable + raise TypeError(f"{msg} Got {type_!r:.100}.") + return type_ + + def evaluate_forward_ref( + forward_ref, + *, + owner=None, + globals=None, + locals=None, + type_params=None, + format=Format.VALUE, + _recursive_guard=frozenset(), + ): + """Evaluate a forward reference as a type hint. + + This is similar to calling the ForwardRef.evaluate() method, + but unlike that method, evaluate_forward_ref() also: + + * Recursively evaluates forward references nested within the type hint. + * Rejects certain objects that are not valid type hints. + * Replaces type hints that evaluate to None with types.NoneType. + * Supports the *FORWARDREF* and *STRING* formats. + + *forward_ref* must be an instance of ForwardRef. *owner*, if given, + should be the object that holds the annotations that the forward reference + derived from, such as a module, class object, or function. It is used to + infer the namespaces to use for looking up names. *globals* and *locals* + can also be explicitly given to provide the global and local namespaces. + *type_params* is a tuple of type parameters that are in scope when + evaluating the forward reference. This parameter must be provided (though + it may be an empty tuple) if *owner* is not given and the forward reference + does not already have an owner set. *format* specifies the format of the + annotation and is a member of the annotationlib.Format enum. + + """ + if format == Format.STRING: + return forward_ref.__forward_arg__ + if forward_ref.__forward_arg__ in _recursive_guard: + return forward_ref + + # Evaluate the forward reference + try: + value = _eval_with_owner( + forward_ref, + owner=owner, + globals=globals, + locals=locals, + type_params=type_params, + ) + except NameError: + if format == Format.FORWARDREF: + return forward_ref + else: + raise + + msg = "Forward references must evaluate to types." + if not _FORWARD_REF_HAS_CLASS: + allow_special_forms = not forward_ref.__forward_is_argument__ + else: + allow_special_forms = forward_ref.__forward_is_class__ + type_ = _lax_type_check( + value, + msg, + is_argument=forward_ref.__forward_is_argument__, + allow_special_forms=allow_special_forms, + ) + + # Recursively evaluate the type + if isinstance(type_, ForwardRef): + if getattr(type_, "__forward_module__", True) is not None: + globals = None + return evaluate_forward_ref( + type_, + globals=globals, + locals=locals, + type_params=type_params, owner=owner, + _recursive_guard=_recursive_guard, format=format + ) + if sys.version_info < (3, 12, 5) and type_params: + # Make use of type_params + locals = dict(locals) if locals else {} + for tvar in type_params: + if tvar.__name__ not in locals: # lets not overwrite something present + locals[tvar.__name__] = tvar + if sys.version_info < (3, 9): + return typing._eval_type( + type_, + globals, + locals, + ) + if sys.version_info < (3, 12, 5): + return typing._eval_type( + type_, + globals, + locals, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + if sys.version_info < (3, 14): + return typing._eval_type( + type_, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + return typing._eval_type( + type_, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + format=format, + owner=owner, + ) + + # Aliases for items that have always been in typing. # Explicitly assign these (rather than using `from typing import *` at the top), # so that we get a CI error if one of these is deleted from typing.py