Skip to content

Commit e556762

Browse files
committed
Start PEP 728 implementation
1 parent 3ebe884 commit e556762

File tree

2 files changed

+36
-21
lines changed

2 files changed

+36
-21
lines changed

src/test_typing_extensions.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@
126126
# 3.13.0.rc1 fixes a problem with @deprecated
127127
TYPING_3_13_0_RC = sys.version_info[:4] >= (3, 13, 0, "candidate")
128128

129+
TYPING_3_14_0 = sys.version_info[:3] >= (3, 14, 0)
130+
129131
# https://github.com/python/cpython/pull/27017 was backported into some 3.9 and 3.10
130132
# versions, but not all
131133
HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters
@@ -4288,6 +4290,24 @@ class ChildWithInlineAndOptional(Untotal, Inline):
42884290
{'inline': bool, 'untotal': str, 'child': bool},
42894291
)
42904292

4293+
wrong_bases = [
4294+
(One, Regular),
4295+
(Regular, One),
4296+
(One, Two, Regular),
4297+
(Inline, Regular),
4298+
(Untotal, Regular),
4299+
]
4300+
for bases in wrong_bases:
4301+
with self.subTest(bases=bases):
4302+
with self.assertRaisesRegex(
4303+
TypeError,
4304+
'cannot inherit from both a TypedDict type and a non-TypedDict',
4305+
):
4306+
class Wrong(*bases):
4307+
pass
4308+
4309+
@skipIf(TYPING_3_14_0, "only supported on older versions")
4310+
def test_closed_typeddict_compat(self):
42914311
class Closed(TypedDict, closed=True):
42924312
__extra_items__: None
42934313

@@ -4306,22 +4326,6 @@ class ChildClosed(Unclosed, Closed):
43064326
self.assertFalse(ChildClosed.__closed__)
43074327
self.assertEqual(ChildClosed.__extra_items__, type(None))
43084328

4309-
wrong_bases = [
4310-
(One, Regular),
4311-
(Regular, One),
4312-
(One, Two, Regular),
4313-
(Inline, Regular),
4314-
(Untotal, Regular),
4315-
]
4316-
for bases in wrong_bases:
4317-
with self.subTest(bases=bases):
4318-
with self.assertRaisesRegex(
4319-
TypeError,
4320-
'cannot inherit from both a TypedDict type and a non-TypedDict',
4321-
):
4322-
class Wrong(*bases):
4323-
pass
4324-
43254329
def test_is_typeddict(self):
43264330
self.assertIs(is_typeddict(Point2D), True)
43274331
self.assertIs(is_typeddict(Point2Dor3D), True)
@@ -4677,7 +4681,8 @@ class AllTheThings(TypedDict):
46774681
},
46784682
)
46794683

4680-
def test_extra_keys_non_readonly(self):
4684+
@skipIf(TYPING_3_14_0, "Old syntax only supported on <3.14")
4685+
def test_extra_keys_non_readonly_compat(self):
46814686
class Base(TypedDict, closed=True):
46824687
__extra_items__: str
46834688

src/typing_extensions.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ def _get_typeddict_qualifiers(annotation_type):
917917
break
918918

919919
class _TypedDictMeta(type):
920-
def __new__(cls, name, bases, ns, *, total=True, closed=False):
920+
def __new__(cls, name, bases, ns, *, total=True, closed=None, extra_items=None):
921921
"""Create new typed dict class object.
922922
923923
This method is called when TypedDict is subclassed,
@@ -929,6 +929,10 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False):
929929
if type(base) is not _TypedDictMeta and base is not typing.Generic:
930930
raise TypeError('cannot inherit from both a TypedDict type '
931931
'and a non-TypedDict base class')
932+
if closed is not None and extra_items is not None:
933+
raise TypeError("Cannot combine closed=True and extra_items")
934+
elif closed is None:
935+
closed = False
932936

933937
if any(issubclass(b, typing.Generic) for b in bases):
934938
generic_base = (typing.Generic,)
@@ -968,7 +972,7 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False):
968972
optional_keys = set()
969973
readonly_keys = set()
970974
mutable_keys = set()
971-
extra_items_type = None
975+
extra_items_type = extra_items
972976

973977
for base in bases:
974978
base_dict = base.__dict__
@@ -978,13 +982,19 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False):
978982
optional_keys.update(base_dict.get('__optional_keys__', ()))
979983
readonly_keys.update(base_dict.get('__readonly_keys__', ()))
980984
mutable_keys.update(base_dict.get('__mutable_keys__', ()))
981-
base_extra_items_type = base_dict.get('__extra_items__', None)
985+
base_extra_items_type = getattr(base, '__extra_items__', None)
982986
if base_extra_items_type is not None:
983987
extra_items_type = base_extra_items_type
988+
if getattr(base, "__closed__", False) and not closed:
989+
raise TypeError("Child of a closed TypedDict must also be closed")
984990

985991
if closed and extra_items_type is None:
986992
extra_items_type = Never
987-
if closed and "__extra_items__" in own_annotations:
993+
994+
# This was specified in an earlier version of PEP 728. Support
995+
# is retained for backwards compatibility, but only for Python 3.13
996+
# and lower.
997+
if closed and sys.version_info < (3, 14) and "__extra_items__" in own_annotations:
988998
annotation_type = own_annotations.pop("__extra_items__")
989999
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
9901000
if Required in qualifiers:

0 commit comments

Comments
 (0)