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>
This commit is contained in:
Sebastian Berg 2022-06-09 08:11:08 -07:00 committed by GitHub
parent a5ba0f4ebc
commit 7fef847662
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 325 additions and 99 deletions

View file

@ -193,11 +193,12 @@ The following functions and structs are used to create
.. c:function:: PyObject* PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases)
Create and return a :ref:`heap type <heap-types>` from the *spec*
(:const:`Py_TPFLAGS_HEAPTYPE`).
(see :const:`Py_TPFLAGS_HEAPTYPE`).
The metaclass *metaclass* is used to construct the resulting type object.
When *metaclass* is ``NULL``, the default :c:type:`PyType_Type` is used
instead. Note that metaclasses that override
When *metaclass* is ``NULL``, the metaclass is derived from *bases*
(or *Py_tp_base[s]* slots if *bases* is ``NULL``, see below).
Note that metaclasses that override
:c:member:`~PyTypeObject.tp_new` are not supported.
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
This function calls :c:func:`PyType_Ready` on the new type.
Note that this function does *not* fully match the behavior of
calling :py:class:`type() <type>` or using the :keyword:`class` statement.
With user-provided base types or metaclasses, prefer
:ref:`calling <capi-call>` :py:class:`type` (or the metaclass)
over ``PyType_From*`` functions.
Specifically:
* :py:meth:`~object.__new__` is not called on the new class
(and it must be set to ``type.__new__``).
* :py:meth:`~object.__init__` is not called on the new class.
* :py:meth:`~object.__init_subclass__` is not called on any bases.
* :py:meth:`~object.__set_name__` is not called on new descriptors.
.. versionadded:: 3.12
.. c:function:: PyObject* PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
@ -228,6 +242,11 @@ The following functions and structs are used to create
The function now accepts a single class as the *bases* argument and
``NULL`` as the ``tp_doc`` slot.
.. versionchanged:: 3.12
The function now finds and uses a metaclass corresponding to the provided
base classes. Previously, only :class:`type` instances were returned.
.. c:function:: PyObject* PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)
@ -235,10 +254,21 @@ The following functions and structs are used to create
.. versionadded:: 3.3
.. versionchanged:: 3.12
The function now finds and uses a metaclass corresponding to the provided
base classes. Previously, only :class:`type` instances were returned.
.. c:function:: PyObject* PyType_FromSpec(PyType_Spec *spec)
Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, NULL)``.
.. versionchanged:: 3.12
The function now finds and uses a metaclass corresponding to the
base classes provided in *Py_tp_base[s]* slots.
Previously, only :class:`type` instances were returned.
.. c:type:: PyType_Spec
Structure defining a type's behavior.

View file

@ -0,0 +1,3 @@
The :c:func:`PyType_FromSpec` API will now find and use a metaclass
based on the provided bases.
An error will be raised if there is a metaclass conflict.

View file

@ -1208,6 +1208,161 @@ test_get_type_name(PyObject *self, PyObject *Py_UNUSED(ignored))
}
static PyType_Slot empty_type_slots[] = {
{0, 0},
};
static PyType_Spec MinimalMetaclass_spec = {
.name = "_testcapi.MinimalMetaclass",
.basicsize = sizeof(PyHeapTypeObject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.slots = empty_type_slots,
};
static PyType_Spec MinimalType_spec = {
.name = "_testcapi.MinimalSpecType",
.basicsize = sizeof(PyObject),
.flags = Py_TPFLAGS_DEFAULT,
.slots = empty_type_slots,
};
static PyObject *
test_from_spec_metatype_inheritance(PyObject *self, PyObject *Py_UNUSED(ignored))
{
PyObject *metaclass = NULL;
PyObject *class = NULL;
PyObject *new = NULL;
PyObject *subclasses = NULL;
PyObject *result = NULL;
int r;
metaclass = PyType_FromSpecWithBases(&MinimalMetaclass_spec, (PyObject*)&PyType_Type);
if (metaclass == NULL) {
goto finally;
}
class = PyObject_CallFunction(metaclass, "s(){}", "TestClass");
if (class == NULL) {
goto finally;
}
new = PyType_FromSpecWithBases(&MinimalType_spec, class);
if (new == NULL) {
goto finally;
}
if (Py_TYPE(new) != (PyTypeObject*)metaclass) {
PyErr_SetString(PyExc_AssertionError,
"Metaclass not set properly!");
goto finally;
}
/* Assert that __subclasses__ is updated */
subclasses = PyObject_CallMethod(class, "__subclasses__", "");
if (!subclasses) {
goto finally;
}
r = PySequence_Contains(subclasses, new);
if (r < 0) {
goto finally;
}
if (r == 0) {
PyErr_SetString(PyExc_AssertionError,
"subclasses not set properly!");
goto finally;
}
result = Py_NewRef(Py_None);
finally:
Py_XDECREF(metaclass);
Py_XDECREF(class);
Py_XDECREF(new);
Py_XDECREF(subclasses);
return result;
}
static PyObject *
test_from_spec_invalid_metatype_inheritance(PyObject *self, PyObject *Py_UNUSED(ignored))
{
PyObject *metaclass_a = NULL;
PyObject *metaclass_b = NULL;
PyObject *class_a = NULL;
PyObject *class_b = NULL;
PyObject *bases = NULL;
PyObject *new = NULL;
PyObject *meta_error_string = NULL;
PyObject *exc_type = NULL;
PyObject *exc_value = NULL;
PyObject *exc_traceback = NULL;
PyObject *result = NULL;
metaclass_a = PyType_FromSpecWithBases(&MinimalMetaclass_spec, (PyObject*)&PyType_Type);
if (metaclass_a == NULL) {
goto finally;
}
metaclass_b = PyType_FromSpecWithBases(&MinimalMetaclass_spec, (PyObject*)&PyType_Type);
if (metaclass_b == NULL) {
goto finally;
}
class_a = PyObject_CallFunction(metaclass_a, "s(){}", "TestClassA");
if (class_a == NULL) {
goto finally;
}
class_b = PyObject_CallFunction(metaclass_b, "s(){}", "TestClassB");
if (class_b == NULL) {
goto finally;
}
bases = PyTuple_Pack(2, class_a, class_b);
if (bases == NULL) {
goto finally;
}
/*
* The following should raise a TypeError due to a MetaClass conflict.
*/
new = PyType_FromSpecWithBases(&MinimalType_spec, bases);
if (new != NULL) {
PyErr_SetString(PyExc_AssertionError,
"MetaType conflict not recognized by PyType_FromSpecWithBases");
goto finally;
}
// Assert that the correct exception was raised
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
PyErr_Fetch(&exc_type, &exc_value, &exc_traceback);
meta_error_string = PyUnicode_FromString("metaclass conflict:");
if (meta_error_string == NULL) {
goto finally;
}
int res = PyUnicode_Contains(exc_value, meta_error_string);
if (res < 0) {
goto finally;
}
if (res == 0) {
PyErr_SetString(PyExc_AssertionError,
"TypeError did not inlclude expected message.");
goto finally;
}
result = Py_NewRef(Py_None);
}
finally:
Py_XDECREF(metaclass_a);
Py_XDECREF(metaclass_b);
Py_XDECREF(bases);
Py_XDECREF(new);
Py_XDECREF(meta_error_string);
Py_XDECREF(exc_type);
Py_XDECREF(exc_value);
Py_XDECREF(exc_traceback);
Py_XDECREF(class_a);
Py_XDECREF(class_b);
return result;
}
static PyObject *
simple_str(PyObject *self) {
return PyUnicode_FromString("<test>");
@ -5952,6 +6107,11 @@ static PyMethodDef TestMethods[] = {
{"test_get_type_name", test_get_type_name, METH_NOARGS},
{"test_get_type_qualname", test_get_type_qualname, METH_NOARGS},
{"test_type_from_ephemeral_spec", test_type_from_ephemeral_spec, METH_NOARGS},
{"test_from_spec_metatype_inheritance", test_from_spec_metatype_inheritance,
METH_NOARGS},
{"test_from_spec_invalid_metatype_inheritance",
test_from_spec_invalid_metatype_inheritance,
METH_NOARGS},
{"get_kwargs", _PyCFunction_CAST(get_kwargs),
METH_VARARGS|METH_KEYWORDS},
{"getargs_tuple", getargs_tuple, METH_VARARGS},

View file

@ -3361,13 +3361,51 @@ static const PySlot_Offset pyslot_offsets[] = {
#include "typeslots.inc"
};
/* Given a PyType_FromMetaclass `bases` argument (NULL, type, or tuple of
* types), return a tuple of types.
*/
inline static PyObject *
get_bases_tuple(PyObject *bases_in, PyType_Spec *spec)
{
if (!bases_in) {
/* Default: look in the spec, fall back to (type,). */
PyTypeObject *base = &PyBaseObject_Type; // borrowed ref
PyObject *bases = NULL; // borrowed ref
const PyType_Slot *slot;
for (slot = spec->slots; slot->slot; slot++) {
switch (slot->slot) {
case Py_tp_base:
base = slot->pfunc;
break;
case Py_tp_bases:
bases = slot->pfunc;
break;
}
}
if (!bases) {
return PyTuple_Pack(1, base);
}
if (PyTuple_Check(bases)) {
return Py_NewRef(bases);
}
PyErr_SetString(PyExc_SystemError, "Py_tp_bases is not a tuple");
return NULL;
}
if (PyTuple_Check(bases_in)) {
return Py_NewRef(bases_in);
}
// Not a tuple, should be a single type
return PyTuple_Pack(1, bases_in);
}
PyObject *
PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
PyType_Spec *spec, PyObject *bases)
PyType_Spec *spec, PyObject *bases_in)
{
PyHeapTypeObject *res;
PyObject *modname;
PyTypeObject *type, *base;
PyHeapTypeObject *res = NULL;
PyObject *modname = NULL;
PyTypeObject *type;
PyObject *bases = NULL;
int r;
const PyType_Slot *slot;
@ -3375,16 +3413,6 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
char *res_start;
short slot_offset, subslot_offset;
if (!metaclass) {
metaclass = &PyType_Type;
}
if (metaclass->tp_new != PyType_Type.tp_new) {
PyErr_SetString(PyExc_TypeError,
"Metaclasses with custom tp_new are not supported.");
return NULL;
}
nmembers = weaklistoffset = dictoffset = vectorcalloffset = 0;
for (slot = spec->slots; slot->slot; slot++) {
if (slot->slot == Py_tp_members) {
@ -3413,17 +3441,58 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
}
}
res = (PyHeapTypeObject*)metaclass->tp_alloc(metaclass, nmembers);
if (res == NULL)
return NULL;
res_start = (char*)res;
if (spec->name == NULL) {
PyErr_SetString(PyExc_SystemError,
"Type spec does not define the name field.");
goto fail;
goto finally;
}
/* Get a tuple of bases.
* bases is a strong reference (unlike bases_in).
*/
bases = get_bases_tuple(bases_in, spec);
if (!bases) {
goto finally;
}
/* Calculate the metaclass */
if (!metaclass) {
metaclass = &PyType_Type;
}
metaclass = _PyType_CalculateMetaclass(metaclass, bases);
if (metaclass == NULL) {
goto finally;
}
if (!PyType_Check(metaclass)) {
PyErr_Format(PyExc_TypeError,
"Metaclass '%R' is not a subclass of 'type'.",
metaclass);
goto finally;
}
if (metaclass->tp_new != PyType_Type.tp_new) {
PyErr_SetString(PyExc_TypeError,
"Metaclasses with custom tp_new are not supported.");
goto finally;
}
/* Calculate best base, and check that all bases are type objects */
PyTypeObject *base = best_base(bases); // borrowed ref
if (base == NULL) {
goto finally;
}
// best_base should check Py_TPFLAGS_BASETYPE & raise a proper exception,
// here we just check its work
assert(_PyType_HasFeature(base, Py_TPFLAGS_BASETYPE));
/* Allocate the new type */
res = (PyHeapTypeObject*)metaclass->tp_alloc(metaclass, nmembers);
if (res == NULL) {
goto finally;
}
res_start = (char*)res;
type = &res->ht_type;
/* The flags must be initialized early, before the GC traverses us */
type->tp_flags = spec->flags | Py_TPFLAGS_HEAPTYPE;
@ -3439,7 +3508,7 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
res->ht_name = PyUnicode_FromString(s);
if (!res->ht_name) {
goto fail;
goto finally;
}
res->ht_qualname = Py_NewRef(res->ht_name);
@ -3455,70 +3524,25 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
Py_ssize_t name_buf_len = strlen(spec->name) + 1;
res->_ht_tpname = PyMem_Malloc(name_buf_len);
if (res->_ht_tpname == NULL) {
goto fail;
goto finally;
}
type->tp_name = memcpy(res->_ht_tpname, spec->name, name_buf_len);
res->ht_module = Py_XNewRef(module);
/* Adjust for empty tuple bases */
if (!bases) {
base = &PyBaseObject_Type;
/* See whether Py_tp_base(s) was specified */
for (slot = spec->slots; slot->slot; slot++) {
if (slot->slot == Py_tp_base)
base = slot->pfunc;
else if (slot->slot == Py_tp_bases) {
bases = slot->pfunc;
}
}
if (!bases) {
bases = PyTuple_Pack(1, base);
if (!bases)
goto fail;
}
else if (!PyTuple_Check(bases)) {
PyErr_SetString(PyExc_SystemError, "Py_tp_bases is not a tuple");
goto fail;
}
else {
Py_INCREF(bases);
}
}
else if (!PyTuple_Check(bases)) {
bases = PyTuple_Pack(1, bases);
if (!bases)
goto fail;
}
else {
Py_INCREF(bases);
}
/* Calculate best base, and check that all bases are type objects */
base = best_base(bases);
if (base == NULL) {
Py_DECREF(bases);
goto fail;
}
if (!_PyType_HasFeature(base, Py_TPFLAGS_BASETYPE)) {
PyErr_Format(PyExc_TypeError,
"type '%.100s' is not an acceptable base type",
base->tp_name);
Py_DECREF(bases);
goto fail;
}
/* Initialize essential fields */
type->tp_as_async = &res->as_async;
type->tp_as_number = &res->as_number;
type->tp_as_sequence = &res->as_sequence;
type->tp_as_mapping = &res->as_mapping;
type->tp_as_buffer = &res->as_buffer;
/* Set tp_base and tp_bases */
type->tp_bases = bases;
Py_INCREF(base);
type->tp_base = base;
/* Set tp_base and tp_bases */
type->tp_base = (PyTypeObject *)Py_NewRef(base);
type->tp_bases = bases;
bases = NULL; // We give our reference to bases to the type
/* Copy the sizes */
type->tp_basicsize = spec->basicsize;
type->tp_itemsize = spec->itemsize;
@ -3526,7 +3550,7 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
if (slot->slot < 0
|| (size_t)slot->slot >= Py_ARRAY_LENGTH(pyslot_offsets)) {
PyErr_SetString(PyExc_RuntimeError, "invalid slot offset");
goto fail;
goto finally;
}
else if (slot->slot == Py_tp_base || slot->slot == Py_tp_bases) {
/* Processed above */
@ -3544,7 +3568,7 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
if (tp_doc == NULL) {
type->tp_doc = NULL;
PyErr_NoMemory();
goto fail;
goto finally;
}
memcpy(tp_doc, slot->pfunc, len);
type->tp_doc = tp_doc;
@ -3579,8 +3603,9 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
type->tp_vectorcall_offset = vectorcalloffset;
}
if (PyType_Ready(type) < 0)
goto fail;
if (PyType_Ready(type) < 0) {
goto finally;
}
if (type->tp_flags & Py_TPFLAGS_MANAGED_DICT) {
res->ht_cached_keys = _PyDict_NewKeysForClass();
@ -3588,29 +3613,33 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
if (type->tp_doc) {
PyObject *__doc__ = PyUnicode_FromString(_PyType_DocWithoutSignature(type->tp_name, type->tp_doc));
if (!__doc__)
goto fail;
if (!__doc__) {
goto finally;
}
r = PyDict_SetItem(type->tp_dict, &_Py_ID(__doc__), __doc__);
Py_DECREF(__doc__);
if (r < 0)
goto fail;
if (r < 0) {
goto finally;
}
}
if (weaklistoffset) {
type->tp_weaklistoffset = weaklistoffset;
if (PyDict_DelItemString((PyObject *)type->tp_dict, "__weaklistoffset__") < 0)
goto fail;
if (PyDict_DelItemString((PyObject *)type->tp_dict, "__weaklistoffset__") < 0) {
goto finally;
}
}
if (dictoffset) {
type->tp_dictoffset = dictoffset;
if (PyDict_DelItemString((PyObject *)type->tp_dict, "__dictoffset__") < 0)
goto fail;
if (PyDict_DelItemString((PyObject *)type->tp_dict, "__dictoffset__") < 0) {
goto finally;
}
}
/* Set type.__module__ */
r = PyDict_Contains(type->tp_dict, &_Py_ID(__module__));
if (r < 0) {
goto fail;
goto finally;
}
if (r == 0) {
s = strrchr(spec->name, '.');
@ -3618,26 +3647,30 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
modname = PyUnicode_FromStringAndSize(
spec->name, (Py_ssize_t)(s - spec->name));
if (modname == NULL) {
goto fail;
goto finally;
}
r = PyDict_SetItem(type->tp_dict, &_Py_ID(__module__), modname);
Py_DECREF(modname);
if (r != 0)
goto fail;
} else {
if (r != 0) {
goto finally;
}
}
else {
if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1,
"builtin type %.200s has no __module__ attribute",
spec->name))
goto fail;
goto finally;
}
}
assert(_PyType_CheckConsistency(type));
return (PyObject*)res;
fail:
Py_DECREF(res);
return NULL;
finally:
if (PyErr_Occurred()) {
Py_CLEAR(res);
}
Py_XDECREF(bases);
Py_XDECREF(modname);
return (PyObject*)res;
}
PyObject *