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

Extend PyType to create metaclasses #4621

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

mbway
Copy link

@mbway mbway commented Oct 14, 2024

I am investigating adding support for metaclasses in PyO3 (#906), mainly as a way to implement proper enums or a close equivalent (#2887).

As a first step I created a metaclass in c. That example can be found in this comment. I then tried to recreate the example in PyO3. I found that the first issue is the missing ability to inherit from type.

This PR allows metaclasses (classes that extend type) to be created from Pyo3 using #[pyclass(extends=PyType)] and used in python (but not used with other Pyo3 classes yet).

I'm not sure if PyType::new_type is required or the correct way to go about calling type(name, bases, namespace) from the PyO3 metaclass. There are probably also lifetime issues with the current implementation. guidance here would be appreciated.

Background: Creating a metaclass in c

Initially I wasn't sure how to correctly inherit from type using the c api since there are no examples I could find online. By pattern matching with similar examples I ended up with:

typedef struct { PyTypeObject base_type; } MyMetaclass;

static PyTypeObject MyMetaclassType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "metaclass_test.MyMetaclass",
    .tp_basicsize = sizeof(MyMetaclass),
    .tp_base = &PyType_Type,
    // etc
};

But this has a problem. PyTypeObject is a PyVarObject and so cannot implement the PySizedLayout trait necessary for using it as a base in Pyo3. After some searching I found PEP-0697 which was very useful because it explains that the actual base of PyType_Type is PyHeapTypeObject meaning that the example should have been:

typedef struct { PyHeapTypeObject base_type; } MyMetaclass;

The PEP also explains a mechanism introduced in python 3.12 for supporting extending opaque or variable sized base classes in extension modules. The one example of an extension module metaclass that I could find (the ctypes metaclass _ctypes.CType_Type defined in _ctypes.c in the cpython source code) uses this mechanism

PyType_Spec pyctype_type_spec = {
    .name = "_ctypes.CType_Type",
    .basicsize = -(Py_ssize_t)sizeof(StgInfo),
    // etc
};

For this approach basicsize is set to a negative number equal to the size of the storage space required just for the
derived class (StfInfo in the example above). A pointer to that memory is accessed using PyObject_GetTypeData().

If Pyo3 supported this then it would be possible to extend builtins (including type) using the limited API (for python >=3.12).

Background: Why is inheriting type required

In python the following does not crash, but also does not work correctly as a metaclass:

import pytest

class MyMetaclass:
    def __new__(cls, name, bases, namespace):
        # adding cls to bases doesn't change much other than MyClass.__bases__
        return type(name, bases, namespace)

    def __getitem__(self, item):
        return item

class MyClass(metaclass=MyMetaclass):
    pass

assert MyMetaclass.__bases__ == (object,)  # not a metaclass
assert type(MyClass) is type  # want this to be `MyMetaclass`
with pytest.raises(TypeError):
    MyClass[123]  # want this to call MyMetaclass.__getitem__

And in the c example removing the .tp_base = &PyType_Type, line from the definition of MyMetaclassType results in

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

because without that line the base becomes object.

@mbway mbway marked this pull request as draft October 14, 2024 21:54
@mbway mbway force-pushed the extend_builtin_type branch 4 times, most recently from b1d1b0a to ad78e08 Compare October 14, 2024 23:06
@mbway mbway force-pushed the extend_builtin_type branch from ad78e08 to bdbcd6a Compare October 14, 2024 23:16
@mbway mbway marked this pull request as ready for review October 14, 2024 23:34
@mejrs
Copy link
Member

mejrs commented Oct 15, 2024

PyHeapTypeObject does not appear to actually be public api (it's not present in the C api documentation and it has fields that are clearly not public api). A better way to achieve this is to implement #1344

@mbway
Copy link
Author

mbway commented Oct 15, 2024

Ok, I was looking to do that next. I wasn't aware that things in the ffi module might not be public

@mejrs
Copy link
Member

mejrs commented Oct 15, 2024

I wasn't aware that things in the ffi module might not be public

There are a bunch of thing in it that aren't supposed to be public. We already removed quite a few underscore apis a while back.

@mbway mbway mentioned this pull request Nov 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants