Metaclasses for Cython extension types#

Cython does not support metaclasses, but this module can be used to implement metaclasses for extension types.


This module has many caveats and you can easily get segfaults if you make a mistake. It relies on undocumented Python and Cython behaviour, so things might break in future versions.

How to use#

To enable this metaclass mechanism, you need to put cimport sage.cpython.cython_metaclass in your module (in the .pxd file if you are using one).

In the extension type (a.k.a. cdef class) for which you want to define a metaclass, define a method __getmetaclass__ with a single unused argument, and turn off the Cython directive always_allow_keywords. This method should return a type to be used as metaclass:

cimport cython
cimport sage.cpython.cython_metaclass
cdef class MyCustomType():
    def __getmetaclass__(_):
        from foo import MyMetaclass
        return MyMetaclass


__getmetaclass__ must be defined as an ordinary method taking a single argument, but this argument should not be used in the method (it will be None).

When a type cls is being constructed with metaclass meta, then meta.__init__(cls, None, None, None) is called from Cython. In Python, this would be meta.__init__(cls, name, bases, dict).


The __getmetaclass__ method is called while the type is being created during the import of the module. Therefore, __getmetaclass__ should not refer to any global objects, including the type being created or other types defined or imported in the module (unless you are very careful). Note that non-imported cdef functions are not Python objects, so those are safe to call.

The same warning applies to the __init__ method of the metaclass.


The __new__ method of the metaclass (including the __cinit__ method for Cython extension types) is never called if you’re using this from Cython. In particular, the metaclass cannot have any attributes or virtual methods.


sage: cython(                                                                       # needs sage.misc.cython
....: '''
....: cimport cython
....: cimport sage.cpython.cython_metaclass
....: cdef class MyCustomType():
....:     @cython.always_allow_keywords(False)
....:     def __getmetaclass__(_):
....:         class MyMetaclass(type):
....:             def __init__(*args):
....:                 print("Calling MyMetaclass.__init__{}".format(args))
....:         return MyMetaclass
....: cdef class MyDerivedType(MyCustomType):
....:     pass
....: ''')
Calling MyMetaclass.__init__(<class '...MyCustomType'>, None, None, None)
Calling MyMetaclass.__init__(<class '...MyDerivedType'>, None, None, None)
sage: MyCustomType.__class__                                                        # needs sage.misc.cython
<class '...MyMetaclass'>
sage: class MyPythonType(MyDerivedType):                                            # needs sage.misc.cython
....:     pass
Calling MyMetaclass.__init__(<class '...MyPythonType'>, 'MyPythonType', (<class '...MyDerivedType'>,), {...})


All this is implemented by defining

#define PyTypeReady(t)  Sage_PyType_Ready(t)

and then implementing the function Sage_PyType_Ready(t) which first calls PyType_Ready(t) and then handles the metaclass stuff.