Skip to content

Commit 7fef847

Browse files
sebergencukouerlend-aasland
authored
bpo-45383: Get metaclass from bases in PyType_From* (GH-28748)
This checks the bases of of a type created using the FromSpec API to inherit the bases metaclasses. The metaclass's alloc function will be called as is done in `tp_new` for classes created in Python. Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Erlend Egeberg Aasland <erlend.aasland@protonmail.com>
1 parent a5ba0f4 commit 7fef847

File tree

4 files changed

+323
-97
lines changed

4 files changed

+323
-97
lines changed

Doc/c-api/type.rst

+33-3
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,12 @@ The following functions and structs are used to create
193193
.. c:function:: PyObject* PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases)
194194
195195
Create and return a :ref:`heap type <heap-types>` from the *spec*
196-
(:const:`Py_TPFLAGS_HEAPTYPE`).
196+
(see :const:`Py_TPFLAGS_HEAPTYPE`).
197197
198198
The metaclass *metaclass* is used to construct the resulting type object.
199-
When *metaclass* is ``NULL``, the default :c:type:`PyType_Type` is used
200-
instead. Note that metaclasses that override
199+
When *metaclass* is ``NULL``, the metaclass is derived from *bases*
200+
(or *Py_tp_base[s]* slots if *bases* is ``NULL``, see below).
201+
Note that metaclasses that override
201202
:c:member:`~PyTypeObject.tp_new` are not supported.
202203
203204
The *bases* argument can be used to specify base classes; it can either
@@ -215,6 +216,19 @@ The following functions and structs are used to create
215216
216217
This function calls :c:func:`PyType_Ready` on the new type.
217218
219+
Note that this function does *not* fully match the behavior of
220+
calling :py:class:`type() <type>` or using the :keyword:`class` statement.
221+
With user-provided base types or metaclasses, prefer
222+
:ref:`calling <capi-call>` :py:class:`type` (or the metaclass)
223+
over ``PyType_From*`` functions.
224+
Specifically:
225+
226+
* :py:meth:`~object.__new__` is not called on the new class
227+
(and it must be set to ``type.__new__``).
228+
* :py:meth:`~object.__init__` is not called on the new class.
229+
* :py:meth:`~object.__init_subclass__` is not called on any bases.
230+
* :py:meth:`~object.__set_name__` is not called on new descriptors.
231+
218232
.. versionadded:: 3.12
219233
220234
.. c:function:: PyObject* PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
@@ -228,17 +242,33 @@ The following functions and structs are used to create
228242
The function now accepts a single class as the *bases* argument and
229243
``NULL`` as the ``tp_doc`` slot.
230244
245+
.. versionchanged:: 3.12
246+
247+
The function now finds and uses a metaclass corresponding to the provided
248+
base classes. Previously, only :class:`type` instances were returned.
249+
231250
232251
.. c:function:: PyObject* PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)
233252
234253
Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, bases)``.
235254
236255
.. versionadded:: 3.3
237256
257+
.. versionchanged:: 3.12
258+
259+
The function now finds and uses a metaclass corresponding to the provided
260+
base classes. Previously, only :class:`type` instances were returned.
261+
238262
.. c:function:: PyObject* PyType_FromSpec(PyType_Spec *spec)
239263
240264
Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, NULL)``.
241265
266+
.. versionchanged:: 3.12
267+
268+
The function now finds and uses a metaclass corresponding to the
269+
base classes provided in *Py_tp_base[s]* slots.
270+
Previously, only :class:`type` instances were returned.
271+
242272
.. c:type:: PyType_Spec
243273
244274
Structure defining a type's behavior.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The :c:func:`PyType_FromSpec` API will now find and use a metaclass
2+
based on the provided bases.
3+
An error will be raised if there is a metaclass conflict.

Modules/_testcapimodule.c

+160
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,161 @@ test_get_type_name(PyObject *self, PyObject *Py_UNUSED(ignored))
12081208
}
12091209

12101210

1211+
static PyType_Slot empty_type_slots[] = {
1212+
{0, 0},
1213+
};
1214+
1215+
static PyType_Spec MinimalMetaclass_spec = {
1216+
.name = "_testcapi.MinimalMetaclass",
1217+
.basicsize = sizeof(PyHeapTypeObject),
1218+
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
1219+
.slots = empty_type_slots,
1220+
};
1221+
1222+
static PyType_Spec MinimalType_spec = {
1223+
.name = "_testcapi.MinimalSpecType",
1224+
.basicsize = sizeof(PyObject),
1225+
.flags = Py_TPFLAGS_DEFAULT,
1226+
.slots = empty_type_slots,
1227+
};
1228+
1229+
static PyObject *
1230+
test_from_spec_metatype_inheritance(PyObject *self, PyObject *Py_UNUSED(ignored))
1231+
{
1232+
PyObject *metaclass = NULL;
1233+
PyObject *class = NULL;
1234+
PyObject *new = NULL;
1235+
PyObject *subclasses = NULL;
1236+
PyObject *result = NULL;
1237+
int r;
1238+
1239+
metaclass = PyType_FromSpecWithBases(&MinimalMetaclass_spec, (PyObject*)&PyType_Type);
1240+
if (metaclass == NULL) {
1241+
goto finally;
1242+
}
1243+
class = PyObject_CallFunction(metaclass, "s(){}", "TestClass");
1244+
if (class == NULL) {
1245+
goto finally;
1246+
}
1247+
1248+
new = PyType_FromSpecWithBases(&MinimalType_spec, class);
1249+
if (new == NULL) {
1250+
goto finally;
1251+
}
1252+
if (Py_TYPE(new) != (PyTypeObject*)metaclass) {
1253+
PyErr_SetString(PyExc_AssertionError,
1254+
"Metaclass not set properly!");
1255+
goto finally;
1256+
}
1257+
1258+
/* Assert that __subclasses__ is updated */
1259+
subclasses = PyObject_CallMethod(class, "__subclasses__", "");
1260+
if (!subclasses) {
1261+
goto finally;
1262+
}
1263+
r = PySequence_Contains(subclasses, new);
1264+
if (r < 0) {
1265+
goto finally;
1266+
}
1267+
if (r == 0) {
1268+
PyErr_SetString(PyExc_AssertionError,
1269+
"subclasses not set properly!");
1270+
goto finally;
1271+
}
1272+
1273+
result = Py_NewRef(Py_None);
1274+
1275+
finally:
1276+
Py_XDECREF(metaclass);
1277+
Py_XDECREF(class);
1278+
Py_XDECREF(new);
1279+
Py_XDECREF(subclasses);
1280+
return result;
1281+
}
1282+
1283+
1284+
static PyObject *
1285+
test_from_spec_invalid_metatype_inheritance(PyObject *self, PyObject *Py_UNUSED(ignored))
1286+
{
1287+
PyObject *metaclass_a = NULL;
1288+
PyObject *metaclass_b = NULL;
1289+
PyObject *class_a = NULL;
1290+
PyObject *class_b = NULL;
1291+
PyObject *bases = NULL;
1292+
PyObject *new = NULL;
1293+
PyObject *meta_error_string = NULL;
1294+
PyObject *exc_type = NULL;
1295+
PyObject *exc_value = NULL;
1296+
PyObject *exc_traceback = NULL;
1297+
PyObject *result = NULL;
1298+
1299+
metaclass_a = PyType_FromSpecWithBases(&MinimalMetaclass_spec, (PyObject*)&PyType_Type);
1300+
if (metaclass_a == NULL) {
1301+
goto finally;
1302+
}
1303+
metaclass_b = PyType_FromSpecWithBases(&MinimalMetaclass_spec, (PyObject*)&PyType_Type);
1304+
if (metaclass_b == NULL) {
1305+
goto finally;
1306+
}
1307+
class_a = PyObject_CallFunction(metaclass_a, "s(){}", "TestClassA");
1308+
if (class_a == NULL) {
1309+
goto finally;
1310+
}
1311+
1312+
class_b = PyObject_CallFunction(metaclass_b, "s(){}", "TestClassB");
1313+
if (class_b == NULL) {
1314+
goto finally;
1315+
}
1316+
1317+
bases = PyTuple_Pack(2, class_a, class_b);
1318+
if (bases == NULL) {
1319+
goto finally;
1320+
}
1321+
1322+
/*
1323+
* The following should raise a TypeError due to a MetaClass conflict.
1324+
*/
1325+
new = PyType_FromSpecWithBases(&MinimalType_spec, bases);
1326+
if (new != NULL) {
1327+
PyErr_SetString(PyExc_AssertionError,
1328+
"MetaType conflict not recognized by PyType_FromSpecWithBases");
1329+
goto finally;
1330+
}
1331+
1332+
// Assert that the correct exception was raised
1333+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
1334+
PyErr_Fetch(&exc_type, &exc_value, &exc_traceback);
1335+
1336+
meta_error_string = PyUnicode_FromString("metaclass conflict:");
1337+
if (meta_error_string == NULL) {
1338+
goto finally;
1339+
}
1340+
int res = PyUnicode_Contains(exc_value, meta_error_string);
1341+
if (res < 0) {
1342+
goto finally;
1343+
}
1344+
if (res == 0) {
1345+
PyErr_SetString(PyExc_AssertionError,
1346+
"TypeError did not inlclude expected message.");
1347+
goto finally;
1348+
}
1349+
result = Py_NewRef(Py_None);
1350+
}
1351+
finally:
1352+
Py_XDECREF(metaclass_a);
1353+
Py_XDECREF(metaclass_b);
1354+
Py_XDECREF(bases);
1355+
Py_XDECREF(new);
1356+
Py_XDECREF(meta_error_string);
1357+
Py_XDECREF(exc_type);
1358+
Py_XDECREF(exc_value);
1359+
Py_XDECREF(exc_traceback);
1360+
Py_XDECREF(class_a);
1361+
Py_XDECREF(class_b);
1362+
return result;
1363+
}
1364+
1365+
12111366
static PyObject *
12121367
simple_str(PyObject *self) {
12131368
return PyUnicode_FromString("<test>");
@@ -5952,6 +6107,11 @@ static PyMethodDef TestMethods[] = {
59526107
{"test_get_type_name", test_get_type_name, METH_NOARGS},
59536108
{"test_get_type_qualname", test_get_type_qualname, METH_NOARGS},
59546109
{"test_type_from_ephemeral_spec", test_type_from_ephemeral_spec, METH_NOARGS},
6110+
{"test_from_spec_metatype_inheritance", test_from_spec_metatype_inheritance,
6111+
METH_NOARGS},
6112+
{"test_from_spec_invalid_metatype_inheritance",
6113+
test_from_spec_invalid_metatype_inheritance,
6114+
METH_NOARGS},
59556115
{"get_kwargs", _PyCFunction_CAST(get_kwargs),
59566116
METH_VARARGS|METH_KEYWORDS},
59576117
{"getargs_tuple", getargs_tuple, METH_VARARGS},

0 commit comments

Comments
 (0)