Skip to content

Static types only inherit sub-slots from the first base #99249

Closed
@encukou

Description

@encukou

If the following is implemented using static types C API, the derived class doesn't inherit __getitem__:

class SimpleMap:
    def __getitem__(self, key): return 'value'

class SimpleObject:
    pass

class DerivedObject(SimpleObject, SimpleMap):
    pass
C implementation
#include <Python.h>

/* SimpleMap: A simple object that allows subscription */

static PyObject *
simplemap_subscript(PyObject *self, PyObject *key)
{
    return PyUnicode_FromString("value");
}

static PyMappingMethods simplemap_as_mapping = {
    .mp_subscript = simplemap_subscript,
};

static PyTypeObject simplemap_type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "SimpleMap",
    .tp_as_mapping = &simplemap_as_mapping,
};

/* SimpleObject: A simple object that does nothing */

static PyTypeObject simpleobject_type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "reproducer.SimpleObject",
};

/* DerivedObject: Derives from SimpleObject and SimpleMap */

static PyTypeObject derivedobject_type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "reproducer.DerivedObject",
    .tp_new = PyType_GenericNew,
};

static struct PyModuleDef moduledef = {
    PyModuleDef_HEAD_INIT,
    .m_name = "reproducer",
};

PyMODINIT_FUNC PyInit_reproducer(void)
{
    PyObject *m;
    m = PyModule_Create(&moduledef);
    if (!m) {
        return NULL;
    }
    if (PyType_Ready(&simplemap_type) < 0) {
        return NULL;
    }
    if (PyModule_AddType(m, &simplemap_type) < 0) {
        return NULL;
    }

    if (PyType_Ready(&simpleobject_type) < 0) {
        return NULL;
    }
    if (PyModule_AddType(m, &simpleobject_type) < 0) {
        return NULL;
    }

    derivedobject_type.tp_bases = Py_BuildValue("(OO)", &simpleobject_type, &simplemap_type);
    if (!derivedobject_type.tp_bases) {
        return NULL;
    }

    if (PyType_Ready(&derivedobject_type) < 0) {
        return NULL;
    }
    if (PyModule_AddType(m, &derivedobject_type) < 0) {
        return NULL;
    }

    return m;
}
>>> import reproducer
>>> instance = reproducer.DerivedObject()
>>> type(instance).mro()
[<class 'reproducer.DerivedObject'>, <class 'reproducer.SimpleObject'>, <class 'SimpleMap'>, <class 'object'>]
>>> instance['key']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'reproducer.DerivedObject' object is not subscriptable

The reason is that type_ready_inherit_as_structs is only called wih one base, not all of them.

I don't think there's a good way to fix it properly. To allow mixing slots from multiple bases (e.g. if the other base had __setitem__), we'd need to allocate new sub-slot structs for the derived type -- as heap types do. Also, it sounds pretty scary to change such long-standing behavior :)
The best we could do that I can think of would be to detect the situation and emit warnings -- possibly DeprecationWarnings, which could be changed to errors in a few releases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions