Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow __slots__ on classes with Py_TPFLAGS_ITEMS_AT_END #103740

Open
jbradaric opened this issue Apr 24, 2023 · 2 comments
Open

Allow __slots__ on classes with Py_TPFLAGS_ITEMS_AT_END #103740

jbradaric opened this issue Apr 24, 2023 · 2 comments
Assignees
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error

Comments

@jbradaric
Copy link
Contributor

Bug report

If a base class is a PyVarObject, classes inheriting from that base cannot add __dict__ or __weakref__ through __slots__ declaration on the class. If __slots__ is not empty, the following exception is raised:
TypeError: nonempty __slots__ not supported for subtype of '...'

Assume that we have a PyVarObject foo.FooBase class (implementation below). Trying to inherit from the class and requesting weakref support doesn't work.

import foo
class WithWeakrefAndDict(foo.FooBase):
    __slots__ = ('__weakref__', '__dict__')
`foo` module implementation
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static const Py_ssize_t N_EXTRA = 4;

typedef struct {
    PyObject_VAR_HEAD
} FooBase;

static PyObject **
FooBase_get_storage(PyObject *self)
{
    char *addr = (char *)self;
    return (PyObject **)(addr + Py_TYPE(self)->tp_basicsize);
}

static int
FooBase_traverse(PyObject *self, visitproc visit, void *arg)
{
    PyObject **storage = FooBase_get_storage(self);
    for (int i = 0; i < N_EXTRA; i++) {
        Py_VISIT(storage[i]);
    }
    Py_VISIT(Py_TYPE(self));
    return 0;
}

static int
FooBase_clear(PyObject *self)
{
    PyObject **storage = FooBase_get_storage(self);
    for (int i = 0; i < N_EXTRA; i++) {
        Py_CLEAR(storage[i]);
    }
    return 0;
}

static void
FooBase_dealloc(PyObject *self)
{
    PyTypeObject *tp = Py_TYPE(self);
    PyObject_GC_UnTrack(self);
    FooBase_clear(self);
    PyObject_GC_Del(self);
    if (tp->tp_flags & Py_TPFLAGS_HEAPTYPE)
        Py_DECREF(tp);
}

static PyObject *
FooBase_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
    PyVarObject *obj = PyObject_GC_NewVar(PyVarObject, type, N_EXTRA);
    if (obj == NULL)
        return NULL;
    PyObject **storage = FooBase_get_storage((PyObject *)obj);
    for (int i = 0; i < Py_SIZE(obj); i++)
        storage[i] = NULL;

    PyObject_GC_Track(obj);
    return (PyObject *)obj;
}

static PyObject *
FooBase_get_extra(PyObject *self, PyObject *args)
{
    Py_ssize_t idx;
    if (!PyArg_ParseTuple(args, "n", &idx))
        return NULL;
    if (idx < 0 || idx >= N_EXTRA) {
        PyErr_Format(PyExc_ValueError, "idx must be >= 0 and < %zd", N_EXTRA);
        return NULL;
    }

    PyObject **storage = FooBase_get_storage(self);
    PyObject *value = storage[idx];
    if (!value) {
        Py_RETURN_NONE;
    } else {
        Py_INCREF(value);
        return value;
    }
}

static PyObject *
FooBase_set_extra(PyObject *self, PyObject *args)
{
    Py_ssize_t idx;
    PyObject *value;
    if (!PyArg_ParseTuple(args, "nO", &idx, &value))
        return NULL;
    if (idx < 0 || idx >= N_EXTRA) {
        PyErr_Format(PyExc_ValueError, "idx must be >= 0 and < %zd", N_EXTRA);
        return NULL;
    }

    PyObject **storage = FooBase_get_storage(self);
    Py_CLEAR(storage[idx]);
    Py_INCREF(value);
    storage[idx] = value;

    Py_RETURN_NONE;
}

static PyMethodDef FooBase_methods[] = {
    {"get_extra", FooBase_get_extra, METH_VARARGS, NULL},
    {"set_extra", FooBase_set_extra, METH_VARARGS, NULL},
    {NULL}
};

static PyTypeObject FooBase_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "foo.FooBase",
    sizeof(FooBase),
    .tp_dealloc = (destructor)FooBase_dealloc,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
    .tp_traverse = FooBase_traverse,
    .tp_clear = FooBase_clear,
    .tp_methods = FooBase_methods,
    .tp_new = FooBase_new,
    .tp_itemsize = sizeof(PyObject *)
};

static struct PyModuleDef foo_module = {
    PyModuleDef_HEAD_INIT,
    "foo",
    NULL,
    -1,
};

PyMODINIT_FUNC
PyInit_foo(void)
{
    PyObject *m = PyModule_Create(&foo_module);
    if (!m)
        return NULL;
    if (PyType_Ready(&FooBase_Type) < 0)
        return NULL;
    Py_INCREF(&FooBase_Type);
    PyModule_AddObject(m, "FooBase", (PyObject *)&FooBase_Type);
    return m;
}

Your environment

  • CPython versions tested on: 3.11.3
  • Operating system and architecture: Linux, x86_64

CC: @encukou

@jbradaric jbradaric added the type-bug An unexpected behavior, bug, or error label Apr 24, 2023
@ronaldoussoren
Copy link
Contributor

This is a duplicate of #61497. The isn't isn't so much that these particular slots aren't supported, but that no slots are supported at all var these objects.

@encukou
Copy link
Member

encukou commented Apr 24, 2023

Let's use this issue for enabling this with Py_TPFLAGS_ITEMS_AT_END specifically. I plan to look into it for 3.13.

@encukou encukou changed the title Cannot use __slots__ = ('__weakref__', '__dict__') with PyVarObject Allow __slots__ on classes with Py_TPFLAGS_ITEMS_AT_END Apr 24, 2023
@encukou encukou self-assigned this Apr 24, 2023
@iritkatriel iritkatriel added the interpreter-core (Objects, Python, Grammar, and Parser dirs) label Nov 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants