How can I use multiple inheritance with a metaclass?
Asked Answered
D

3

8

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 ?

Duenas answered 22/3, 2016 at 10:35 Comment(0)
C
7

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.

Cochise answered 22/3, 2016 at 11:20 Comment(0)
C
17

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'>].

Cary answered 16/4, 2016 at 23:17 Comment(0)
C
7

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.

Cochise answered 22/3, 2016 at 11:20 Comment(0)
A
0

It doesn't need to be complex, but you will have to decide whether some ResourceMixins should be also registered in REGISTRY (here they will). In python 2 just use __metaclass__ I've spent few hours because i didn't save the code and was wondering what kind of sorcery is this. In my case i wanted to add extra method_decorators, but Resource class have it already declared so it's not possible to add it in attrs, but set on new_cls object. Also don't forget to call MethodViewType.__new__ (super(), not type).

from flask_restful import Resource
from flask.views import MethodViewType

class ResourceRegistry(MethodViewType):

    REGISTRY = {}

    def __new__(cls, name, bases, attrs):
        new_cls = super().__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 DefaultResource(Resource, metaclass=ResourceRegistry):

    @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()
Avera answered 29/7, 2020 at 16:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.