Skip to content

Commit fcf5265

Browse files
Backport evaluate_forward_ref() changes (python#611)
Refer to python/cpython#133961 I copied the tests from Python 3.14. Two don't pass but could probably be made to pass by backporting more of annotationlib, but that's more than I think we should do now. Fixes python#608
1 parent fadc1ed commit fcf5265

File tree

3 files changed

+158
-99
lines changed

3 files changed

+158
-99
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
on Python versions <3.10. PEP 604 was introduced in Python 3.10, and
55
`typing_extensions` does not generally attempt to backport PEP-604 methods
66
to prior versions.
7+
- Further update `typing_extensions.evaluate_forward_ref` with changes in Python 3.14.
78

89
# Release 4.14.0rc1 (May 24, 2025)
910

src/test_typing_extensions.py

Lines changed: 149 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8944,7 +8944,147 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self):
89448944
set(results.generic_func.__type_params__)
89458945
)
89468946

8947-
class TestEvaluateForwardRefs(BaseTestCase):
8947+
8948+
class EvaluateForwardRefTests(BaseTestCase):
8949+
def test_evaluate_forward_ref(self):
8950+
int_ref = typing_extensions.ForwardRef('int')
8951+
self.assertIs(typing_extensions.evaluate_forward_ref(int_ref), int)
8952+
self.assertIs(
8953+
typing_extensions.evaluate_forward_ref(int_ref, type_params=()),
8954+
int,
8955+
)
8956+
self.assertIs(
8957+
typing_extensions.evaluate_forward_ref(int_ref, format=typing_extensions.Format.VALUE),
8958+
int,
8959+
)
8960+
self.assertIs(
8961+
typing_extensions.evaluate_forward_ref(
8962+
int_ref, format=typing_extensions.Format.FORWARDREF,
8963+
),
8964+
int,
8965+
)
8966+
self.assertEqual(
8967+
typing_extensions.evaluate_forward_ref(
8968+
int_ref, format=typing_extensions.Format.STRING,
8969+
),
8970+
'int',
8971+
)
8972+
8973+
def test_evaluate_forward_ref_undefined(self):
8974+
missing = typing_extensions.ForwardRef('missing')
8975+
with self.assertRaises(NameError):
8976+
typing_extensions.evaluate_forward_ref(missing)
8977+
self.assertIs(
8978+
typing_extensions.evaluate_forward_ref(
8979+
missing, format=typing_extensions.Format.FORWARDREF,
8980+
),
8981+
missing,
8982+
)
8983+
self.assertEqual(
8984+
typing_extensions.evaluate_forward_ref(
8985+
missing, format=typing_extensions.Format.STRING,
8986+
),
8987+
"missing",
8988+
)
8989+
8990+
def test_evaluate_forward_ref_nested(self):
8991+
ref = typing_extensions.ForwardRef("Union[int, list['str']]")
8992+
ns = {"Union": Union}
8993+
if sys.version_info >= (3, 11):
8994+
expected = Union[int, list[str]]
8995+
else:
8996+
expected = Union[int, list['str']] # TODO: evaluate nested forward refs in Python < 3.11
8997+
self.assertEqual(
8998+
typing_extensions.evaluate_forward_ref(ref, globals=ns),
8999+
expected,
9000+
)
9001+
self.assertEqual(
9002+
typing_extensions.evaluate_forward_ref(
9003+
ref, globals=ns, format=typing_extensions.Format.FORWARDREF
9004+
),
9005+
expected,
9006+
)
9007+
self.assertEqual(
9008+
typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.STRING),
9009+
"Union[int, list['str']]",
9010+
)
9011+
9012+
why = typing_extensions.ForwardRef('"\'str\'"')
9013+
self.assertIs(typing_extensions.evaluate_forward_ref(why), str)
9014+
9015+
@skipUnless(sys.version_info >= (3, 10), "Relies on PEP 604")
9016+
def test_evaluate_forward_ref_nested_pep604(self):
9017+
ref = typing_extensions.ForwardRef("int | list['str']")
9018+
if sys.version_info >= (3, 11):
9019+
expected = int | list[str]
9020+
else:
9021+
expected = int | list['str'] # TODO: evaluate nested forward refs in Python < 3.11
9022+
self.assertEqual(
9023+
typing_extensions.evaluate_forward_ref(ref),
9024+
expected,
9025+
)
9026+
self.assertEqual(
9027+
typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.FORWARDREF),
9028+
expected,
9029+
)
9030+
self.assertEqual(
9031+
typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.STRING),
9032+
"int | list['str']",
9033+
)
9034+
9035+
def test_evaluate_forward_ref_none(self):
9036+
none_ref = typing_extensions.ForwardRef('None')
9037+
self.assertIs(typing_extensions.evaluate_forward_ref(none_ref), None)
9038+
9039+
def test_globals(self):
9040+
A = "str"
9041+
ref = typing_extensions.ForwardRef('list[A]')
9042+
with self.assertRaises(NameError):
9043+
typing_extensions.evaluate_forward_ref(ref)
9044+
self.assertEqual(
9045+
typing_extensions.evaluate_forward_ref(ref, globals={'A': A}),
9046+
list[str] if sys.version_info >= (3, 11) else list['str'],
9047+
)
9048+
9049+
def test_owner(self):
9050+
ref = typing_extensions.ForwardRef("A")
9051+
9052+
with self.assertRaises(NameError):
9053+
typing_extensions.evaluate_forward_ref(ref)
9054+
9055+
# We default to the globals of `owner`,
9056+
# so it no longer raises `NameError`
9057+
self.assertIs(
9058+
typing_extensions.evaluate_forward_ref(ref, owner=Loop), A
9059+
)
9060+
9061+
@skipUnless(sys.version_info >= (3, 14), "Not yet implemented in Python < 3.14")
9062+
def test_inherited_owner(self):
9063+
# owner passed to evaluate_forward_ref
9064+
ref = typing_extensions.ForwardRef("list['A']")
9065+
self.assertEqual(
9066+
typing_extensions.evaluate_forward_ref(ref, owner=Loop),
9067+
list[A],
9068+
)
9069+
9070+
# owner set on the ForwardRef
9071+
ref = typing_extensions.ForwardRef("list['A']", owner=Loop)
9072+
self.assertEqual(
9073+
typing_extensions.evaluate_forward_ref(ref),
9074+
list[A],
9075+
)
9076+
9077+
@skipUnless(sys.version_info >= (3, 14), "Not yet implemented in Python < 3.14")
9078+
def test_partial_evaluation(self):
9079+
ref = typing_extensions.ForwardRef("list[A]")
9080+
with self.assertRaises(NameError):
9081+
typing_extensions.evaluate_forward_ref(ref)
9082+
9083+
self.assertEqual(
9084+
typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.FORWARDREF),
9085+
list[EqualToForwardRef('A')],
9086+
)
9087+
89489088
def test_global_constant(self):
89499089
if sys.version_info[:3] > (3, 10, 0):
89509090
self.assertTrue(_FORWARD_REF_HAS_CLASS)
@@ -9107,30 +9247,17 @@ class Y(Generic[Tx]):
91079247
self.assertEqual(get_args(evaluated_ref3), (Z[str],))
91089248

91099249
def test_invalid_special_forms(self):
9110-
# tests _lax_type_check to raise errors the same way as the typing module.
9111-
# Regex capture "< class 'module.name'> and "module.name"
9112-
with self.assertRaisesRegex(
9113-
TypeError, r"Plain .*Protocol('>)? is not valid as type argument"
9114-
):
9115-
evaluate_forward_ref(typing.ForwardRef("Protocol"), globals=vars(typing))
9116-
with self.assertRaisesRegex(
9117-
TypeError, r"Plain .*Generic('>)? is not valid as type argument"
9118-
):
9119-
evaluate_forward_ref(typing.ForwardRef("Generic"), globals=vars(typing))
9120-
with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"):
9121-
evaluate_forward_ref(typing.ForwardRef("Final"), globals=vars(typing))
9122-
with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"):
9123-
evaluate_forward_ref(typing.ForwardRef("ClassVar"), globals=vars(typing))
9250+
for name in ("Protocol", "Final", "ClassVar", "Generic"):
9251+
with self.subTest(name=name):
9252+
self.assertIs(
9253+
evaluate_forward_ref(typing.ForwardRef(name), globals=vars(typing)),
9254+
getattr(typing, name),
9255+
)
91249256
if _FORWARD_REF_HAS_CLASS:
91259257
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_class=True), globals=vars(typing)), Final)
91269258
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_class=True), globals=vars(typing)), ClassVar)
9127-
with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"):
9128-
evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing))
9129-
with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"):
9130-
evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing))
9131-
else:
9132-
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final)
9133-
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)
9259+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final)
9260+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)
91349261

91359262

91369263
class TestSentinels(BaseTestCase):

src/typing_extensions.py

Lines changed: 8 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4060,57 +4060,6 @@ def _eval_with_owner(
40604060
forward_ref.__forward_value__ = value
40614061
return value
40624062

4063-
def _lax_type_check(
4064-
value, msg, is_argument=True, *, module=None, allow_special_forms=False
4065-
):
4066-
"""
4067-
A lax Python 3.11+ like version of typing._type_check
4068-
"""
4069-
if hasattr(typing, "_type_convert"):
4070-
if (
4071-
sys.version_info >= (3, 10, 3)
4072-
or (3, 9, 10) < sys.version_info[:3] < (3, 10)
4073-
):
4074-
# allow_special_forms introduced later cpython/#30926 (bpo-46539)
4075-
type_ = typing._type_convert(
4076-
value,
4077-
module=module,
4078-
allow_special_forms=allow_special_forms,
4079-
)
4080-
# module was added with bpo-41249 before is_class (bpo-46539)
4081-
elif "__forward_module__" in typing.ForwardRef.__slots__:
4082-
type_ = typing._type_convert(value, module=module)
4083-
else:
4084-
type_ = typing._type_convert(value)
4085-
else:
4086-
if value is None:
4087-
return type(None)
4088-
if isinstance(value, str):
4089-
return ForwardRef(value)
4090-
type_ = value
4091-
invalid_generic_forms = (Generic, Protocol)
4092-
if not allow_special_forms:
4093-
invalid_generic_forms += (ClassVar,)
4094-
if is_argument:
4095-
invalid_generic_forms += (Final,)
4096-
if (
4097-
isinstance(type_, typing._GenericAlias)
4098-
and get_origin(type_) in invalid_generic_forms
4099-
):
4100-
raise TypeError(f"{type_} is not valid as type argument") from None
4101-
if type_ in (Any, LiteralString, NoReturn, Never, Self, TypeAlias):
4102-
return type_
4103-
if allow_special_forms and type_ in (ClassVar, Final):
4104-
return type_
4105-
if (
4106-
isinstance(type_, (_SpecialForm, typing._SpecialForm))
4107-
or type_ in (Generic, Protocol)
4108-
):
4109-
raise TypeError(f"Plain {type_} is not valid as type argument") from None
4110-
if type(type_) is tuple: # lax version with tuple instead of callable
4111-
raise TypeError(f"{msg} Got {type_!r:.100}.")
4112-
return type_
4113-
41144063
def evaluate_forward_ref(
41154064
forward_ref,
41164065
*,
@@ -4163,24 +4112,15 @@ def evaluate_forward_ref(
41634112
else:
41644113
raise
41654114

4166-
msg = "Forward references must evaluate to types."
4167-
if not _FORWARD_REF_HAS_CLASS:
4168-
allow_special_forms = not forward_ref.__forward_is_argument__
4169-
else:
4170-
allow_special_forms = forward_ref.__forward_is_class__
4171-
type_ = _lax_type_check(
4172-
value,
4173-
msg,
4174-
is_argument=forward_ref.__forward_is_argument__,
4175-
allow_special_forms=allow_special_forms,
4176-
)
4115+
if isinstance(value, str):
4116+
value = ForwardRef(value)
41774117

41784118
# Recursively evaluate the type
4179-
if isinstance(type_, ForwardRef):
4180-
if getattr(type_, "__forward_module__", True) is not None:
4119+
if isinstance(value, ForwardRef):
4120+
if getattr(value, "__forward_module__", True) is not None:
41814121
globals = None
41824122
return evaluate_forward_ref(
4183-
type_,
4123+
value,
41844124
globals=globals,
41854125
locals=locals,
41864126
type_params=type_params, owner=owner,
@@ -4194,28 +4134,19 @@ def evaluate_forward_ref(
41944134
locals[tvar.__name__] = tvar
41954135
if sys.version_info < (3, 12, 5):
41964136
return typing._eval_type(
4197-
type_,
4137+
value,
41984138
globals,
41994139
locals,
42004140
recursive_guard=_recursive_guard | {forward_ref.__forward_arg__},
42014141
)
4202-
if sys.version_info < (3, 14):
4142+
else:
42034143
return typing._eval_type(
4204-
type_,
4144+
value,
42054145
globals,
42064146
locals,
42074147
type_params,
42084148
recursive_guard=_recursive_guard | {forward_ref.__forward_arg__},
42094149
)
4210-
return typing._eval_type(
4211-
type_,
4212-
globals,
4213-
locals,
4214-
type_params,
4215-
recursive_guard=_recursive_guard | {forward_ref.__forward_arg__},
4216-
format=format,
4217-
owner=owner,
4218-
)
42194150

42204151

42214152
class Sentinel:

0 commit comments

Comments
 (0)