Skip to content

Commit 572531b

Browse files
committed
Make the attribute non-optional and support kwarg compat.
Also reorganize the test cases and add coverage. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 6be0a11 commit 572531b

File tree

3 files changed

+82
-43
lines changed

3 files changed

+82
-43
lines changed

doc/index.rst

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -396,10 +396,10 @@ Special typing primitives
396396
.. versionadded:: 4.9.0
397397

398398
The experimental ``closed`` keyword argument and the special key
399-
``"__extra_items__"`` proposed in :pep:`728` are supported.
399+
``__extra_items__`` proposed in :pep:`728` are supported.
400400

401401
When ``closed`` is unspecified or ``closed=False`` is given,
402-
``"__extra_items__"`` behave like a regular key. Otherwise, this becomes a
402+
``__extra_items__`` behaves like a regular key. Otherwise, this becomes a
403403
special key that does not show up in ``__readonly_keys__``,
404404
``__mutable_keys__``, ``__required_keys__``, ``__optional_keys``, or
405405
``__annotations__``.
@@ -408,16 +408,22 @@ Special typing primitives
408408

409409
.. attribute:: __closed__
410410

411-
A boolean flag indicating the value of the keyword argument ``closed``
412-
on the current ``TypedDict``.
411+
A boolean flag indicating whether the current ``TypedDict`` is
412+
considered closed. This is not inherited by the ``TypedDict``'s
413+
subclasses.
413414

414415
.. versionadded:: 4.10.0
415416

416417
.. attribute:: __extra_items__
417418

418419
The type annotation of the extra items allowed on the ``TypedDict``.
419-
This attribute does not appear on a TypedDict that has itself and all
420-
its bases non-closed.
420+
This attribute defaults to ``None`` on a TypedDict that has itself and
421+
all its bases non-closed. This default is different from ``type(None)``
422+
that represents ``__extra_items__: None`` defined on a closed
423+
``TypedDict``.
424+
425+
If ``__extra_items__`` is not defined or inherited on a closed
426+
``TypedDict``, this defaults to ``Never``.
421427

422428
.. versionadded:: 4.10.0
423429

@@ -455,7 +461,7 @@ Special typing primitives
455461

456462
.. versionchanged:: 4.10.0
457463

458-
The keyword argument ``closed`` and the special key ``"__extra_items__"``
464+
The keyword argument ``closed`` and the special key ``__extra_items__``
459465
when ``closed=True`` is given were supported.
460466

461467
.. class:: TypeVar(name, *constraints, bound=None, covariant=False,

src/test_typing_extensions.py

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3820,6 +3820,24 @@ class ChildWithInlineAndOptional(Untotal, Inline):
38203820
{'inline': bool, 'untotal': str, 'child': bool},
38213821
)
38223822

3823+
class Closed(TypedDict, closed=True):
3824+
__extra_items__: None
3825+
3826+
class Unclosed(TypedDict, closed=False):
3827+
...
3828+
3829+
class ChildUnclosed(Closed, Unclosed):
3830+
...
3831+
3832+
self.assertFalse(ChildUnclosed.__closed__)
3833+
self.assertEqual(ChildUnclosed.__extra_items__, type(None))
3834+
3835+
class ChildClosed(Unclosed, Closed):
3836+
...
3837+
3838+
self.assertFalse(ChildClosed.__closed__)
3839+
self.assertEqual(ChildClosed.__extra_items__, type(None))
3840+
38233841
wrong_bases = [
38243842
(One, Regular),
38253843
(Regular, One),
@@ -4219,79 +4237,92 @@ def test_regular_extra_items(self):
42194237
class ExtraReadOnly(TypedDict):
42204238
__extra_items__: ReadOnly[str]
42214239

4222-
class ExtraRequired(TypedDict):
4223-
__extra_items__: Required[str]
4224-
4225-
class ExtraNotRequired(TypedDict):
4226-
__extra_items__: NotRequired[str]
4227-
42284240
self.assertEqual(ExtraReadOnly.__required_keys__, frozenset({'__extra_items__'}))
42294241
self.assertEqual(ExtraReadOnly.__optional_keys__, frozenset({}))
42304242
self.assertEqual(ExtraReadOnly.__readonly_keys__, frozenset({'__extra_items__'}))
42314243
self.assertEqual(ExtraReadOnly.__mutable_keys__, frozenset({}))
4244+
self.assertEqual(ExtraReadOnly.__extra_items__, None)
4245+
self.assertFalse(ExtraReadOnly.__closed__)
4246+
4247+
class ExtraRequired(TypedDict):
4248+
__extra_items__: Required[str]
42324249

42334250
self.assertEqual(ExtraRequired.__required_keys__, frozenset({'__extra_items__'}))
42344251
self.assertEqual(ExtraRequired.__optional_keys__, frozenset({}))
42354252
self.assertEqual(ExtraRequired.__readonly_keys__, frozenset({}))
42364253
self.assertEqual(ExtraRequired.__mutable_keys__, frozenset({'__extra_items__'}))
4254+
self.assertEqual(ExtraRequired.__extra_items__, None)
4255+
self.assertFalse(ExtraRequired.__closed__)
4256+
4257+
class ExtraNotRequired(TypedDict):
4258+
__extra_items__: NotRequired[str]
42374259

42384260
self.assertEqual(ExtraNotRequired.__required_keys__, frozenset({}))
42394261
self.assertEqual(ExtraNotRequired.__optional_keys__, frozenset({'__extra_items__'}))
42404262
self.assertEqual(ExtraNotRequired.__readonly_keys__, frozenset({}))
42414263
self.assertEqual(ExtraNotRequired.__mutable_keys__, frozenset({'__extra_items__'}))
4264+
self.assertEqual(ExtraNotRequired.__extra_items__, None)
4265+
self.assertFalse(ExtraNotRequired.__closed__)
42424266

42434267
def test_closed_inheritance(self):
42444268
class Base(TypedDict, closed=True):
42454269
__extra_items__: ReadOnly[Union[str, None]]
4246-
4247-
class Child(Base):
4248-
a: int
4249-
__extra_items__: int
4250-
4251-
class GrandChild(Child, closed=True):
4252-
__extra_items__: str
42534270

42544271
self.assertEqual(Base.__required_keys__, frozenset({}))
42554272
self.assertEqual(Base.__optional_keys__, frozenset({}))
42564273
self.assertEqual(Base.__readonly_keys__, frozenset({}))
42574274
self.assertEqual(Base.__mutable_keys__, frozenset({}))
4275+
self.assertEqual(Base.__annotations__, {})
42584276
self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]])
4277+
self.assertTrue(Base.__closed__)
4278+
4279+
class Child(Base):
4280+
a: int
4281+
__extra_items__: int
42594282

42604283
self.assertEqual(Child.__required_keys__, frozenset({'a', "__extra_items__"}))
42614284
self.assertEqual(Child.__optional_keys__, frozenset({}))
42624285
self.assertEqual(Child.__readonly_keys__, frozenset({}))
42634286
self.assertEqual(Child.__mutable_keys__, frozenset({'a', "__extra_items__"}))
4287+
self.assertEqual(Child.__annotations__, {"__extra_items__": int, "a": int})
42644288
self.assertEqual(Child.__extra_items__, ReadOnly[Union[str, None]])
4289+
self.assertFalse(Child.__closed__)
4290+
4291+
class GrandChild(Child, closed=True):
4292+
__extra_items__: str
42654293

42664294
self.assertEqual(GrandChild.__required_keys__, frozenset({'a', "__extra_items__"}))
42674295
self.assertEqual(GrandChild.__optional_keys__, frozenset({}))
42684296
self.assertEqual(GrandChild.__readonly_keys__, frozenset({}))
42694297
self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a', "__extra_items__"}))
4270-
self.assertEqual(GrandChild.__extra_items__, str)
4271-
4272-
self.assertEqual(Base.__annotations__, {})
4273-
self.assertEqual(Child.__annotations__, {"__extra_items__": int, "a": int})
42744298
self.assertEqual(GrandChild.__annotations__, {"__extra_items__": int, "a": int})
4275-
4276-
self.assertTrue(Base.__closed__)
4277-
self.assertFalse(Child.__closed__)
4299+
self.assertEqual(GrandChild.__extra_items__, str)
42784300
self.assertTrue(GrandChild.__closed__)
42794301

4280-
def test_absent_extra_items(self):
4302+
def test_implicit_extra_items(self):
42814303
class Base(TypedDict):
42824304
a: int
4283-
4305+
4306+
self.assertEqual(Base.__extra_items__, None)
4307+
self.assertFalse(Base.__closed__)
4308+
42844309
class ChildA(Base, closed=True):
42854310
...
42864311

4312+
self.assertEqual(ChildA.__extra_items__, Never)
4313+
self.assertTrue(ChildA.__closed__)
4314+
42874315
class ChildB(Base, closed=True):
42884316
__extra_items__: None
4289-
4290-
self.assertNotIn("__extra_items__", Base.__dict__)
4291-
self.assertIn("__extra_items__", ChildA.__dict__)
4292-
self.assertIn("__extra_items__", ChildB.__dict__)
4293-
self.assertEqual(ChildA.__extra_items__, Never)
4317+
42944318
self.assertEqual(ChildB.__extra_items__, type(None))
4319+
self.assertTrue(ChildB.__closed__)
4320+
4321+
def test_backwards_compatibility(self):
4322+
with self.assertWarns(DeprecationWarning):
4323+
TD = TypedDict("TD", closed=int)
4324+
self.assertFalse(TD.__closed__)
4325+
self.assertEqual(TD.__annotations__, {"closed": int})
42954326

42964327

42974328
class AnnotatedTests(BaseTestCase):

src/typing_extensions.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -920,7 +920,7 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False):
920920
optional_keys = set()
921921
readonly_keys = set()
922922
mutable_keys = set()
923-
extra_items_type = _marker
923+
extra_items_type = None
924924

925925
for base in bases:
926926
base_dict = base.__dict__
@@ -930,18 +930,18 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False):
930930
optional_keys.update(base_dict.get('__optional_keys__', ()))
931931
readonly_keys.update(base_dict.get('__readonly_keys__', ()))
932932
mutable_keys.update(base_dict.get('__mutable_keys__', ()))
933-
if '__extra_items__' in base_dict:
934-
extra_items_type = base_dict['__extra_items__']
935-
936-
if closed and extra_items_type is _marker:
933+
if (base_extra_items_type := base_dict.get('__extra_items__', None)) is not None:
934+
extra_items_type = base_extra_items_type
935+
936+
if closed and extra_items_type is None:
937937
extra_items_type = Never
938938
if closed and "__extra_items__" in own_annotations:
939939
annotation_type = own_annotations.pop("__extra_items__")
940940
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
941941
if Required in qualifiers or NotRequired in qualifiers:
942942
raise TypeError(
943-
f"Special key __extra_items__ does not support"
944-
" Required and NotRequired"
943+
"Special key __extra_items__ does not support "
944+
"Required and NotRequired"
945945
)
946946
extra_items_type = annotation_type
947947

@@ -972,8 +972,7 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False):
972972
if not hasattr(tp_dict, '__total__'):
973973
tp_dict.__total__ = total
974974
tp_dict.__closed__ = closed
975-
if extra_items_type is not _marker:
976-
tp_dict.__extra_items__ = extra_items_type
975+
tp_dict.__extra_items__ = extra_items_type
977976
return tp_dict
978977

979978
__call__ = dict # static method
@@ -1047,6 +1046,9 @@ class Point2D(TypedDict):
10471046
"using the functional syntax, pass an empty dictionary, e.g. "
10481047
) + example + "."
10491048
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
1049+
if closed is not False and closed is not True:
1050+
kwargs["closed"] = closed
1051+
closed = False
10501052
fields = kwargs
10511053
elif kwargs:
10521054
raise TypeError("TypedDict takes either a dict or keyword arguments,"

0 commit comments

Comments
 (0)