Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ jobs:
cd src
python --version # just to make sure we're running the right one
python -m unittest test_typing_extensions.py
continue-on-error: ${{ endsWith(matrix.python-version, '-dev') }}

- name: Test CPython typing test suite
# Test suite fails on PyPy even without typing_extensions
Expand All @@ -80,7 +79,6 @@ jobs:
# Run the typing test suite from CPython with typing_extensions installed,
# because we monkeypatch typing under some circumstances.
python -c 'import typing_extensions; import test.__main__' test_typing -v
continue-on-error: ${{ endsWith(matrix.python-version, '-dev') }}

linting:
name: Lint
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ New features:
Patch by [Victorien Plot](https://github.com/Viicos).
- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by
Sebastian Rittau.
- Fix tests for Python 3.14. Patch by Jelle Zijlstra.

# Release 4.13.2 (April 10, 2025)

Expand Down
80 changes: 68 additions & 12 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@
runtime_checkable,
)

if sys.version_info >= (3, 14):
from test.support import EqualToForwardRef
else:
EqualToForwardRef = typing.ForwardRef

NoneType = type(None)
T = TypeVar("T")
KT = TypeVar("KT")
Expand Down Expand Up @@ -5152,6 +5157,64 @@ def test_inline(self):
self.assertIs(type(inst), dict)
self.assertEqual(inst["a"], 1)

def test_annotations(self):
# _type_check is applied
with self.assertRaisesRegex(TypeError, "Plain typing.Optional is not valid as type argument"):
class X(TypedDict):
a: Optional

# _type_convert is applied
class Y(TypedDict):
a: None
b: "int"
if sys.version_info >= (3, 14):
import annotationlib

fwdref = EqualToForwardRef('int', module=__name__)
self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref})
self.assertEqual(Y.__annotate__(annotationlib.Format.FORWARDREF), {'a': type(None), 'b': fwdref})
else:
self.assertEqual(Y.__annotations__, {'a': type(None), 'b': typing.ForwardRef('int', module=__name__)})

@skipUnless(TYPING_3_14_0, "Only supported on 3.14")
def test_delayed_type_check(self):
# _type_check is also applied later
class Z(TypedDict):
a: undefined # noqa: F821

with self.assertRaises(NameError):
Z.__annotations__

undefined = Final
with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"):
Z.__annotations__

undefined = None # noqa: F841
self.assertEqual(Z.__annotations__, {'a': type(None)})

@skipUnless(TYPING_3_14_0, "Only supported on 3.14")
def test_deferred_evaluation(self):
class A(TypedDict):
x: NotRequired[undefined] # noqa: F821
y: ReadOnly[undefined] # noqa: F821
z: Required[undefined] # noqa: F821

self.assertEqual(A.__required_keys__, frozenset({'y', 'z'}))
self.assertEqual(A.__optional_keys__, frozenset({'x'}))
self.assertEqual(A.__readonly_keys__, frozenset({'y'}))
self.assertEqual(A.__mutable_keys__, frozenset({'x', 'z'}))

with self.assertRaises(NameError):
A.__annotations__

import annotationlib
self.assertEqual(
A.__annotate__(annotationlib.Format.STRING),
{'x': 'NotRequired[undefined]', 'y': 'ReadOnly[undefined]',
'z': 'Required[undefined]'},
)


class AnnotatedTests(BaseTestCase):

def test_repr(self):
Expand Down Expand Up @@ -5963,7 +6026,7 @@ def test_substitution(self):
U2 = Unpack[Ts]
self.assertEqual(C2[U1], (str, int, str))
self.assertEqual(C2[U2], (str, Unpack[Ts]))
self.assertEqual(C2["U2"], (str, typing.ForwardRef("U2")))
self.assertEqual(C2["U2"], (str, EqualToForwardRef("U2")))

if (3, 12, 0) <= sys.version_info < (3, 12, 4):
with self.assertRaises(AssertionError):
Expand Down Expand Up @@ -7250,8 +7313,8 @@ def test_or(self):
self.assertEqual(X | "x", Union[X, "x"])
self.assertEqual("x" | X, Union["x", X])
# make sure the order is correct
self.assertEqual(get_args(X | "x"), (X, typing.ForwardRef("x")))
self.assertEqual(get_args("x" | X), (typing.ForwardRef("x"), X))
self.assertEqual(get_args(X | "x"), (X, EqualToForwardRef("x")))
self.assertEqual(get_args("x" | X), (EqualToForwardRef("x"), X))

def test_union_constrained(self):
A = TypeVar('A', str, bytes)
Expand Down Expand Up @@ -8819,7 +8882,7 @@ class X:
type_params=None,
format=Format.FORWARDREF,
)
self.assertEqual(evaluated_ref, typing.ForwardRef("doesnotexist2"))
self.assertEqual(evaluated_ref, EqualToForwardRef("doesnotexist2"))

def test_evaluate_with_type_params(self):
# Use a T name that is not in globals
Expand Down Expand Up @@ -8906,13 +8969,6 @@ def test_fwdref_with_globals(self):
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)

def test_fwdref_with_owner(self):
self.assertEqual(
evaluate_forward_ref(typing.ForwardRef("Counter[int]"), owner=collections),
Expand Down Expand Up @@ -8956,7 +9012,7 @@ class Y(Generic[Tx]):
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})
evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y, "Tx": Tx})
self.assertEqual(get_origin(evaluated_ref2), Y)
self.assertEqual(get_args(evaluated_ref2), (Y[Tx],))

Expand Down
67 changes: 56 additions & 11 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import typing
import warnings

if sys.version_info >= (3, 14):
import annotationlib

__all__ = [
# Super-special typing primitives.
'Any',
Expand Down Expand Up @@ -1018,21 +1021,31 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
tp_dict.__orig_bases__ = bases

annotations = {}
own_annotate = None
if "__annotations__" in ns:
own_annotations = ns["__annotations__"]
elif "__annotate__" in ns:
# TODO: Use inspect.VALUE here, and make the annotations lazily evaluated
own_annotations = ns["__annotate__"](1)
elif sys.version_info >= (3, 14):
if hasattr(annotationlib, "get_annotate_from_class_namespace"):
own_annotate = annotationlib.get_annotate_from_class_namespace(ns)
else:
# 3.14.0a7 and earlier
own_annotate = ns.get("__annotate__")
if own_annotate is not None:
own_annotations = annotationlib.call_annotate_function(
own_annotate, Format.FORWARDREF, owner=tp_dict
)
else:
own_annotations = {}
else:
own_annotations = {}
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
if _TAKES_MODULE:
own_annotations = {
own_checked_annotations = {
n: typing._type_check(tp, msg, module=tp_dict.__module__)
for n, tp in own_annotations.items()
}
else:
own_annotations = {
own_checked_annotations = {
n: typing._type_check(tp, msg)
for n, tp in own_annotations.items()
}
Expand All @@ -1045,7 +1058,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
for base in bases:
base_dict = base.__dict__

annotations.update(base_dict.get('__annotations__', {}))
if sys.version_info <= (3, 14):
annotations.update(base_dict.get('__annotations__', {}))
required_keys.update(base_dict.get('__required_keys__', ()))
optional_keys.update(base_dict.get('__optional_keys__', ()))
readonly_keys.update(base_dict.get('__readonly_keys__', ()))
Expand All @@ -1055,8 +1069,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
# is retained for backwards compatibility, but only for Python
# 3.13 and lower.
if (closed and sys.version_info < (3, 14)
and "__extra_items__" in own_annotations):
annotation_type = own_annotations.pop("__extra_items__")
and "__extra_items__" in own_checked_annotations):
annotation_type = own_checked_annotations.pop("__extra_items__")
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
if Required in qualifiers:
raise TypeError(
Expand All @@ -1070,8 +1084,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None,
)
extra_items_type = annotation_type

annotations.update(own_annotations)
for annotation_key, annotation_type in own_annotations.items():
annotations.update(own_checked_annotations)
for annotation_key, annotation_type in own_checked_annotations.items():
qualifiers = set(_get_typeddict_qualifiers(annotation_type))

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

tp_dict.__annotations__ = annotations
if sys.version_info >= (3, 14):
def __annotate__(format):
annos = {}
for base in bases:
if base is Generic:
continue
base_annotate = base.__annotate__
if base_annotate is None:
continue
base_annos = annotationlib.call_annotate_function(
base.__annotate__, format, owner=base)
annos.update(base_annos)
if own_annotate is not None:
own = annotationlib.call_annotate_function(
own_annotate, format, owner=tp_dict)
if format != Format.STRING:
own = {
n: typing._type_check(tp, msg, module=tp_dict.__module__)
for n, tp in own.items()
}
elif format == Format.STRING:
own = annotationlib.annotations_to_string(own_annotations)
elif format in (Format.FORWARDREF, Format.VALUE):
own = own_checked_annotations
else:
raise NotImplementedError(format)
annos.update(own)
return annos

tp_dict.__annotate__ = __annotate__
else:
tp_dict.__annotations__ = annotations
tp_dict.__required_keys__ = frozenset(required_keys)
tp_dict.__optional_keys__ = frozenset(optional_keys)
tp_dict.__readonly_keys__ = frozenset(readonly_keys)
Expand Down
Loading