Skip to content

Commit

Permalink
Start PEP 728 implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra committed Dec 11, 2024
1 parent 3ebe884 commit e556762
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 21 deletions.
39 changes: 22 additions & 17 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@
# 3.13.0.rc1 fixes a problem with @deprecated
TYPING_3_13_0_RC = sys.version_info[:4] >= (3, 13, 0, "candidate")

TYPING_3_14_0 = sys.version_info[:3] >= (3, 14, 0)

# https://github.com/python/cpython/pull/27017 was backported into some 3.9 and 3.10
# versions, but not all
HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters
Expand Down Expand Up @@ -4288,6 +4290,24 @@ class ChildWithInlineAndOptional(Untotal, Inline):
{'inline': bool, 'untotal': str, 'child': bool},
)

wrong_bases = [
(One, Regular),
(Regular, One),
(One, Two, Regular),
(Inline, Regular),
(Untotal, Regular),
]
for bases in wrong_bases:
with self.subTest(bases=bases):
with self.assertRaisesRegex(
TypeError,
'cannot inherit from both a TypedDict type and a non-TypedDict',
):
class Wrong(*bases):
pass

@skipIf(TYPING_3_14_0, "only supported on older versions")
def test_closed_typeddict_compat(self):
class Closed(TypedDict, closed=True):
__extra_items__: None

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

wrong_bases = [
(One, Regular),
(Regular, One),
(One, Two, Regular),
(Inline, Regular),
(Untotal, Regular),
]
for bases in wrong_bases:
with self.subTest(bases=bases):
with self.assertRaisesRegex(
TypeError,
'cannot inherit from both a TypedDict type and a non-TypedDict',
):
class Wrong(*bases):
pass

def test_is_typeddict(self):
self.assertIs(is_typeddict(Point2D), True)
self.assertIs(is_typeddict(Point2Dor3D), True)
Expand Down Expand Up @@ -4677,7 +4681,8 @@ class AllTheThings(TypedDict):
},
)

def test_extra_keys_non_readonly(self):
@skipIf(TYPING_3_14_0, "Old syntax only supported on <3.14")
def test_extra_keys_non_readonly_compat(self):
class Base(TypedDict, closed=True):
__extra_items__: str

Expand Down
18 changes: 14 additions & 4 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ def _get_typeddict_qualifiers(annotation_type):
break

class _TypedDictMeta(type):
def __new__(cls, name, bases, ns, *, total=True, closed=False):
def __new__(cls, name, bases, ns, *, total=True, closed=None, extra_items=None):
"""Create new typed dict class object.
This method is called when TypedDict is subclassed,
Expand All @@ -929,6 +929,10 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False):
if type(base) is not _TypedDictMeta and base is not typing.Generic:
raise TypeError('cannot inherit from both a TypedDict type '
'and a non-TypedDict base class')
if closed is not None and extra_items is not None:
raise TypeError("Cannot combine closed=True and extra_items")
elif closed is None:
closed = False

if any(issubclass(b, typing.Generic) for b in bases):
generic_base = (typing.Generic,)
Expand Down Expand Up @@ -968,7 +972,7 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False):
optional_keys = set()
readonly_keys = set()
mutable_keys = set()
extra_items_type = None
extra_items_type = extra_items

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

if closed and extra_items_type is None:
extra_items_type = Never
if closed and "__extra_items__" in own_annotations:

# This was specified in an earlier version of PEP 728. Support
# 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__")
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
if Required in qualifiers:
Expand Down

0 comments on commit e556762

Please sign in to comment.