8000 Fix tests on Python 3.14 (#592) · python/typing_extensions@11cc786 · GitHub
[go: up one dir, main page]

Skip to content

Commit 11cc786

Browse files
Fix tests on Python 3.14 (#592)
1 parent d44e9cf commit 11cc786

File tree

4 files changed

+162
-25
lines changed

4 files changed

+162
-25
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ jobs:
7070
cd src
7171
python --version # just to make sure we're running the right one
7272
python -m unittest test_typing_extensions.py
73-
continue-on-error: ${{ endsWith(matrix.python-version, '-dev') }}
7473
7574
- name: Test CPython typing test suite
7675
# Test suite fails on PyPy even without typing_extensions
@@ -80,7 +79,6 @@ jobs:
8079
# Run the typing test suite from CPython with typing_extensions installed,
8180
# because we monkeypatch typing under some circumstances.
8281
python -c 'import typing_extensions; import test.__main__' test_typing -v
83-
continue-on-error: ${{ endsWith(matrix.python-version, '-dev') }}
8482
8583
linting:
8684
name: Lint

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ New features:
88
Patch by [Victorien Plot](https://github.com/Viicos).
99
- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by
1010
Sebastian Rittau.
11+
- Fix tests for Python 3.14. Patch by Jelle Zijlstra.
1112

1213
# Release 4.13.2 (April 10, 2025)
1314

src/test_typing_extensions.py

Lines changed: 105 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,48 @@ def assertNotIsSubclass(self, cls, class_or_tuple, msg=None):
439439
raise self.failureException(message)
440440

441441

442+
class EqualToForwardRef:
443+
"""Helper to ease use of annotationlib.ForwardRef in tests.
444+
445+
This checks only attributes that can be set using the constructor.
446+
447+
"""
448+
449+
def __init__(
450+
self,
451+
arg,
452+
*,
453+
module=None,
454+
owner=None,
455+
is_class=False,
456+
):
457+
self.__forward_arg__ = arg
458+
self.__forward_is_class__ = is_class
459+
self.__forward_module__ = module
460+
self.__owner__ = owner
461+
462+
def __eq__(self, other):
463+
if not isinstance(other, (EqualToForwardRef, typing.ForwardRef)):
464+
return NotImplemented
465+
if sys.version_info >= (3, 14) and self.__owne 9E88 r__ != other.__owner__:
466+
return False
467+
return (
468+
self.__forward_arg__ == other.__forward_arg__
469+
and self.__forward_module__ == other.__forward_module__
470+
and self.__forward_is_class__ == other.__forward_is_class__
471+
)
472+
473+
def __repr__(self):
474+
extra = []
475+
if self.__forward_module__ is not None:
476+
extra.append(f", module={self.__forward_module__!r}")
477+
if self.__forward_is_class__:
478+
extra.append(", is_class=True")
479+
if sys.version_info >= (3, 14) and self.__owner__ is not None:
480+
extra.append(f", owner={self.__owner__!r}")
481+
return f"EqualToForwardRef({self.__forward_arg__!r}{''.join(extra)})"
482+
483+
442484
class Employee:
443485
pass
444486

@@ -5152,6 +5194,64 @@ def test_inline(self):
51525194
self.assertIs(type(inst), dict)
51535195
self.assertEqual(inst["a"], 1)
51545196

5197+
def test_annotations(self):
5198+
# _type_check is applied
5199+
with self.assertRaisesRegex(TypeError, "Plain typing.Optional is not valid as type argument"):
5200+
class X(TypedDict):
5201+
a: Optional
5202+
5203+
# _type_convert is applied
5204+
class Y(TypedDict):
5205+
a: None
5206+
b: "int"
5207+
if sys.version_info >= (3, 14):
5208+
import annotationlib
5209+
5210+
fwdref = EqualToForwardRef('int', module=__name__)
5211+
self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref})
5212+
self.assertEqual(Y.__annotate__(annotationlib.Format.FORWARDREF), {'a': type(None), 'b': fwdref})
5213+
else:
5214+
self.assertEqual(Y.__annotations__, {'a': type(None), 'b': typing.ForwardRef('int', module=__name__)})
5215+
5216+
@skipUnless(TYPING_3_14_0, "Only supported on 3.14")
5217+
def test_delayed_type_check(self):
5218+
# _type_check is also applied later
5219+
class Z(TypedDict):
5220+
a: undefined # noqa: F821
5221+
5222+
with self.assertRaises(NameError):
5223+
Z.__annotations__
5224+
5225+
undefined = Final
5226+
with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"):
5227+
Z.__annotations__
5228+
5229+
undefined = None # noqa: F841
5230+
self.assertEqual(Z.__annotations__, {'a': type(None)})
5231+
5232+
@skipUnless(TYPING_3_14_0, "Only supported on 3.14")
5233+
def test_deferred_evaluation(self):
5234+
class A(TypedDict):
5235+
x: NotRequired[undefined] # noqa: F821
5236+
y: ReadOnly[undefined] # noqa: F821
5237+
z: Required[undefined] # noqa: F821
5238+
5239+
self.assertEqual(A.__required_keys__, frozenset({'y', 'z'}))
5240+
self.assertEqual(A.__optional_keys__, frozenset({'x'}))
5241+
self.assertEqual(A.__readonly_keys__, frozenset({'y'}))
5242+
self.assertEqual(A.__mutable_keys__, frozenset({'x', 'z'}))
5243+
5244+
with self.assertRaises(NameError):
5245+
A.__annotations__
5246+
5247+
import annotationlib
5248+
self.assertEqual(
5249+
A.__annotate__(annotationlib.Format.STRING),
5250+
{'x': 'NotRequired[undefined]', 'y': 'ReadOnly[undefined]',
5251+
'z': 'Required[undefined]'},
5252+
)
5253+
5254+
51555255
class AnnotatedTests(BaseTestCase):
51565256

51575257
def test_repr(self):
@@ -5963,7 +6063,7 @@ def test_substitution(self):
59636063
U2 = Unpack[Ts]
59646064
self.assertEqual(C2[U1], (str, int, str))
59656065
self.assertEqual(C2[U2], (str, Unpack[Ts]))
5966-
self.assertEqual(C2["U2"], (str, typing.ForwardRef("U2")))
6066+
self.assertEqual(C2["U2"], (str, EqualToForwardRef("U2")))
59676067

59686068
if (3, 12, 0) <= sys.version_info < (3, 12, 4):
59696069
with self.assertRaises(AssertionError):
@@ -7250,8 +7350,8 @@ def test_or(self):
72507350
self.assertEqual(X | "x", Union[X, "x"])
72517351
self.assertEqual("x" | X, Union["x", X])
72527352
# make sure the order is correct
7253-
self.assertEqual(get_args(X | "x"), (X, typing.ForwardRef("x")))
7254-
self.assertEqual(get_args("x" | X), (typing.ForwardRef("x"), X))
7353+
self.assertEqual(get_args(X | "x"), (X, EqualToForwardRef("x")))
7354+
self.assertEqual(get_args("x" | X), (EqualToForwardRef("x"), X))
72557355

72567356
def test_union_constrained(self):
72577357
A = TypeVar('A', str, bytes)
@@ -8819,7 +8919,7 @@ class X:
88198919
type_params=None,
88208920
format=Format.FORWARDREF,
88218921
)
8822-
self.assertEqual(evaluated_ref, typing.ForwardRef("doesnotexist2"))
8922+
self.assertEqual(evaluated_ref, EqualToForwardRef("doesnotexist2"))
88238923

88248924
def test_evaluate_with_type_params(self):
88258925
# Use a T name that is not in globals
@@ -8906,13 +9006,6 @@ def test_fwdref_with_globals(self):
89069006
obj = object()
89079007
self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": obj}), obj)
89089008

8909-
def test_fwdref_value_is_cached(self):
8910-
fr = typing.ForwardRef("hello")
8911-
with self.assertRaises(NameError):
8912-
evaluate_forward_ref(fr)
8913-
self.assertIs(evaluate_forward_ref(fr, globals={"hello": str}), str)
8914-
self.assertIs(evaluate_forward_ref(fr), str)
8915-
89169009
def test_fwdref_with_owner(self):
89179010
self.assertEqual(
89189011
evaluate_forward_ref(typing.ForwardRef("Counter[int]"), owner=collections),
@@ -8956,7 +9049,7 @@ class Y(Generic[Tx]):
89569049
self.assertEqual(get_args(evaluated_ref1b), (Y[Tx],))
89579050

89589051
with self.subTest("nested string of TypeVar"):
8959-
evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y})
9052+
evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y, "Tx": Tx})
89609053
self.assertEqual(get_origin(evaluated_ref2), Y)
89619054
self.assertEqual(get_args(evaluated_ref2), (Y[Tx],))
89629055

src/typing_extensions.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
import typing
1515
import warnings
1616

17+
if sys.version_info >= (3, 14):
18+
import annotationlib
19+
1720
__all__ = [
1821
# Super-special typing primitives.
1922
'Any',
@@ -1018,21 +1021,31 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
10181021
tp_dict.__orig_bases__ = bases
10191022

10201023
annotations = {}
1024+
own_annotate = None
10211025
if "__annotations__" in ns:
10221026
own_annotations = ns["__annotations__"]
1023-
elif "__annotate__" in ns:
1024-
# TODO: Use inspect.VALUE here, and make the annotations lazily evaluated
1025-
own_annotations = ns["__annotate__"](1)
1027+
elif sys.version_info >= (3, 14):
1028+
if hasattr(annotationlib, "get_annotate_from_class_namespace"):
1029+
own_annotate = annotationlib.get_annotate_from_class_namespace(ns)
1030+
else:
1031+
# 3.14.0a7 and earlier
1032+
own_annotate = ns.get("__annotate__")
1033+
if own_annotate is not None:
1034+
own_annotations = annotationlib.call_annotate_function(
1035+
own_annotate, Format.FORWARDREF, owner=tp_dict
1036+
)
1037+
else:
1038+
own_annotations = {}
10261039
else:
10271040
own_annotations = {}
10281041
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
10291042
if _TAKES_MODULE:
1030-
own_annotations = {
1043+
own_checked_annotations = {
10311044
n: typing._type_check(tp, msg, module=tp_dict.__module__)
10321045
for n, tp in own_annotations.items()
10331046
}
10341047
else:
1035-
own_annotations = {
1048+
own_checked_annotations = {
10361049
n: typing._type_check(tp, msg)
10371050
for n, tp in own_annotations.items()
10381051
}
@@ -1045,7 +1058,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
10451058
for base in bases:
10461059
base_dict = base.__dict__
10471060

1048-
annotations.update(base_dict.get('__annotations__', {}))
1061+
if sys.version_info <= (3, 14):
1062+
annotations.update(base_dict.get('__annotations__', {}))
10491063
required_keys.update(base_dict.get('__required_keys__', ()))
10501064
optional_keys.update(base_dict.get('__optional_keys__', ()))
10511065
readonly_keys.update(base_dict.get('__readonly_keys__', ()))
@@ -1055,8 +1069,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
10551069
# is retained for backwards compatibility, but only for Python
10561070
# 3.13 and lower.
10571071
if (closed and sys.version_info < (3, 14)
1058-
and "__extra_items__" in own_annotations):
1059-
annotation_type = own_annotations.pop("__extra_items__")
1072+
and "__extra_items__" in own_checked_annotations):
1073+
annotation_type = own_checked_annotations.pop("__extra_items__")
10601074
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
10611075
if Required in qualifiers:
10621076
raise TypeError(
@@ -1070,8 +1084,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
10701084
)
10711085
extra_items_type = annotation_type
10721086

1073-
annotations.update(own_annotations)
1074-
for annotation_key, annotation_type in own_annotations.items():
1087+
annotations.update(own_checked_annotations)
1088+
for annotation_key, annotation_type in own_checked_annotations.items():
10751089
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
10761090

10771091
if Required in qualifiers:
@@ -1089,7 +1103,38 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
10891103
mutable_keys.add(annotation_key)
10901104
readonly_keys.discard(annotation_key)
10911105

1092-
tp_dict.__annotations__ = annotations
1106+
if sys.version_info >= (3, 14):
1107+
def __annotate__(format):
1108+
annos = {}
1109+
for base in bases:
1110+
if base is Generic:
1111+
continue
1112+
base_annotate = base.__annotate__
1113+
if base_annotate is None:
1114+
continue
1115+
base_annos = annotationlib.call_annotate_function(
1116+
base.__annotate__, format, owner=base)
1117+
annos.update(base_annos)
1118+
if own_annotate is not None:
1119+
own = annotationlib.call_annotate_function(
1120+
own_annotate, format, owner=tp_dict)
1121+
if format != Format.STRING:
1122+
own = {
1123+
n: typing._type_check(tp, msg, module=tp_dict.__module__)
1124+
for n, tp in own.items()
1125+
}
1126+
elif format == Format.STRING:
1127+
own = annotationlib.annotations_to_string(own_annotations)
1128+
elif format in (Format.FORWARDREF, Format.VALUE):
1129+
own = own_checked_annotations
1130+
else:
1131+
raise NotImplementedError(format)
1132+
annos.update(own)
1133+
return annos
1134+
1135+
tp_dict.__annotate__ = __annotate__
1136+
else:
1137+
tp_dict.__annotations__ = annotations
10931138
tp_dict.__required_keys__ = frozenset(required_keys)
10941139
tp_dict.__optional_keys__ = frozenset(optional_keys)
10951140
tp_dict.__readonly_keys__ = frozenset(readonly_keys)

0 commit comments

Comments
 (0)
0