Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -672,11 +672,6 @@ struct _Py_interp_cached_objects {

/* object.__reduce__ */
PyObject *objreduce;
#ifndef Py_GIL_DISABLED
/* resolve_slotdups() */
PyObject *type_slots_pname;
pytype_slotdef *type_slots_ptrs[MAX_EQUIV];
#endif

/* TypeVar and related types */
PyTypeObject *generic_type;
Expand Down
3 changes: 3 additions & 0 deletions Include/internal/pycore_typeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ typedef int (*_py_validate_type)(PyTypeObject *);
extern int _PyType_Validate(PyTypeObject *ty, _py_validate_type validate, unsigned int *tp_version);
extern int _PyType_CacheGetItemForSpecialization(PyHeapTypeObject *ht, PyObject *descriptor, uint32_t tp_version);

// Precalculates count of non-unique slots and fills wrapperbase.name_count.
extern int _PyType_InitSlotDefs(PyInterpreterState *interp);

#ifdef __cplusplus
}
#endif
Expand Down
5 changes: 2 additions & 3 deletions Lib/test/test_asyncio/test_eager_task_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,9 @@ def tearDown(self):
asyncio.all_tasks = asyncio.tasks.all_tasks = self._all_tasks
return super().tearDown()


@unittest.skip("skip")
def test_issue105987(self):
code = """if 1:
from _asyncio import _swap_current_task
from _asyncio import _swap_current_task, _set_running_loop

class DummyTask:
pass
Expand All @@ -329,6 +327,7 @@ class DummyLoop:
pass

l = DummyLoop()
_set_running_loop(l)
_swap_current_task(l, DummyTask())
t = _swap_current_task(l, None)
"""
Expand Down
99 changes: 98 additions & 1 deletion Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
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
Expand Down Expand Up @@ -8820,6 +8820,32 @@ class ChildWithInlineAndOptional(Untotal, Inline):
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)
Expand Down Expand Up @@ -9147,6 +9173,71 @@ class AllTheThings(TypedDict):
},
)

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"):
Expand Down Expand Up @@ -9376,6 +9467,12 @@ class A(typing.Match):
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):

Expand Down
67 changes: 64 additions & 3 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
'no_type_check',
'no_type_check_decorator',
'NoDefault',
'NoExtraItems',
'NoReturn',
'NotRequired',
'overload',
Expand Down Expand Up @@ -3063,6 +3064,33 @@ def _namedtuple_mro_entries(bases):
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)
Expand All @@ -3086,7 +3114,8 @@ def _get_typeddict_qualifiers(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,
Expand All @@ -3098,6 +3127,8 @@ def __new__(cls, name, bases, ns, total=True):
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,)
Expand Down Expand Up @@ -3209,6 +3240,8 @@ def __annotate__(format):
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
Expand All @@ -3220,7 +3253,8 @@ def __subclasscheck__(cls, other):
__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
Expand Down Expand Up @@ -3274,14 +3308,41 @@ class DatabaseUser(TypedDict):
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()
if module is not None:
# 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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve class creation times by up to 12% by pre-computing type slots
just once. Patch by Sergey Miryanov.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:class:`typing.TypedDict` now supports the ``closed`` and ``extra_items``
keyword arguments (as described in :pep:`728`) to control whether additional
non-required keys are allowed and to specify their value type.
Loading
Loading