diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 0e039de8ae05b3a..2124e76514f1af0 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -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; diff --git a/Include/internal/pycore_typeobject.h b/Include/internal/pycore_typeobject.h index 402f168547e3e8f..3661f171e2b013a 100644 --- a/Include/internal/pycore_typeobject.h +++ b/Include/internal/pycore_typeobject.h @@ -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 diff --git a/Lib/test/test_asyncio/test_eager_task_factory.py b/Lib/test/test_asyncio/test_eager_task_factory.py index da79ee9260a0e39..0561b54a3f19c66 100644 --- a/Lib/test/test_asyncio/test_eager_task_factory.py +++ b/Lib/test/test_asyncio/test_eager_task_factory.py @@ -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 @@ -329,6 +327,7 @@ class DummyLoop: pass l = DummyLoop() + _set_running_loop(l) _swap_current_task(l, DummyTask()) t = _swap_current_task(l, None) """ diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 1c8b2978aa3f094..428089d88cc5ad3 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -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 @@ -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) @@ -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"): @@ -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): diff --git a/Lib/typing.py b/Lib/typing.py index df84e2c8764d9ca..4311a77b8db7495 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -141,6 +141,7 @@ 'no_type_check', 'no_type_check_decorator', 'NoDefault', + 'NoExtraItems', 'NoReturn', 'NotRequired', 'overload', @@ -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) @@ -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, @@ -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,) @@ -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 @@ -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 @@ -3274,6 +3308,32 @@ 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() @@ -3281,7 +3341,8 @@ class DatabaseUser(TypedDict): # 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 diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-10-01-52-42.gh-issue-132042.fePwlj.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-10-01-52-42.gh-issue-132042.fePwlj.rst new file mode 100644 index 000000000000000..cd37a93b3a5af7c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-10-01-52-42.gh-issue-132042.fePwlj.rst @@ -0,0 +1,2 @@ +Improve class creation times by up to 12% by pre-computing type slots +just once. Patch by Sergey Miryanov. diff --git a/Misc/NEWS.d/next/Library/2025-08-18-07-10-55.gh-issue-137840.9b7AnG.rst b/Misc/NEWS.d/next/Library/2025-08-18-07-10-55.gh-issue-137840.9b7AnG.rst new file mode 100644 index 000000000000000..5ea25a862043185 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-18-07-10-55.gh-issue-137840.9b7AnG.rst @@ -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. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 7d03655e77a0bbc..9398bcb29c83e44 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -11422,6 +11422,11 @@ static pytype_slotdef slotdefs[] = { {NULL} }; +/* Stores the number of times where slotdefs has elements with same name. + This counter precalculated by _PyType_InitSlotDefs() when the main + interpreter starts. */ +static uint8_t slotdefs_name_counts[Py_ARRAY_LENGTH(slotdefs)]; + /* Given a type pointer and an offset gotten from a slotdef entry, return a pointer to the actual slot. This is not quite the same as simply adding the offset to the type pointer, since it takes care to indirect through the @@ -11464,61 +11469,6 @@ slotptr(PyTypeObject *type, int ioffset) return (void **)ptr; } -/* Return a slot pointer for a given name, but ONLY if the attribute has - exactly one slot function. The name must be an interned string. */ -static void ** -resolve_slotdups(PyTypeObject *type, PyObject *name) -{ - /* XXX Maybe this could be optimized more -- but is it worth it? */ - -#ifdef Py_GIL_DISABLED - pytype_slotdef *ptrs[MAX_EQUIV]; - pytype_slotdef **pp = ptrs; - /* Collect all slotdefs that match name into ptrs. */ - for (pytype_slotdef *p = slotdefs; p->name_strobj; p++) { - if (p->name_strobj == name) - *pp++ = p; - } - *pp = NULL; -#else - /* pname and ptrs act as a little cache */ - PyInterpreterState *interp = _PyInterpreterState_GET(); -#define pname _Py_INTERP_CACHED_OBJECT(interp, type_slots_pname) -#define ptrs _Py_INTERP_CACHED_OBJECT(interp, type_slots_ptrs) - pytype_slotdef *p, **pp; - - if (pname != name) { - /* Collect all slotdefs that match name into ptrs. */ - pname = name; - pp = ptrs; - for (p = slotdefs; p->name_strobj; p++) { - if (p->name_strobj == name) - *pp++ = p; - } - *pp = NULL; - } -#endif - - /* Look in all slots of the type matching the name. If exactly one of these - has a filled-in slot, return a pointer to that slot. - Otherwise, return NULL. */ - void **res, **ptr; - res = NULL; - for (pp = ptrs; *pp; pp++) { - ptr = slotptr(type, (*pp)->offset); - if (ptr == NULL || *ptr == NULL) - continue; - if (res != NULL) - return NULL; - res = ptr; - } -#ifndef Py_GIL_DISABLED -#undef pname -#undef ptrs -#endif - return res; -} - // Return true if "name" corresponds to at least one slot definition. This is // a more accurate but more expensive test compared to is_dunder_name(). static bool @@ -11645,7 +11595,15 @@ update_one_slot(PyTypeObject *type, pytype_slotdef *p, pytype_slotdef **next_p, } if (Py_IS_TYPE(descr, &PyWrapperDescr_Type) && ((PyWrapperDescrObject *)descr)->d_base->name_strobj == p->name_strobj) { - void **tptr = resolve_slotdups(type, p->name_strobj); + void **tptr; + size_t index = (p - slotdefs) / sizeof(slotdefs[0]); + if (slotdefs_name_counts[index] == 1) { + tptr = slotptr(type, p->offset); + } + else { + tptr = NULL; + } + if (tptr == NULL || tptr == ptr) generic = p->function; d = (PyWrapperDescrObject *)descr; @@ -11858,6 +11816,76 @@ update_all_slots(PyTypeObject* type) #endif +int +_PyType_InitSlotDefs(PyInterpreterState *interp) +{ + if (!_Py_IsMainInterpreter(interp)) { + return 0; + } + PyObject *bytearray = NULL; + PyObject *cache = PyDict_New(); + if (!cache) { + return -1; + } + + pytype_slotdef *p; + Py_ssize_t idx = 0; + for (p = slotdefs; p->name_strobj; p++, idx++) { + assert(idx < 255); + + if (PyDict_GetItemRef(cache, p->name_strobj, &bytearray) < 0) { + goto error; + } + + if (!bytearray) { + Py_ssize_t size = sizeof(uint8_t) * (1 + MAX_EQUIV); + bytearray = PyByteArray_FromStringAndSize(NULL, size); + if (!bytearray) { + goto error; + } + + uint8_t *data = (uint8_t *)PyByteArray_AS_STRING(bytearray); + data[0] = 0; + + if (PyDict_SetItem(cache, p->name_strobj, bytearray) < 0) { + goto error; + } + } + + assert(PyByteArray_CheckExact(bytearray)); + uint8_t *data = (uint8_t *)PyByteArray_AS_STRING(bytearray); + + data[0] += 1; + assert(data[0] < MAX_EQUIV); + + data[data[0]] = (uint8_t)idx; + + Py_CLEAR(bytearray); + } + + memset(slotdefs_name_counts, 0, sizeof(slotdefs_name_counts)); + + Py_ssize_t pos = 0; + PyObject *key = NULL; + PyObject *value = NULL; + while (PyDict_Next(cache, &pos, &key, &value)) { + uint8_t *data = (uint8_t *)PyByteArray_AS_STRING(value); + uint8_t n = data[0]; + for (uint8_t i = 0; i < n; i++) { + uint8_t idx = data[i + 1]; + slotdefs_name_counts[idx] = n; + } + } + + Py_DECREF(cache); + return 0; + +error: + Py_XDECREF(bytearray); + Py_DECREF(cache); + return -1; +} + PyObject * _PyType_GetSlotWrapperNames(void) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 185c9ae752819a4..b813166f167d70b 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -836,6 +836,10 @@ pycore_init_builtins(PyThreadState *tstate) } interp->callable_cache.object__getattribute__ = object__getattribute__; + if (_PyType_InitSlotDefs(interp) < 0) { + return _PyStatus_ERR("failed to init slotdefs"); + } + if (_PyBuiltins_AddExceptions(bimod) < 0) { return _PyStatus_ERR("failed to add exceptions to builtins"); } diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 4fdb7b3cd1abf20..c3b13d69f0de8e0 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -344,6 +344,8 @@ Objects/obmalloc.c - obmalloc_state_main - Objects/obmalloc.c - obmalloc_state_initialized - Objects/typeobject.c - name_op - Objects/typeobject.c - slotdefs - +# It initialized only once when main interpeter starts +Objects/typeobject.c - slotdefs_name_counts - Objects/unicodeobject.c - stripfuncnames - Objects/unicodeobject.c - utf7_category - Objects/unicodeobject.c unicode_decode_call_errorhandler_wchar argparse -