diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 4771d0a7781bd6..48eb16bd90834b 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -563,10 +563,10 @@ but need extra remarks for use as slots: :c:member:`Slot ID ` for the name of the type, used to set :c:member:`PyTypeObject.tp_name`. - This slot (or :c:func:`PyType_Spec.name`) is required to create a type. + This slot (or :c:member:`PyType_Spec.name`) is required to create a type. This may not be used in :c:member:`PyType_Spec.slots`. - Use :c:func:`PyType_Spec.name` instead. + Use :c:member:`PyType_Spec.name` instead. .. impl-detail:: @@ -585,7 +585,7 @@ but need extra remarks for use as slots: The value must be positive. This may not be used in :c:member:`PyType_Spec.slots`. - Use :c:func:`PyType_Spec.basicsize` instead. + Use :c:member:`PyType_Spec.basicsize` instead. This slot may not be used with :c:func:`PyType_GetSlot`. Use :c:member:`PyTypeObject.tp_basicsize` instead if needed, but be aware @@ -616,7 +616,7 @@ but need extra remarks for use as slots: :c:macro:`!Py_tp_extra_basicsize` is an error. This may not be used in :c:member:`PyType_Spec.slots`. - Use negative :c:func:`PyType_Spec.basicsize` instead. + Use negative :c:member:`PyType_Spec.basicsize` instead. This slot may not be used with :c:func:`PyType_GetSlot`. @@ -648,7 +648,7 @@ but need extra remarks for use as slots: - With the :c:macro:`Py_TPFLAGS_ITEMS_AT_END` flag. This may not be used in :c:member:`PyType_Spec.slots`. - Use :c:func:`PyType_Spec.itemsize` instead. + Use :c:member:`PyType_Spec.itemsize` instead. This slot may not be used with :c:func:`PyType_GetSlot`. @@ -663,13 +663,44 @@ but need extra remarks for use as slots: :c:func:`PyType_FromSpecWithBases` sets it automatically. This may not be used in :c:member:`PyType_Spec.slots`. - Use negative :c:func:`PyType_Spec.basicsize` instead. + Use negative :c:member:`PyType_Spec.basicsize` instead. This slot may not be used with :c:func:`PyType_GetSlot`. Use :c:func:`PyType_GetFlags` instead. .. versionadded:: 3.15 +.. c:macro:: Py_tp_bases + + :c:member:`Slot ID ` for type flags, used to set + :c:member:`PyTypeObject.tp_bases`. + + The slot can be set to a tuple of type objects which the newly created + type should inherit from, like the "positional arguments" of + a Python :ref:`class definition `. + + Alternately, the slot can be set to a single type object to specify + a single base. + The effect is the same as specifying a one-element tuple. + + .. versionchanged:: 3.15 + + Previously, :c:macro:`!Py_tp_bases` required a tuple of types. + +.. c:macro:: Py_tp_base + + Equivalent to :c:macro:`Py_tp_bases` (with ``s`` at the end). + If both are specified, :c:macro:`!Py_tp_bases` takes priority and + this slot is ignored. + + .. versionchanged:: 3.15 + + Previously, :c:macro:`!Py_tp_base` required a single type, not a tuple. + + .. soft-deprecated:: 3.15 + + When not targetting older Python versions, pefer :c:macro:`!Py_tp_bases`. + The following slots do not correspond to public fields in the underlying structures: diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst index dcc9e243c2f314..16dcb880712d24 100644 --- a/Doc/c-api/typeobj.rst +++ b/Doc/c-api/typeobj.rst @@ -1936,12 +1936,12 @@ and :c:data:`PyType_Type` effectively act as defaults.) .. c:member:: PyTypeObject* PyTypeObject.tp_base - .. corresponding-type-slot:: Py_tp_base - An optional pointer to a base type from which type properties are inherited. At this level, only single inheritance is supported; multiple inheritance require dynamically creating a type object by calling the metatype. + For the corresponding slot ID, see :c:macro:`Py_tp_base`. + .. note:: .. from Modules/xxmodule.c @@ -2253,17 +2253,12 @@ and :c:data:`PyType_Type` effectively act as defaults.) .. c:member:: PyObject* PyTypeObject.tp_bases - .. corresponding-type-slot:: Py_tp_bases - Tuple of base types. This field should be set to ``NULL`` and treated as read-only. Python will fill it in when the type is :c:func:`initialized `. - For dynamically created classes, the :c:data:`Py_tp_bases` - :c:type:`slot ` can be used instead of the *bases* argument - of :c:func:`PyType_FromSpecWithBases`. - The argument form is preferred. + For the corresponding slot ID, see :c:macro:`Py_tp_bases`. .. warning:: diff --git a/Doc/tools/removed-ids.txt b/Doc/tools/removed-ids.txt index 05fc89d9fe2d27..81c0f098e4c8a8 100644 --- a/Doc/tools/removed-ids.txt +++ b/Doc/tools/removed-ids.txt @@ -29,3 +29,7 @@ reference/expressions.html: grammar-token-python-grammar-enclosure reference/expressions.html: grammar-token-python-grammar-list_display reference/expressions.html: grammar-token-python-grammar-parenth_form reference/expressions.html: grammar-token-python-grammar-set_display + +# Moved to a different page +c-api/typeobj.html: c.Py_tp_base +c-api/typeobj.html: c.Py_tp_bases diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 7f9a0f0e286645..db6903da9f6344 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -2476,6 +2476,12 @@ New features * :c:func:`PyModule_FromDefAndSpec2` * :c:func:`PyModule_ExecDef` + + The slots :c:macro:`Py_tp_bases` and :c:macro:`Py_tp_base` are now + equivalent: they can be set either to a single type or a tuple of types. + The :c:macro:`Py_tp_bases` slot is preferred; the other is ignored if both + are specified. + (Contributed by Petr Viktorin in :gh:`149044`.) * Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 3debc6369e89fb..6d84f0b8c305df 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -924,7 +924,7 @@ def test_tp_bases_slot(self): def test_tp_bases_slot_none(self): self.assertRaisesRegex( TypeError, - "metaclass conflict", + "bases must be types", _testcapi.create_heapctype_with_none_bases_slot ) diff --git a/Lib/test/test_capi/test_slots.py b/Lib/test/test_capi/test_slots.py index c78b118712b11d..b8b6d00b5f84d5 100644 --- a/Lib/test/test_capi/test_slots.py +++ b/Lib/test/test_capi/test_slots.py @@ -312,3 +312,38 @@ def test_repeat_error(self): _testlimitedcapi.module_from_slots("repeat_exec", FakeSpec()) with self.assertRaisesRegex(SystemError, "multiple"): _testlimitedcapi.module_from_slots("repeat_gil", FakeSpec()) + + def test_bases_slots(self): + create = _testlimitedcapi.type_from_base_slots + + # Py_tp_bases overrides Py_tp_base + cls = create(base=int, bases=float) + self.assertEqual(cls.mro(), [cls, float, object]) + + # type is equivalent to one-element tuple + cls = create(base=None, bases=int) + self.assertEqual(cls.mro(), [cls, int, object]) + + cls = create(base=None, bases=(int,)) + self.assertEqual(cls.mro(), [cls, int, object]) + + cls = create(base=int) + self.assertEqual(cls.mro(), [cls, int, object]) + + cls = create(base=(int,)) + self.assertEqual(cls.mro(), [cls, int, object]) + + # Tuple of bases works + class Custom: + pass + cls = create(bases=int) + sub = create(base=float, bases=(Custom, cls, int)) + self.assertEqual(sub.mro(), [sub, Custom, cls, int, object]) + + # Reasonable error message for non-types + with self.assertRaisesRegex(TypeError, + "bases must be types; got 'NoneType'"): + create(base=None) + with self.assertRaisesRegex(TypeError, + "bases must be types; got 'str'"): + create(bases="a string") diff --git a/Misc/NEWS.d/next/C_API/2026-06-10-15-22-44.gh-issue-149044.O7KEcs.rst b/Misc/NEWS.d/next/C_API/2026-06-10-15-22-44.gh-issue-149044.O7KEcs.rst new file mode 100644 index 00000000000000..fe0730b1bf87c4 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-06-10-15-22-44.gh-issue-149044.O7KEcs.rst @@ -0,0 +1,3 @@ +Improved error message when specifying non-type base classes in +:c:macro:`Py_tp_bases`, :c:macro:`Py_tp_base`, and *bases* argument to +:c:func:`PyType_FromMetaclass` and other ``PyType_From*`` functions. diff --git a/Modules/_testlimitedcapi/slots.c b/Modules/_testlimitedcapi/slots.c index 7a8d6466e53a09..9abe53d2115464 100644 --- a/Modules/_testlimitedcapi/slots.c +++ b/Modules/_testlimitedcapi/slots.c @@ -607,6 +607,47 @@ module_from_null_slot(PyObject* Py_UNUSED(module), PyObject *args) }, spec); } + + +static PyObject * +type_from_base_slots( + PyObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *base = NULL; + PyObject *bases = NULL; + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "|OO", + (char*[]){"base", "bases", NULL}, + &base, &bases)) + { + return NULL; + } + + PySlot empty_slots[] = { + PySlot_END + }; + + PySlot base_slots[] = { + PySlot_DATA(Py_tp_base, base), + PySlot_END + }; + + PySlot bases_slots[] = { + PySlot_DATA(Py_tp_bases, bases), + PySlot_END + }; + + PySlot slots[] = { + PySlot_STATIC_DATA(Py_tp_name, "_testcapi.HeapCTypeWithBases"), + PySlot_UINT64(Py_tp_flags, Py_TPFLAGS_BASETYPE), + PySlot_DATA(Py_slot_subslots, base ? base_slots: empty_slots), + PySlot_DATA(Py_slot_subslots, bases ? bases_slots: empty_slots), + PySlot_END + }; + + return PyType_FromSlots(slots); +} + static PyMethodDef _TestMethods[] = { {"type_from_slots", type_from_slots, METH_VARARGS}, {"module_from_gil_slot", module_from_gil_slot, METH_VARARGS}, @@ -614,6 +655,8 @@ static PyMethodDef _TestMethods[] = { {"type_from_null_spec_slot", type_from_null_spec_slot, METH_VARARGS}, {"module_from_slots", module_from_slots, METH_VARARGS}, {"module_from_null_slot", module_from_null_slot, METH_VARARGS}, + {"type_from_base_slots", _PyCFunction_CAST(type_from_base_slots), + METH_VARARGS | METH_KEYWORDS}, {NULL}, }; static PyMethodDef *TestMethods = _TestMethods; diff --git a/Objects/typeobject.c b/Objects/typeobject.c index e0464fe6475cfd..12821b134d9709 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -3712,9 +3712,9 @@ find_best_base(PyObject *bases) for (i = 0; i < n; i++) { PyObject *base_proto = PyTuple_GET_ITEM(bases, i); if (!PyType_Check(base_proto)) { - PyErr_SetString( + PyErr_Format( PyExc_TypeError, - "bases must be types"); + "bases must be types; got '%T'", base_proto); return NULL; } PyTypeObject *base_i = (PyTypeObject *)base_proto; @@ -4162,8 +4162,9 @@ _PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases) for (i = 0; i < nbases; i++) { tmp = PyTuple_GET_ITEM(bases, i); tmptype = Py_TYPE(tmp); - if (PyType_IsSubtype(winner, tmptype)) + if (PyType_IsSubtype(winner, tmptype)) { continue; + } if (PyType_IsSubtype(tmptype, winner)) { winner = tmptype; continue; @@ -5524,6 +5525,12 @@ type_from_slots_or_spec( } } + /* Calculate best base, and check that all bases are type objects */ + PyTypeObject *base = find_best_base(bases); // borrowed ref + if (base == NULL) { + goto finally; + } + /* Calculate the metaclass */ if (!metaclass) { @@ -5546,11 +5553,6 @@ type_from_slots_or_spec( goto finally; } - /* Calculate best base, and check that all bases are type objects */ - PyTypeObject *base = find_best_base(bases); // borrowed ref - if (base == NULL) { - goto finally; - } // find_best_base() should check Py_TPFLAGS_BASETYPE & raise a proper // exception, here we just check its work assert(_PyType_HasFeature(base, Py_TPFLAGS_BASETYPE));