Skip to content

Commit b05b4a5

Browse files
committed
Support __extra__ for PEP 728.
Signed-off-by: Zixuan James Li <[email protected]>
1 parent 69b48c3 commit b05b4a5

File tree

4 files changed

+54
-0
lines changed

4 files changed

+54
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- Add support for PEP 728, making `__extra__` a reserved key for TypedDict.
4+
Patch by Zixuan James Li.
35
- Speedup `issubclass()` checks against simple runtime-checkable protocols by
46
around 6% (backporting https://github.com/python/cpython/pull/112717, by Alex
57
Waygood).

doc/index.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,10 @@ Special typing primitives
388388
are mutable if they do not carry the :data:`ReadOnly` qualifier.
389389

390390
.. versionadded:: 4.9.0
391+
392+
The experimental reserved key `__extra__` proposed in :pep:`728` is
393+
supported. It annotates the value type of the additional keys that might
394+
appear in the TypedDict.
391395

392396
.. versionchanged:: 4.3.0
393397

@@ -421,6 +425,10 @@ Special typing primitives
421425

422426
Support for the :data:`ReadOnly` qualifier was added.
423427

428+
.. versionchanged:: 4.10.0
429+
430+
The reserved key `__extra__` was supported.
431+
424432
.. class:: TypeVar(name, *constraints, bound=None, covariant=False,
425433
contravariant=False, infer_variance=False, default=...)
426434

src/test_typing_extensions.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4171,6 +4171,43 @@ class AllTheThings(TypedDict):
41714171
self.assertEqual(AllTheThings.__optional_keys__, frozenset({'c', 'd'}))
41724172
self.assertEqual(AllTheThings.__readonly_keys__, frozenset({'a', 'b', 'c'}))
41734173
self.assertEqual(AllTheThings.__mutable_keys__, frozenset({'d'}))
4174+
4175+
def test_extra_keys_non_readonly(self):
4176+
class Base(TypedDict):
4177+
__extra__: str
4178+
4179+
class Child(Base):
4180+
a: int
4181+
4182+
self.assertEqual(Child.__required_keys__, frozenset({'a'}))
4183+
self.assertEqual(Child.__optional_keys__, frozenset({}))
4184+
self.assertEqual(Child.__readonly_keys__, frozenset({}))
4185+
self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
4186+
4187+
def test_extra_keys_readonly(self):
4188+
class Base(TypedDict):
4189+
__extra__: ReadOnly[str]
4190+
4191+
class Child(Base):
4192+
a: int
4193+
4194+
self.assertEqual(Child.__required_keys__, frozenset({'a'}))
4195+
self.assertEqual(Child.__optional_keys__, frozenset({}))
4196+
self.assertEqual(Child.__readonly_keys__, frozenset({}))
4197+
self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
4198+
4199+
def test_extra_key_required(self):
4200+
with self.assertRaisesRegex(
4201+
TypeError,
4202+
"Reserved key __extra__ does not support Required and NotRequired"
4203+
):
4204+
TypedDict("A", {"__extra__": Required[int]})
4205+
4206+
with self.assertRaisesRegex(
4207+
TypeError,
4208+
"Reserved key __extra__ does not support Required and NotRequired"
4209+
):
4210+
TypedDict("A", {"__extra__": NotRequired[int]})
41744211

41754212

41764213
class AnnotatedTests(BaseTestCase):

src/typing_extensions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,13 @@ def __new__(cls, name, bases, ns, *, total=True):
933933
for annotation_key, annotation_type in own_annotations.items():
934934
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
935935

936+
if annotation_key == "__extra__":
937+
if Required in qualifiers or NotRequired in qualifiers:
938+
raise TypeError(
939+
f"Reserved key __extra__ does not support"
940+
" Required and NotRequired"
941+
)
942+
continue
936943
if Required in qualifiers:
937944
required_keys.add(annotation_key)
938945
elif NotRequired in qualifiers:

0 commit comments

Comments
 (0)