Search code examples
pythonmetaclass

In a library that uses metaclasses a lot, how can I avoid annoying the user with metaclass conflicts?


The library I'm writing makes heavy use of metaclasses. As an example, here is a basic singleton implementation:

class SingletonMeta(type):
    _instance = None

    def __call__(self, *args, **kwargs):
        if self._instance is None:
            self._instance = super().__call__(*args, **kwargs)

        return self._instance

class ExampleSingleton(metaclass=SingletonMeta):
    pass

This works perfectly fine, but problems arise when multiple inheritance is used and the other class also has a metaclass. Metaclasses are fairly common in the standard library; the most notable is abc.ABCMeta. A naive attempt to make an abstract singleton fails:

class AbstractSingleton(ExampleSingleton, abc.ABC):
    pass

Traceback (most recent call last):
  File "untitled.py", line 25, in <module>
    class AbstractSingleton(ExampleSingleton, abc.ABC):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

The workaround is easy enough - create a new metaclass that inherits from SingletonMeta and ABCMeta - but it's really annoying for anyone who wants to use my library.

class AbstractSingletonMeta(SingletonMeta, abc.ABCMeta):
    pass

class AbstractSingleton(metaclass=AbstractSingletonMeta):
    pass

# no metaclass conflict

What's the best way to deal with this problem?

Some of my ideas:

  1. Since abstract classes are fairly common, I could make SingletonMeta a subclass of ABCMeta.
  2. I could implement AbstractSingletonMeta in my library for the user's convenience.
  3. Since any callable can be used as a metaclass, I could implement a function that automatically merges the metaclasses of all parent classes. (The usage would be like class AbstractSingleton(ExampleSingleton, abc.ABC, metaclass=auto_merge_metaclasses):)
  4. In the spirit of "explicit is better than implicit", I could do nothing and let the user sort out the metaclass conflicts.

Solution

  • Since all the information about the needed metaclasses and what needs to be combined is already present in the base classes, and those are passed to the metaclass call, it is possible to have a callable that will inspect all the bases and their used metaclasses, and dynamically create a combining metaclass.

    The types module have some callables that otherwise make it easy to pick the correct metaclass for a set of base classes. So, the function bellow should suffice to your needs, if all your used metaclasses are combinable in an arbitrary order:

    from types import prepare_class
    
    def combine_meta(name, bases, namespace, **kwargs):
        metaclasses = {prepare_class(name, (base,))[0] for base in bases}
        metaclasses.discard(type)
    
        if len(metaclasses) > 1:
            meta_name = '_'.join(mcs.__name__ for mcs in metaclasses)
            metaclass = combine_meta(meta_name, tuple(metaclasses), {})
        elif len(metaclasses) == 1:
            metaclass = metaclasses.pop()
        else:
            metaclass = type
        return metaclass(name, bases, namespace, **kwargs)
    

    I've tested this with this sequence in the interactive interpreter, and it worked that far:

    class M1(type): pass
    class M2(type): pass
    class A(metaclass=M1): pass
    class B(metaclass=M2): pass
    class C(A, B): pass  # This raises a metaclassconflict
    class C(A, B, metaclass=combine_meta): pass