Skip to content

Commit 16185e9

Browse files
authored
gh-149044: Improve Py_tp_base[s] docs & error message for non-type bases (GH-151252)
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 6b142ab commit 16185e9

9 files changed

Lines changed: 142 additions & 23 deletions

File tree

Doc/c-api/type.rst

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -563,10 +563,10 @@ but need extra remarks for use as slots:
563563
:c:member:`Slot ID <PySlot.sl_id>` for the name of the type,
564564
used to set :c:member:`PyTypeObject.tp_name`.
565565
566-
This slot (or :c:func:`PyType_Spec.name`) is required to create a type.
566+
This slot (or :c:member:`PyType_Spec.name`) is required to create a type.
567567
568568
This may not be used in :c:member:`PyType_Spec.slots`.
569-
Use :c:func:`PyType_Spec.name` instead.
569+
Use :c:member:`PyType_Spec.name` instead.
570570
571571
.. impl-detail::
572572
@@ -585,7 +585,7 @@ but need extra remarks for use as slots:
585585
The value must be positive.
586586
587587
This may not be used in :c:member:`PyType_Spec.slots`.
588-
Use :c:func:`PyType_Spec.basicsize` instead.
588+
Use :c:member:`PyType_Spec.basicsize` instead.
589589
590590
This slot may not be used with :c:func:`PyType_GetSlot`.
591591
Use :c:member:`PyTypeObject.tp_basicsize` instead if needed, but be aware
@@ -616,7 +616,7 @@ but need extra remarks for use as slots:
616616
:c:macro:`!Py_tp_extra_basicsize` is an error.
617617
618618
This may not be used in :c:member:`PyType_Spec.slots`.
619-
Use negative :c:func:`PyType_Spec.basicsize` instead.
619+
Use negative :c:member:`PyType_Spec.basicsize` instead.
620620
621621
This slot may not be used with :c:func:`PyType_GetSlot`.
622622
@@ -648,7 +648,7 @@ but need extra remarks for use as slots:
648648
- With the :c:macro:`Py_TPFLAGS_ITEMS_AT_END` flag.
649649
650650
This may not be used in :c:member:`PyType_Spec.slots`.
651-
Use :c:func:`PyType_Spec.itemsize` instead.
651+
Use :c:member:`PyType_Spec.itemsize` instead.
652652
653653
This slot may not be used with :c:func:`PyType_GetSlot`.
654654
@@ -663,13 +663,44 @@ but need extra remarks for use as slots:
663663
:c:func:`PyType_FromSpecWithBases` sets it automatically.
664664
665665
This may not be used in :c:member:`PyType_Spec.slots`.
666-
Use negative :c:func:`PyType_Spec.basicsize` instead.
666+
Use negative :c:member:`PyType_Spec.basicsize` instead.
667667
668668
This slot may not be used with :c:func:`PyType_GetSlot`.
669669
Use :c:func:`PyType_GetFlags` instead.
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/tools/removed-ids.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ reference/expressions.html: grammar-token-python-grammar-enclosure
2929
reference/expressions.html: grammar-token-python-grammar-list_display
3030
reference/expressions.html: grammar-token-python-grammar-parenth_form
3131
reference/expressions.html: grammar-token-python-grammar-set_display
32+
33+
# Moved to a different page
34+
c-api/typeobj.html: c.Py_tp_base
35+
c-api/typeobj.html: c.Py_tp_bases

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+
:c: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
@@ -3717,9 +3717,9 @@ find_best_base(PyObject *bases)
37173717
for (i = 0; i < n; i++) {
37183718
PyObject *base_proto = PyTuple_GET_ITEM(bases, i);
37193719
if (!PyType_Check(base_proto)) {
3720-
PyErr_SetString(
3720+
PyErr_Format(
37213721
PyExc_TypeError,
3722-
"bases must be types");
3722+
"bases must be types; got '%T'", base_proto);
37233723
return NULL;
37243724
}
37253725
PyTypeObject *base_i = (PyTypeObject *)base_proto;
@@ -4167,8 +4167,9 @@ _PyType_CalculateMetaclass(PyTypeObject *metatype, PyObject *bases)
41674167
for (i = 0; i < nbases; i++) {
41684168
tmp = PyTuple_GET_ITEM(bases, i);
41694169
tmptype = Py_TYPE(tmp);
4170-
if (PyType_IsSubtype(winner, tmptype))
4170+
if (PyType_IsSubtype(winner, tmptype)) {
41714171
continue;
4172+
}
41724173
if (PyType_IsSubtype(tmptype, winner)) {
41734174
winner = tmptype;
41744175
continue;
@@ -5529,6 +5530,12 @@ type_from_slots_or_spec(
55295530
}
55305531
}
55315532

5533+
/* Calculate best base, and check that all bases are type objects */
5534+
PyTypeObject *base = find_best_base(bases); // borrowed ref
5535+
if (base == NULL) {
5536+
goto finally;
5537+
}
5538+
55325539
/* Calculate the metaclass */
55335540

55345541
if (!metaclass) {
@@ -5551,11 +5558,6 @@ type_from_slots_or_spec(
55515558
goto finally;
55525559
}
55535560

5554-
/* Calculate best base, and check that all bases are type objects */
5555-
PyTypeObject *base = find_best_base(bases); // borrowed ref
5556-
if (base == NULL) {
5557-
goto finally;
5558-
}
55595561
// find_best_base() should check Py_TPFLAGS_BASETYPE & raise a proper
55605562
// exception, here we just check its work
55615563
assert(_PyType_HasFeature(base, Py_TPFLAGS_BASETYPE));

0 commit comments

Comments
 (0)