Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use multiple inheritance with a metaclass?

I'm trying to register all the resources that I defined with Flask-RESTFUL using the registry pattern.

from flask_restful import Resource

class ResourceRegistry(type):

    REGISTRY = {}

    def __new__(cls, name, bases, attrs):
        new_cls = type.__new__(cls, name, bases, attrs)
        cls.REGISTRY[new_cls.__name__] = new_cls
        return new_cls

    @classmethod
    def get_registry(cls):
        return dict(cls.REGISTRY)


class BaseRegistered(object):
    __metaclass__ = ResourceRegistry


class DefaultResource(BaseRegistered, Resource):

    @classmethod
    def get_resource_name(cls):
        s = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', cls.__name__)
        return '/' + re.sub('([a-z0-9])([A-Z])', r'\1-\2', s).lower()

When the whole thing is launched I get the following:

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

I've tried with layers of proxy classes but the result is still the same. So is there a way to register my resources using this pattern ?

like image 625
Ketouem Avatar asked Mar 22 '16 10:03

Ketouem


2 Answers

A metaclass is the class used to build a class, which means that the very structure of a class - its internal __dict__, for example, or the automatic delegation mechanism provided by __getattribute__ - are created into the class by the metaclass.

When you inherit from a parent class you are implicitly inheriting its metaclass, because you are basically just extending the structure of the parent class, which is in part provided by its metaclass.

So, when you inherit from a class you have either to keep the metaclass given you by your parent or define a new metaclass which is derived from that of your parent. This is true in the case of multiple inheritance as well.

An example of wrong Python code is

class ParentType(type):
    pass

class Parent(metaclass=ParentType):
    pass

class ChildType(type):
    pass

class Child(Parent, metaclass=ChildType):
    pass

This gives exactly the error you mention in your question. Making ChildType a subclass of ParentType solves the issue

class ParentType(type):
    pass

class Parent(metaclass=ParentType):
    pass

class ChildType(ParentType):
    pass

class Child(Parent, metaclass=ChildType):
    pass

The answer given by Dologan correctly addresses the problem creating a metaclass which inherits from both the metaclasses and then use it straightforwardly.

I would, if possible, avoid such a complex solution, however. Multiple inheritance is a bit difficult to manage - not a bad thing, just complex - and doing it in a metaclass could really result in something difficult to grasp.

It obviously depends on what you are trying to accomplish and how good you document and test the solution. If you want or need to solve it with metaclasses I would suggest to leverage the Abstract Base Classes (which are basically categories). ABCs contain the list of the registered classes in their _abc_registry attribute, but there is no official method to get it, and it is better to avoid directly using it. You may however derive ABCMeta and make it keep a public registry with this code

import abc

class RegistryABCMeta(abc.ABCMeta):
    def __init__(self, name, bases, namespace):
        super().__init__(name, bases, namespace)
        self.registry = []

    def register(self, subclass):
        super().register(subclass)
        self.registry.append(subclass)


class RegistryABC(metaclass=RegistryABCMeta):
    pass

You may now create categories just inheriting from RegistryABC and using the register() method:

class MyCategory(RegistryABC):
    pass

MyCategory.register(tuple)
MyCategory.register(str)

print(MyCategory.registry)

and what you get here is [<class 'tuple'>, <class 'str'>].

like image 184
Leonardo Giordani Avatar answered Oct 26 '22 12:10

Leonardo Giordani


Your DefaultResource class seems to be inheriting from classes with two different metaclasses: BaseRegistered (with metaclass ResourceRegistry) and Resource (with Flask's MethodViewType metaclass).

This answer would suggest doing something like:

from flask.views import MethodViewType    

class CombinedType(ResourceRegistry, MethodViewType):
    pass

class BaseRegistered(object):
    __metaclass__ = Combinedtype

And then proceed as before.

like image 30
Dologan Avatar answered Oct 26 '22 14:10

Dologan