Repository: python/typing_extensions

Issue:
Add typing_extensions.evaluate_forward_ref
We're building typing_extensions 4.12.2 with Python 3.14.0a1 in Fedora Linux. Unittests fail during the build:

```
+ /usr/bin/python3 -m unittest discover
.F................................................................................................................................................................s..s..s........................................................................................................................................................................................s.........................................s.s...............................s........
======================================================================
FAIL: test_drop_in_for_typing (test_typing_extensions.AllTests.test_drop_in_for_typing)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/builddir/build/BUILD/python-typing-extensions-4.12.2-build/typing_extensions-4.12.2/src/test_typing_extensions.py", line 5747, in test_drop_in_for_typing
    self.assertGreaterEqual(te_all, t_all - exceptions)
    ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: {'final', 'SupportsComplex', 'TYPE_CHECKING', 'Generator', 'Dict', 'Required', 'MutableSequence', 'cast', 'no_type_check_decorator', 'OrderedDict', 'ParamSpec', 'ChainMap', 'Collection', 'Deque', 'Container', 'clear_overloads', 'IO', 'NotRequired', 'NoReturn', 'NamedTuple', 'LiteralString', 'Set', 'Self', 'Awaitable', 'MutableSet', 'AsyncGenerator', 'TypeIs', 'Literal', 'Mapping', 'Iterator', 'DefaultDict', 'ClassVar', 'ParamSpecArgs', 'MutableMapping', 'CapsuleType', 'get_origin', 'ValuesView', 'is_typeddict', 'Match', 'SupportsFloat', 'TextIO', 'Doc', 'get_type_hints', 'Final', 'NewType', 'TypeAliasType', 'Reversible', 'FrozenSet', 'Never', 'AsyncContextManager', 'Hashable', 'get_args', 'ReadOnly', 'runtime', 'BinaryIO', 'TypeVar', 'Sized', 'IntVar', 'Optional', 'AsyncIterator', 'Any', 'ItemsView', 'Coroutine', 'MappingView', 'KeysView', 'is_protocol', 'get_overloads', 'Annotated', 'AnyStr', 'NoDefault', 'dataclass_transform', 'Buffer', 'TypeAlias', 'get_protocol_members', 'SupportsBytes', 'overload', 'Sequence', 'Unpack', 'AbstractSet', 'reveal_type', 'ParamSpecKwargs', 'SupportsAbs', 'Text', 'SupportsIndex', 'Type', 'TypedDict', 'Counter', 'Union', 'Pattern', 'get_original_bases', 'Protocol', 'SupportsRound', 'TypeVarTuple', 'assert_never', 'List', 'Iterable', 'Concatenate', 'runtime_checkable', 'Tuple', 'TypeGuard', 'override', 'no_type_check', 'deprecated', 'AsyncIterable', 'ForwardRef', 'ContextManager', 'Callable', 'Generic', 'SupportsInt', 'assert_type'} not greater than or equal to {'final', 'SupportsComplex', 'AsyncContextManager', 'evaluate_forward_ref', 'TYPE_CHECKING', 'Hashable', 'Generator', 'get_args', 'ReadOnly', 'Dict', 'Required', 'MutableSequence', 'cast', 'BinaryIO', 'TypeVar', 'Sized', 'no_type_check_decorator', 'OrderedDict', 'Optional', 'ParamSpec', 'ChainMap', 'AsyncIterator', 'Any', 'ItemsView', 'Collection', 'Coroutine', 'MappingView', 'KeysView', 'is_protocol', 'get_overloads', 'Deque', 'Annotated', 'AnyStr', 'NoDefault', 'Container', 'clear_overloads', 'dataclass_transform', 'IO', 'NotRequired', 'NoReturn', 'TypeAlias', 'get_protocol_members', 'SupportsBytes', 'NamedTuple', 'LiteralString', 'Sequence', 'overload', 'AbstractSet', 'Set', 'Unpack', 'Awaitable', 'Self', 'reveal_type', 'SupportsAbs', 'ParamSpecKwargs', 'Text', 'MutableSet', 'AsyncGenerator', 'SupportsIndex', 'Type', 'TypeIs', 'Literal', 'Mapping', 'TypedDict', 'Union', 'Counter', 'Pattern', 'Protocol', 'Iterator', 'DefaultDict', 'ClassVar', 'ParamSpecArgs', 'MutableMapping', 'SupportsRound', 'get_origin', 'TypeVarTuple', 'assert_never', 'ValuesView', 'List', 'is_typeddict', 'Iterable', 'Match', 'Concatenate', 'SupportsFloat', 'runtime_checkable', 'Tuple', 'TypeGuard', 'TextIO', 'no_type_check', 'get_type_hints', 'override', 'Final', 'NewType', 'AsyncIterable', 'ForwardRef', 'ContextManager', 'TypeAliasType', 'Reversible', 'Callable', 'Generic', 'FrozenSet', 'Never', 'SupportsInt', 'assert_type'}
```

Hints from issue comments:
@JelleZijlstra: Looks like it's because the new `typing.evaluate_forward_ref` is not in typing_extensions. PR welcome to add it.@hroncok: For reference, this was added to Python via https://github.com/python/cpython/pull/119891

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c11f96..139d92c 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 d321ce0..ea5d776 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 <type-params>` 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/typing_extensions.py b/src/typing_extensions.py
index 993a284..ded403f 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 @@ __all__ = [
     'dataclass_transform',
     'deprecated',
     'Doc',
+    'evaluate_forward_ref',
     'get_overloads',
     'final',
     'Format',
@@ -142,6 +145,9 @@ PEP_560 = True
 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 @@ _PEP_649_OR_749_IMPLEMENTED = (
 class Format(enum.IntEnum):
     VALUE = 1
     FORWARDREF = 2
-    SOURCE = 3
+    STRING = 3
 
 
 if _PEP_649_OR_749_IMPLEMENTED:
@@ -4036,13 +4042,13 @@ else:
           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 @@ else:
             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 @@ else:
             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


Test patch:
diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py
index 2a3a800..10efcd2 100644
--- a/src/test_typing_extensions.py
+++ b/src/test_typing_extensions.py
@@ -28,6 +28,7 @@ from unittest.mock import patch
 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 @@ from typing_extensions import (
     clear_overloads,
     dataclass_transform,
     deprecated,
+    evaluate_forward_ref,
     final,
     get_annotations,
     get_args,
@@ -7948,7 +7950,7 @@ class TestGetAnnotations(BaseTestCase):
         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 @@ class TestGetAnnotations(BaseTestCase):
                 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 @@ class TestGetAnnotations(BaseTestCase):
                     {"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 @@ class TestGetAnnotations(BaseTestCase):
                 )
 
         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 @@ class TestGetAnnotations(BaseTestCase):
         )
         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 @@ class TestGetAnnotations(BaseTestCase):
             {"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 @@ class TestGetAnnotationsWithPEP695(BaseTestCase):
             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()