Skip to content

Commit a5973ce

Browse files
committed
Backport evaluate_forward_ref() changes
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 #608
1 parent fadc1ed commit a5973ce

3 files changed

Lines changed: 131 additions & 99 deletions

File tree

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: 122 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8944,7 +8944,120 @@ 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("int | list['str']")
8992+
self.assertEqual(
8993+
typing_extensions.evaluate_forward_ref(ref),
8994+
int | list[str],
8995+
)
8996+
self.assertEqual(
8997+
typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.FORWARDREF),
8998+
int | list[str],
8999+
)
9000+
self.assertEqual(
9001+
typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.STRING),
9002+
"int | list['str']",
9003+
)
9004+
9005+
why = typing_extensions.ForwardRef('"\'str\'"')
9006+
self.assertIs(typing_extensions.evaluate_forward_ref(why), str)
9007+
9008+
def test_evaluate_forward_ref_none(self):
9009+
none_ref = typing_extensions.ForwardRef('None')
9010+
self.assertIs(typing_extensions.evaluate_forward_ref(none_ref), None)
9011+
9012+
def test_globals(self):
9013+
A = "str"
9014+
ref = typing_extensions.ForwardRef('list[A]')
9015+
with self.assertRaises(NameError):
9016+
typing_extensions.evaluate_forward_ref(ref)
9017+
self.assertEqual(
9018+
typing_extensions.evaluate_forward_ref(ref, globals={'A': A}),
9019+
list[str],
9020+
)
9021+
9022+
def test_owner(self):
9023+
ref = typing_extensions.ForwardRef("A")
9024+
9025+
with self.assertRaises(NameError):
9026+
typing_extensions.evaluate_forward_ref(ref)
9027+
9028+
# We default to the globals of `owner`,
9029+
# so it no longer raises `NameError`
9030+
self.assertIs(
9031+
typing_extensions.evaluate_forward_ref(ref, owner=Loop), A
9032+
)
9033+
9034+
@skipUnless(sys.version_info >= (3, 14), "Not yet implemented in Python < 3.14")
9035+
def test_inherited_owner(self):
9036+
# owner passed to evaluate_forward_ref
9037+
ref = typing_extensions.ForwardRef("list['A']")
9038+
self.assertEqual(
9039+
typing_extensions.evaluate_forward_ref(ref, owner=Loop),
9040+
list[A],
9041+
)
9042+
9043+
# owner set on the ForwardRef
9044+
ref = typing_extensions.ForwardRef("list['A']", owner=Loop)
9045+
self.assertEqual(
9046+
typing_extensions.evaluate_forward_ref(ref),
9047+
list[A],
9048+
)
9049+
9050+
@skipUnless(sys.version_info >= (3, 14), "Not yet implemented in Python < 3.14")
9051+
def test_partial_evaluation(self):
9052+
ref = typing_extensions.ForwardRef("list[A]")
9053+
with self.assertRaises(NameError):
9054+
typing_extensions.evaluate_forward_ref(ref)
9055+
9056+
self.assertEqual(
9057+
typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.FORWARDREF),
9058+
list[EqualToForwardRef('A')],
9059+
)
9060+
89489061
def test_global_constant(self):
89499062
if sys.version_info[:3] > (3, 10, 0):
89509063
self.assertTrue(_FORWARD_REF_HAS_CLASS)
@@ -9107,30 +9220,17 @@ class Y(Generic[Tx]):
91079220
self.assertEqual(get_args(evaluated_ref3), (Z[str],))
91089221

91099222
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))
9223+
for name in ("Protocol", "Final", "ClassVar", "Generic"):
9224+
with self.subTest(name=name):
9225+
self.assertIs(
9226+
evaluate_forward_ref(typing.ForwardRef(name), globals=vars(typing)),
9227+
getattr(typing, name),
9228+
)
91249229
if _FORWARD_REF_HAS_CLASS:
91259230
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_class=True), globals=vars(typing)), Final)
91269231
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)
9232+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final)
9233+
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)
91349234

91359235

91369236
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)