Skip to content

Commit 8184ac6

Browse files
Add backport of evaluate_forward_ref (#497)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent dbf852b commit 8184ac6

File tree

4 files changed

+513
-20
lines changed

4 files changed

+513
-20
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ aliases that have a `Concatenate` special form as their argument.
2626
- Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795):
2727
fix `TypeAliasType` not raising an error on non-tuple inputs for `type_params`.
2828
Patch by [Daraan](https://github.com/Daraan).
29+
- Backport `evaluate_forward_ref` from CPython PR
30+
[#119891](https://github.com/python/cpython/pull/119891) to evaluate `ForwardRef`s.
31+
Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra.
2932
- Fix that lists and ... could not be used for parameter expressions for `TypeAliasType`
3033
instances before Python 3.11.
3134
Patch by [Daraan](https://github.com/Daraan).

doc/index.rst

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,37 @@ Functions
753753

754754
.. versionadded:: 4.2.0
755755

756+
.. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE)
757+
758+
Evaluate an :py:class:`typing.ForwardRef` as a :py:term:`type hint`.
759+
760+
This is similar to calling :py:meth:`annotationlib.ForwardRef.evaluate`,
761+
but unlike that method, :func:`!evaluate_forward_ref` also:
762+
763+
* Recursively evaluates forward references nested within the type hint.
764+
However, the amount of recursion is limited in Python 3.8 and 3.10.
765+
* Raises :exc:`TypeError` when it encounters certain objects that are
766+
not valid type hints.
767+
* Replaces type hints that evaluate to :const:`!None` with
768+
:class:`types.NoneType`.
769+
* Supports the :attr:`Format.FORWARDREF` and
770+
:attr:`Format.STRING` formats.
771+
772+
*forward_ref* must be an instance of :py:class:`typing.ForwardRef`.
773+
*owner*, if given, should be the object that holds the annotations that
774+
the forward reference derived from, such as a module, class object, or function.
775+
It is used to infer the namespaces to use for looking up names.
776+
*globals* and *locals* can also be explicitly given to provide
777+
the global and local namespaces.
778+
*type_params* is a tuple of :py:ref:`type parameters <type-params>` that
779+
are in scope when evaluating the forward reference.
780+
This parameter must be provided (though it may be an empty tuple) if *owner*
781+
is not given and the forward reference does not already have an owner set.
782+
*format* specifies the format of the annotation and is a member of
783+
the :class:`Format` enum.
784+
785+
.. versionadded:: 4.13.0
786+
756787
.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE)
757788

758789
See :py:func:`inspect.get_annotations`. In the standard library since Python 3.10.
@@ -764,7 +795,7 @@ Functions
764795
of the :pep:`649` behavior on versions of Python that do not support it.
765796

766797
The purpose of this backport is to allow users who would like to use
767-
:attr:`Format.FORWARDREF` or :attr:`Format.SOURCE` semantics once
798+
:attr:`Format.FORWARDREF` or :attr:`Format.STRING` semantics once
768799
:pep:`649` is implemented, but who also
769800
want to support earlier Python versions, to simply write::
770801

@@ -911,7 +942,7 @@ Enums
911942
``typing_extensions`` emulates this value on versions of Python which do
912943
not support :pep:`649` by returning the same value as for ``VALUE`` semantics.
913944

914-
.. attribute:: SOURCE
945+
.. attribute:: STRING
915946

916947
Equal to 3. When :pep:`649` is implemented, this format will produce an annotation
917948
dictionary where the values have been replaced by strings containing

src/test_typing_extensions.py

Lines changed: 214 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import typing_extensions
2929
from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated
3030
from typing_extensions import (
31+
_FORWARD_REF_HAS_CLASS,
3132
_PEP_649_OR_749_IMPLEMENTED,
3233
Annotated,
3334
Any,
@@ -82,6 +83,7 @@
8283
clear_overloads,
8384
dataclass_transform,
8485
deprecated,
86+
evaluate_forward_ref,
8587
final,
8688
get_annotations,
8789
get_args,
@@ -7948,7 +7950,7 @@ def f2(a: "undefined"): # noqa: F821
79487950
self.assertEqual(get_annotations(f2, format=2), {"a": "undefined"})
79497951

79507952
self.assertEqual(
7951-
get_annotations(f1, format=Format.SOURCE),
7953+
get_annotations(f1, format=Format.STRING),
79527954
{"a": "int"},
79537955
)
79547956
self.assertEqual(get_annotations(f1, format=3), {"a": "int"})
@@ -7975,7 +7977,7 @@ def foo():
79757977
foo, format=Format.FORWARDREF, eval_str=True
79767978
)
79777979
get_annotations(
7978-
foo, format=Format.SOURCE, eval_str=True
7980+
foo, format=Format.STRING, eval_str=True
79797981
)
79807982

79817983
def test_stock_annotations(self):
@@ -7989,7 +7991,7 @@ def foo(a: int, b: str):
79897991
{"a": int, "b": str},
79907992
)
79917993
self.assertEqual(
7992-
get_annotations(foo, format=Format.SOURCE),
7994+
get_annotations(foo, format=Format.STRING),
79937995
{"a": "int", "b": "str"},
79947996
)
79957997

@@ -8084,43 +8086,43 @@ def test_stock_annotations_in_module(self):
80848086
)
80858087

80868088
self.assertEqual(
8087-
get_annotations(isa, format=Format.SOURCE),
8089+
get_annotations(isa, format=Format.STRING),
80888090
{"a": "int", "b": "str"},
80898091
)
80908092
self.assertEqual(
8091-
get_annotations(isa.MyClass, format=Format.SOURCE),
8093+
get_annotations(isa.MyClass, format=Format.STRING),
80928094
{"a": "int", "b": "str"},
80938095
)
80948096
mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass"
80958097
self.assertEqual(
8096-
get_annotations(isa.function, format=Format.SOURCE),
8098+
get_annotations(isa.function, format=Format.STRING),
80978099
{"a": "int", "b": "str", "return": mycls},
80988100
)
80998101
self.assertEqual(
81008102
get_annotations(
8101-
isa.function2, format=Format.SOURCE
8103+
isa.function2, format=Format.STRING
81028104
),
81038105
{"a": "int", "b": "str", "c": mycls, "return": mycls},
81048106
)
81058107
self.assertEqual(
81068108
get_annotations(
8107-
isa.function3, format=Format.SOURCE
8109+
isa.function3, format=Format.STRING
81088110
),
81098111
{"a": "int", "b": "str", "c": "MyClass"},
81108112
)
81118113
self.assertEqual(
8112-
get_annotations(inspect, format=Format.SOURCE),
8114+
get_annotations(inspect, format=Format.STRING),
81138115
{},
81148116
)
81158117
self.assertEqual(
81168118
get_annotations(
8117-
isa.UnannotatedClass, format=Format.SOURCE
8119+
isa.UnannotatedClass, format=Format.STRING
81188120
),
81198121
{},
81208122
)
81218123
self.assertEqual(
81228124
get_annotations(
8123-
isa.unannotated_function, format=Format.SOURCE
8125+
isa.unannotated_function, format=Format.STRING
81248126
),
81258127
{},
81268128
)
@@ -8141,7 +8143,7 @@ def test_stock_annotations_on_wrapper(self):
81418143
)
81428144
mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass"
81438145
self.assertEqual(
8144-
get_annotations(wrapped, format=Format.SOURCE),
8146+
get_annotations(wrapped, format=Format.STRING),
81458147
{"a": "int", "b": "str", "return": mycls},
81468148
)
81478149
self.assertEqual(
@@ -8160,10 +8162,10 @@ def test_stringized_annotations_in_module(self):
81608162
{"eval_str": False},
81618163
{"format": Format.VALUE},
81628164
{"format": Format.FORWARDREF},
8163-
{"format": Format.SOURCE},
8165+
{"format": Format.STRING},
81648166
{"format": Format.VALUE, "eval_str": False},
81658167
{"format": Format.FORWARDREF, "eval_str": False},
8166-
{"format": Format.SOURCE, "eval_str": False},
8168+
{"format": Format.STRING, "eval_str": False},
81678169
]:
81688170
with self.subTest(**kwargs):
81698171
self.assertEqual(
@@ -8466,6 +8468,204 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
84668468
set(results.generic_func.__type_params__)
84678469
)
84688470

8471+
class TestEvaluateForwardRefs(BaseTestCase):
8472+
def test_global_constant(self):
8473+
if sys.version_info[:3] > (3, 10, 0):
8474+
self.assertTrue(_FORWARD_REF_HAS_CLASS)
8475+
8476+
def test_forward_ref_fallback(self):
8477+
with self.assertRaises(NameError):
8478+
evaluate_forward_ref(typing.ForwardRef("doesntexist"))
8479+
ref = typing.ForwardRef("doesntexist")
8480+
self.assertIs(evaluate_forward_ref(ref, format=Format.FORWARDREF), ref)
8481+
8482+
class X:
8483+
unresolvable = "doesnotexist2"
8484+
8485+
evaluated_ref = evaluate_forward_ref(
8486+
typing.ForwardRef("X.unresolvable"),
8487+
locals={"X": X},
8488+
type_params=None,
8489+
format=Format.FORWARDREF,
8490+
)
8491+
self.assertEqual(evaluated_ref, typing.ForwardRef("doesnotexist2"))
8492+
8493+
def test_evaluate_with_type_params(self):
8494+
# Use a T name that is not in globals
8495+
self.assertNotIn("Tx", globals())
8496+
if not TYPING_3_12_0:
8497+
Tx = TypeVar("Tx")
8498+
class Gen(Generic[Tx]):
8499+
alias = int
8500+
if not hasattr(Gen, "__type_params__"):
8501+
Gen.__type_params__ = (Tx,)
8502+
self.assertEqual(Gen.__type_params__, (Tx,))
8503+
del Tx
8504+
else:
8505+
ns = {}
8506+
exec(textwrap.dedent("""
8507+
class Gen[Tx]:
8508+
alias = int
8509+
"""), None, ns)
8510+
Gen = ns["Gen"]
8511+
8512+
# owner=None, type_params=None
8513+
# NOTE: The behavior of owner=None might change in the future when ForwardRef.__owner__ is available
8514+
with self.assertRaises(NameError):
8515+
evaluate_forward_ref(typing.ForwardRef("Tx"))
8516+
with self.assertRaises(NameError):
8517+
evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=())
8518+
with self.assertRaises(NameError):
8519+
evaluate_forward_ref(typing.ForwardRef("Tx"), owner=int)
8520+
8521+
(Tx,) = Gen.__type_params__
8522+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=Gen.__type_params__), Tx)
8523+
8524+
# For this test its important that Tx is not a global variable, i.e. do not use "T" here
8525+
self.assertNotIn("Tx", globals())
8526+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), owner=Gen), Tx)
8527+
8528+
# Different type_params take precedence
8529+
not_Tx = TypeVar("Tx") # different TypeVar with same name
8530+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=(not_Tx,), owner=Gen), not_Tx)
8531+
8532+
# globals can take higher precedence
8533+
if _FORWARD_REF_HAS_CLASS:
8534+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx", is_class=True), owner=Gen, globals={"Tx": str}), str)
8535+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx", is_class=True), owner=Gen, type_params=(not_Tx,), globals={"Tx": str}), str)
8536+
8537+
with self.assertRaises(NameError):
8538+
evaluate_forward_ref(typing.ForwardRef("alias"), type_params=Gen.__type_params__)
8539+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen), int)
8540+
# If you pass custom locals, we don't look at the owner's locals
8541+
with self.assertRaises(NameError):
8542+
evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen, locals={})
8543+
# But if the name exists in the locals, it works
8544+
self.assertIs(
8545+
evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen, locals={"alias": str}), str
8546+
)
8547+
8548+
@skipUnless(
8549+
HAS_FORWARD_MODULE, "Needs module 'forward' to test forward references"
8550+
)
8551+
def test_fwdref_with_module(self):
8552+
self.assertIs(
8553+
evaluate_forward_ref(typing.ForwardRef("Counter", module="collections")), collections.Counter
8554+
)
8555+
self.assertEqual(
8556+
evaluate_forward_ref(typing.ForwardRef("Counter[int]", module="collections")),
8557+
collections.Counter[int],
8558+
)
8559+
8560+
with self.assertRaises(NameError):
8561+
# If globals are passed explicitly, we don't look at the module dict
8562+
evaluate_forward_ref(typing.ForwardRef("Format", module="annotationlib"), globals={})
8563+
8564+
def test_fwdref_to_builtin(self):
8565+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), int)
8566+
if HAS_FORWARD_MODULE:
8567+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int", module="collections")), int)
8568+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), owner=str), int)
8569+
8570+
# builtins are still searched with explicit globals
8571+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={}), int)
8572+
8573+
def test_fwdref_with_globals(self):
8574+
# explicit values in globals have precedence
8575+
obj = object()
8576+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": obj}), obj)
8577+
8578+
def test_fwdref_value_is_cached(self):
8579+
fr = typing.ForwardRef("hello")
8580+
with self.assertRaises(NameError):
8581+
evaluate_forward_ref(fr)
8582+
self.assertIs(evaluate_forward_ref(fr, globals={"hello": str}), str)
8583+
self.assertIs(evaluate_forward_ref(fr), str)
8584+
8585+
@skipUnless(TYPING_3_9_0, "Needs PEP 585 support")
8586+
def test_fwdref_with_owner(self):
8587+
self.assertEqual(
8588+
evaluate_forward_ref(typing.ForwardRef("Counter[int]"), owner=collections),
8589+
collections.Counter[int],
8590+
)
8591+
8592+
def test_name_lookup_without_eval(self):
8593+
# test the codepath where we look up simple names directly in the
8594+
# namespaces without going through eval()
8595+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), int)
8596+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), locals={"int": str}), str)
8597+
self.assertIs(
8598+
evaluate_forward_ref(typing.ForwardRef("int"), locals={"int": float}, globals={"int": str}),
8599+
float,
8600+
)
8601+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": str}), str)
8602+
import builtins
8603+
8604+
from test import support
8605+
with support.swap_attr(builtins, "int", dict):
8606+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), dict)
8607+
8608+
def test_nested_strings(self):
8609+
# This variable must have a different name TypeVar
8610+
Tx = TypeVar("Tx")
8611+
8612+
class Y(Generic[Tx]):
8613+
a = "X"
8614+
bT = "Y[T_nonlocal]"
8615+
8616+
Z = TypeAliasType("Z", Y[Tx], type_params=(Tx,))
8617+
8618+
evaluated_ref1a = evaluate_forward_ref(typing.ForwardRef("Y[Y['Tx']]"), locals={"Y": Y, "Tx": Tx})
8619+
self.assertEqual(get_origin(evaluated_ref1a), Y)
8620+
self.assertEqual(get_args(evaluated_ref1a), (Y[Tx],))
8621+
8622+
evaluated_ref1b = evaluate_forward_ref(
8623+
typing.ForwardRef("Y[Y['Tx']]"), locals={"Y": Y}, type_params=(Tx,)
8624+
)
8625+
self.assertEqual(get_origin(evaluated_ref1b), Y)
8626+
self.assertEqual(get_args(evaluated_ref1b), (Y[Tx],))
8627+
8628+
with self.subTest("nested string of TypeVar"):
8629+
evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y})
8630+
self.assertEqual(get_origin(evaluated_ref2), Y)
8631+
if not TYPING_3_9_0:
8632+
self.skipTest("Nested string 'Tx' stays ForwardRef in 3.8")
8633+
self.assertEqual(get_args(evaluated_ref2), (Y[Tx],))
8634+
8635+
with self.subTest("nested string of TypeAliasType and alias"):
8636+
# NOTE: Using Y here works for 3.10
8637+
evaluated_ref3 = evaluate_forward_ref(typing.ForwardRef("""Y['Z["StrAlias"]']"""), locals={"Y": Y, "Z": Z, "StrAlias": str})
8638+
self.assertEqual(get_origin(evaluated_ref3), Y)
8639+
if sys.version_info[:2] in ((3,8), (3, 10)):
8640+
self.skipTest("Nested string 'StrAlias' is not resolved in 3.8 and 3.10")
8641+
self.assertEqual(get_args(evaluated_ref3), (Z[str],))
8642+
8643+
def test_invalid_special_forms(self):
8644+
# tests _lax_type_check to raise errors the same way as the typing module.
8645+
# Regex capture "< class 'module.name'> and "module.name"
8646+
with self.assertRaisesRegex(
8647+
TypeError, r"Plain .*Protocol('>)? is not valid as type argument"
8648+
):
8649+
evaluate_forward_ref(typing.ForwardRef("Protocol"), globals=vars(typing))
8650+
with self.assertRaisesRegex(
8651+
TypeError, r"Plain .*Generic('>)? is not valid as type argument"
8652+
):
8653+
evaluate_forward_ref(typing.ForwardRef("Generic"), globals=vars(typing))
8654+
with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"):
8655+
evaluate_forward_ref(typing.ForwardRef("Final"), globals=vars(typing))
8656+
with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"):
8657+
evaluate_forward_ref(typing.ForwardRef("ClassVar"), globals=vars(typing))
8658+
if _FORWARD_REF_HAS_CLASS:
8659+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_class=True), globals=vars(typing)), Final)
8660+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_class=True), globals=vars(typing)), ClassVar)
8661+
with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"):
8662+
evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing))
8663+
with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"):
8664+
evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing))
8665+
else:
8666+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final)
8667+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)
8668+
84698669

84708670
if __name__ == '__main__':
84718671
main()

0 commit comments

Comments
 (0)