Skip to content

Commit 63d365a

Browse files
committed
gh-149044: Improve Py_tp_base[s] docs & error message for non-type bases
The initial implementation of PEP 820 worsened the error message when non-types are given as base types in Py_tp_bases & Py_tp_base. Bring back the 'bases must be types' wording and add a 'got' note for easier debugging. Improve slot ID documentation, and soft-deprecate Py_tp_base (as per the PEP).
1 parent 3ca93ab commit 63d365a

8 files changed

Lines changed: 132 additions & 17 deletions

File tree

Doc/c-api/type.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,37 @@ but need extra remarks for use as slots:
670670
671671
.. versionadded:: 3.15
672672
673+
.. c:macro:: Py_tp_bases
674+
675+
:c:member:`Slot ID <PySlot.sl_id>` for type flags, used to set
676+
:c:member:`PyTypeObject.tp_bases`.
677+
678+
The slot can be set to a tuple of type objects which the newly created
679+
type should inherit from, like the "positional arguments" of
680+
a Python :ref:`class definition <class>`.
681+
682+
Alternately, the slot can be set to a single type object to specify
683+
a single base.
684+
The effect is the same as specifying a one-element tuple.
685+
686+
.. versionchanged:: 3.15
687+
688+
Previously, :c:macro:`!Py_tp_bases` required a tuple of types.
689+
690+
.. c:macro:: Py_tp_base
691+
692+
Equivalent to :c:macro:`Py_tp_bases` (with ``s`` at the end).
693+
If both are specified, :c:macro:`!Py_tp_bases` takes priority and
694+
this slot is ignored.
695+
696+
.. versionchanged:: 3.15
697+
698+
Previously, :c:macro:`!Py_tp_base` required a single type, not a tuple.
699+
700+
.. soft-deprecated:: 3.15
701+
702+
When not targetting older Python versions, pefer :c:macro:`!Py_tp_bases`.
703+
673704
The following slots do not correspond to public fields in the
674705
underlying structures:
675706

Doc/c-api/typeobj.rst

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,12 +1936,12 @@ and :c:data:`PyType_Type` effectively act as defaults.)
19361936

19371937
.. c:member:: PyTypeObject* PyTypeObject.tp_base
19381938
1939-
.. corresponding-type-slot:: Py_tp_base
1940-
19411939
An optional pointer to a base type from which type properties are inherited. At
19421940
this level, only single inheritance is supported; multiple inheritance require
19431941
dynamically creating a type object by calling the metatype.
19441942

1943+
For the corresponding slot ID, see :c:macro:`Py_tp_base`.
1944+
19451945
.. note::
19461946

19471947
.. from Modules/xxmodule.c
@@ -2253,17 +2253,12 @@ and :c:data:`PyType_Type` effectively act as defaults.)
22532253

22542254
.. c:member:: PyObject* PyTypeObject.tp_bases
22552255
2256-
.. corresponding-type-slot:: Py_tp_bases
2257-
22582256
Tuple of base types.
22592257

22602258
This field should be set to ``NULL`` and treated as read-only.
22612259
Python will fill it in when the type is :c:func:`initialized <PyType_Ready>`.
22622260

2263-
For dynamically created classes, the :c:data:`Py_tp_bases`
2264-
:c:type:`slot <PyType_Slot>` can be used instead of the *bases* argument
2265-
of :c:func:`PyType_FromSpecWithBases`.
2266-
The argument form is preferred.
2261+
For the corresponding slot ID, see :c:macro:`Py_tp_bases`.
22672262

22682263
.. warning::
22692264

Doc/whatsnew/3.15.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2476,6 +2476,12 @@ New features
24762476
* :c:func:`PyModule_FromDefAndSpec2`
24772477
* :c:func:`PyModule_ExecDef`
24782478

2479+
2480+
The slots :c:macro:`Py_tp_bases` and :c:macro:`Py_tp_base` are now
2481+
equivalent: they can be set either to a single type or a tuple of types.
2482+
The :c:macro:`Py_tp_bases` slot is preferred; the other is ignored if both
2483+
are specified.
2484+
24792485
(Contributed by Petr Viktorin in :gh:`149044`.)
24802486

24812487
* Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and

Lib/test/test_capi/test_misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ def test_tp_bases_slot(self):
924924
def test_tp_bases_slot_none(self):
925925
self.assertRaisesRegex(
926926
TypeError,
927-
"metaclass conflict",
927+
"bases must be types",
928928
_testcapi.create_heapctype_with_none_bases_slot
929929
)
930930

Lib/test/test_capi/test_slots.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,38 @@ def test_repeat_error(self):
312312
_testlimitedcapi.module_from_slots("repeat_exec", FakeSpec())
313313
with self.assertRaisesRegex(SystemError, "multiple"):
314314
_testlimitedcapi.module_from_slots("repeat_gil", FakeSpec())
315+
316+
def test_bases_slots(self):
317+
create = _testlimitedcapi.type_from_base_slots
318+
319+
# Py_tp_bases overrides Py_tp_base
320+
cls = create(base=int, bases=float)
321+
self.assertEqual(cls.mro(), [cls, float, object])
322+
323+
# type is equivalent to one-element tuple
324+
cls = create(base=None, bases=int)
325+
self.assertEqual(cls.mro(), [cls, int, object])
326+
327+
cls = create(base=None, bases=(int,))
328+
self.assertEqual(cls.mro(), [cls, int, object])
329+
330+
cls = create(base=int)
331+
self.assertEqual(cls.mro(), [cls, int, object])
332+
333+
cls = create(base=(int,))
334+
self.assertEqual(cls.mro(), [cls, int, object])
335+
336+
# Tuple of bases works
337+
class Custom:
338+
pass
339+
cls = create(bases=int)
340+
sub = create(base=float, bases=(Custom, cls, int))
341+
self.assertEqual(sub.mro(), [sub, Custom, cls, int, object])
342+
343+
# Reasonable error message for non-types
344+
with self.assertRaisesRegex(TypeError,
345+
"bases must be types; got 'NoneType'"):
346+
create(base=None)
347+
with self.assertRaisesRegex(TypeError,
348+
"bases must be types; got 'str'"):
349+
create(bases="a string")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Improved error message when specifying non-type base classes in
2+
:c:macro:`Py_tp_bases`, :c:macro:`Py_tp_base`, and *bases* argument to
3+
:func:`PyType_FromMetaclass` and other ``PyType_From*`` functions.

Modules/_testlimitedcapi/slots.c

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,13 +607,56 @@ module_from_null_slot(PyObject* Py_UNUSED(module), PyObject *args)
607607
}, spec);
608608
}
609609

610+
611+
612+
static PyObject *
613+
type_from_base_slots(
614+
PyObject *self, PyObject *args, PyObject *kwargs)
615+
{
616+
PyObject *base = NULL;
617+
PyObject *bases = NULL;
618+
if (!PyArg_ParseTupleAndKeywords(
619+
args, kwargs, "|OO",
620+
(char*[]){"base", "bases", NULL},
621+
&base, &bases))
622+
{
623+
return NULL;
624+
}
625+
626+
PySlot empty_slots[] = {
627+
PySlot_END
628+
};
629+
630+
PySlot base_slots[] = {
631+
PySlot_DATA(Py_tp_base, base),
632+
PySlot_END
633+
};
634+
635+
PySlot bases_slots[] = {
636+
PySlot_DATA(Py_tp_bases, bases),
637+
PySlot_END
638+
};
639+
640+
PySlot slots[] = {
641+
PySlot_STATIC_DATA(Py_tp_name, "_testcapi.HeapCTypeWithBases"),
642+
PySlot_UINT64(Py_tp_flags, Py_TPFLAGS_BASETYPE),
643+
PySlot_DATA(Py_slot_subslots, base ? base_slots: empty_slots),
644+
PySlot_DATA(Py_slot_subslots, bases ? bases_slots: empty_slots),
645+
PySlot_END
646+
};
647+
648+
return PyType_FromSlots(slots);
649+
}
650+
610651
static PyMethodDef _TestMethods[] = {
611652
{"type_from_slots", type_from_slots, METH_VARARGS},
612653
{"module_from_gil_slot", module_from_gil_slot, METH_VARARGS},
613654
{"type_from_null_slot", type_from_null_slot, METH_VARARGS},
614655
{"type_from_null_spec_slot", type_from_null_spec_slot, METH_VARARGS},
615656
{"module_from_slots", module_from_slots, METH_VARARGS},
616657
{"module_from_null_slot", module_from_null_slot, METH_VARARGS},
658+
{"type_from_base_slots", _PyCFunction_CAST(type_from_base_slots),
659+
METH_VARARGS | METH_KEYWORDS},
617660
{NULL},
618661
};
619662
static PyMethodDef *TestMethods = _TestMethods;

Objects/typeobject.c

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3712,9 +3712,9 @@ find_best_base(PyObject *bases)
37123712
for (i = 0; i < n; i++) {
37133713
PyObject *base_proto = PyTuple_GET_ITEM(bases, i);
37143714
if (!PyType_Check(base_proto)) {
3715-
PyErr_SetString(
3715+
PyErr_Format(
37163716
PyExc_TypeError,
3717-
"bases must be types");
3717+
"bases must be types; got '%T'", base_proto);
37183718
return NULL;
37193719
}
37203720
PyTypeObject *base_i = (PyTypeObject *)base_proto;
@@ -4162,8 +4162,9 @@ _PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases)
41624162
for (i = 0; i < nbases; i++) {
41634163
tmp = PyTuple_GET_ITEM(bases, i);
41644164
tmptype = Py_TYPE(tmp);
4165-
if (PyType_IsSubtype(winner, tmptype))
4165+
if (PyType_IsSubtype(winner, tmptype)) {
41664166
continue;
4167+
}
41674168
if (PyType_IsSubtype(tmptype, winner)) {
41684169
winner = tmptype;
41694170
continue;
@@ -5524,6 +5525,12 @@ type_from_slots_or_spec(
55245525
}
55255526
}
55265527

5528+
/* Calculate best base, and check that all bases are type objects */
5529+
PyTypeObject *base = find_best_base(bases); // borrowed ref
5530+
if (base == NULL) {
5531+
goto finally;
5532+
}
5533+
55275534
/* Calculate the metaclass */
55285535

55295536
if (!metaclass) {
@@ -5546,11 +5553,6 @@ type_from_slots_or_spec(
55465553
goto finally;
55475554
}
55485555

5549-
/* Calculate best base, and check that all bases are type objects */
5550-
PyTypeObject *base = find_best_base(bases); // borrowed ref
5551-
if (base == NULL) {
5552-
goto finally;
5553-
}
55545556
// find_best_base() should check Py_TPFLAGS_BASETYPE & raise a proper
55555557
// exception, here we just check its work
55565558
assert(_PyType_HasFeature(base, Py_TPFLAGS_BASETYPE));

0 commit comments

Comments
 (0)