Jim
Jim

Reputation: 769

Creating a metaclass in Cython

I aim to create a metaclass (let's call it SlotsMeta) in Cython that performs the following:

  1. It takes all the class variables defined by the class that uses the SlotsMeta as metaclass and converts them into readonly attributes in Cython.

I am using trying to convert all class variables (as keys in the parameter **kwargs) passed into the __cinit__ method of SlotsMeta to a Tuple[str] and assign it to SlotsMeta.__slots__.

I have looked at the following references:

  1. Python Metaclass defining __slots__ makes __slots__ readonly
  2. https://github.com/sagemath/sagelib/blob/master/sage/misc/classcall_metaclass.pxd
  3. https://github.com/sagemath/sagelib/blob/master/sage/misc/classcall_metaclass.pyx

My implementation is as follows:

#cython: language_level=3
# copied from https://github.com/sagemath/sagelib/blob/master/sage/misc/classcall_metaclass.pyx
from cpython cimport PyObject, Py_XDECREF

cdef extern from "Python.h":
    ctypedef PyObject *(*callfunc)(type, object, object) except NULL
    ctypedef struct PyTypeObject_call "PyTypeObject":
        callfunc tp_call # needed to call type.__call__ at very high speed.
    cdef PyTypeObject_call PyType_Type # Python's type

cdef class SlotsMeta(type):
    cdef readonly tuple __slots__

    def __init__(mcls, str name, tuple bases, dict attrs):
        attrs_keys = tuple(str(k) for k in attrs.keys())
        mcls.__slots__ = attrs_keys

    def __call__(cls, *args, **kwargs):
        ptr = PyType_Type.tp_call(cls, args, kwargs)
        inst = <object>ptr
        inst.__init__(*args, **kwargs)
        Py_XDECREF(ptr) # During the cast to <object> Cython did INCREF(res)
        return inst

The test function that fails is:

class A(metaclass=SlotsMeta):
    B: str = "H"
 
    
def test_slotsmeta():
    a = A() # passes
    assert a.B == "H" # passes
    with pytest.raises(AttributeError): # passes
        a.C # passes
    with pytest.raises(AttributeError):
        a.C = 500 # failed
        
    assert a.__slots__ == ("B", ) # failed

What failed:

  1. a.C = 500 should not be possible but it is.
  2. a.__slots__ should return ("B", ) but it doesn't.
  3. a.__dict__ exists when it shouldn't.

Upvotes: 1

Views: 93

Answers (0)

Related Questions