from typing import dataclass_transform
from typing import no_type_check, no_type_check_decorator
from typing import Type
-from typing import NamedTuple, NotRequired, Required, ReadOnly, TypedDict
+from typing import NamedTuple, NotRequired, Required, ReadOnly, TypedDict, NoExtraItems
from typing import IO, TextIO, BinaryIO
from typing import Pattern, Match
from typing import Annotated, ForwardRef
class Wrong(*bases):
pass
+ def test_closed_values(self):
+ class Implicit(TypedDict): ...
+ class ExplicitTrue(TypedDict, closed=True): ...
+ class ExplicitFalse(TypedDict, closed=False): ...
+
+ self.assertIsNone(Implicit.__closed__)
+ self.assertIs(ExplicitTrue.__closed__, True)
+ self.assertIs(ExplicitFalse.__closed__, False)
+
+ def test_extra_items_class_arg(self):
+ class TD(TypedDict, extra_items=int):
+ a: str
+
+ self.assertIs(TD.__extra_items__, int)
+ self.assertEqual(TD.__annotations__, {'a': str})
+ self.assertEqual(TD.__required_keys__, frozenset({'a'}))
+ self.assertEqual(TD.__optional_keys__, frozenset())
+
+ class NoExtra(TypedDict):
+ a: str
+
+ self.assertIs(NoExtra.__extra_items__, NoExtraItems)
+ self.assertEqual(NoExtra.__annotations__, {'a': str})
+ self.assertEqual(NoExtra.__required_keys__, frozenset({'a'}))
+ self.assertEqual(NoExtra.__optional_keys__, frozenset())
+
def test_is_typeddict(self):
self.assertIs(is_typeddict(Point2D), True)
self.assertIs(is_typeddict(Union[str, int]), False)
},
)
+ def test_closed_inheritance(self):
+ class Base(TypedDict, extra_items=ReadOnly[Union[str, None]]):
+ a: int
+
+ self.assertEqual(Base.__required_keys__, frozenset({"a"}))
+ self.assertEqual(Base.__optional_keys__, frozenset({}))
+ self.assertEqual(Base.__readonly_keys__, frozenset({}))
+ self.assertEqual(Base.__mutable_keys__, frozenset({"a"}))
+ self.assertEqual(Base.__annotations__, {"a": int})
+ self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]])
+ self.assertIsNone(Base.__closed__)
+
+ class Child(Base, extra_items=int):
+ a: str
+
+ self.assertEqual(Child.__required_keys__, frozenset({'a'}))
+ self.assertEqual(Child.__optional_keys__, frozenset({}))
+ self.assertEqual(Child.__readonly_keys__, frozenset({}))
+ self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
+ self.assertEqual(Child.__annotations__, {"a": str})
+ self.assertIs(Child.__extra_items__, int)
+ self.assertIsNone(Child.__closed__)
+
+ class GrandChild(Child, closed=True):
+ a: float
+
+ self.assertEqual(GrandChild.__required_keys__, frozenset({'a'}))
+ self.assertEqual(GrandChild.__optional_keys__, frozenset({}))
+ self.assertEqual(GrandChild.__readonly_keys__, frozenset({}))
+ self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a'}))
+ self.assertEqual(GrandChild.__annotations__, {"a": float})
+ self.assertIs(GrandChild.__extra_items__, NoExtraItems)
+ self.assertIs(GrandChild.__closed__, True)
+
+ class GrandGrandChild(GrandChild):
+ ...
+ self.assertEqual(GrandGrandChild.__required_keys__, frozenset({'a'}))
+ self.assertEqual(GrandGrandChild.__optional_keys__, frozenset({}))
+ self.assertEqual(GrandGrandChild.__readonly_keys__, frozenset({}))
+ self.assertEqual(GrandGrandChild.__mutable_keys__, frozenset({'a'}))
+ self.assertEqual(GrandGrandChild.__annotations__, {"a": float})
+ self.assertIs(GrandGrandChild.__extra_items__, NoExtraItems)
+ self.assertIsNone(GrandGrandChild.__closed__)
+
+ def test_implicit_extra_items(self):
+ class Base(TypedDict):
+ a: int
+
+ self.assertIs(Base.__extra_items__, NoExtraItems)
+ self.assertIsNone(Base.__closed__)
+
+ class ChildA(Base, closed=True):
+ ...
+
+ self.assertEqual(ChildA.__extra_items__, NoExtraItems)
+ self.assertIs(ChildA.__closed__, True)
+
+ def test_cannot_combine_closed_and_extra_items(self):
+ with self.assertRaisesRegex(
+ TypeError,
+ "Cannot combine closed=True and extra_items"
+ ):
+ class TD(TypedDict, closed=True, extra_items=range):
+ x: str
+
def test_annotations(self):
# _type_check is applied
with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"):
class B(typing.Pattern):
pass
+ def test_typed_dict_signature(self):
+ self.assertListEqual(
+ list(inspect.signature(TypedDict).parameters),
+ ['typename', 'fields', 'total', 'closed', 'extra_items']
+ )
+
class AnnotatedTests(BaseTestCase):
'no_type_check',
'no_type_check_decorator',
'NoDefault',
+ 'NoExtraItems',
'NoReturn',
'NotRequired',
'overload',
NamedTuple.__mro_entries__ = _namedtuple_mro_entries
+class _SingletonMeta(type):
+ def __setattr__(cls, attr, value):
+ # TypeError is consistent with the behavior of NoneType
+ raise TypeError(
+ f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}"
+ )
+
+
+class _NoExtraItemsType(metaclass=_SingletonMeta):
+ """The type of the NoExtraItems singleton."""
+
+ __slots__ = ()
+
+ def __new__(cls):
+ return globals().get("NoExtraItems") or object.__new__(cls)
+
+ def __repr__(self):
+ return 'typing.NoExtraItems'
+
+ def __reduce__(self):
+ return 'NoExtraItems'
+
+NoExtraItems = _NoExtraItemsType()
+del _NoExtraItemsType
+del _SingletonMeta
+
+
def _get_typeddict_qualifiers(annotation_type):
while True:
annotation_origin = get_origin(annotation_type)
class _TypedDictMeta(type):
- def __new__(cls, name, bases, ns, total=True):
+ def __new__(cls, name, bases, ns, total=True, closed=None,
+ extra_items=NoExtraItems):
"""Create a new typed dict class object.
This method is called when TypedDict is subclassed,
if type(base) is not _TypedDictMeta and base is not 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 NoExtraItems:
+ raise TypeError(f"Cannot combine closed={closed!r} and extra_items")
if any(issubclass(b, Generic) for b in bases):
generic_base = (Generic,)
tp_dict.__readonly_keys__ = frozenset(readonly_keys)
tp_dict.__mutable_keys__ = frozenset(mutable_keys)
tp_dict.__total__ = total
+ tp_dict.__closed__ = closed
+ tp_dict.__extra_items__ = extra_items
return tp_dict
__call__ = dict # static method
__instancecheck__ = __subclasscheck__
-def TypedDict(typename, fields, /, *, total=True):
+def TypedDict(typename, fields, /, *, total=True, closed=None,
+ extra_items=NoExtraItems):
"""A simple typed namespace. At runtime it is equivalent to a plain dict.
TypedDict creates a dictionary type such that a type checker will expect all
id: ReadOnly[int] # the "id" key must not be modified
username: str # the "username" key can be changed
+ The closed argument controls whether the TypedDict allows additional
+ non-required items during inheritance and assignability checks.
+ If closed=True, the TypedDict does not allow additional items::
+
+ Point2D = TypedDict('Point2D', {'x': int, 'y': int}, closed=True)
+ class Point3D(Point2D):
+ z: int # Type checker error
+
+ Passing closed=False explicitly requests TypedDict's default open behavior.
+ If closed is not provided, the behavior is inherited from the superclass.
+ A type checker is only expected to support a literal False or True as the
+ value of the closed argument.
+
+ The extra_items argument can instead be used to specify the assignable type
+ of unknown non-required keys::
+
+ Point2D = TypedDict('Point2D', {'x': int, 'y': int}, extra_items=int)
+ class Point3D(Point2D):
+ z: int # OK
+ label: str # Type checker error
+
+ The extra_items argument is also inherited through subclassing. It is unset
+ by default, and it may not be used with the closed argument at the same
+ time.
+
+ See PEP 728 for more information about closed and extra_items.
"""
ns = {'__annotations__': dict(fields)}
module = _caller()
# Setting correct module is necessary to make typed dict classes pickleable.
ns['__module__'] = module
- td = _TypedDictMeta(typename, (), ns, total=total)
+ td = _TypedDictMeta(typename, (), ns, total=total, closed=closed,
+ extra_items=extra_items)
td.__orig_bases__ = (TypedDict,)
return td