ssanderson / python-interface

Minimal Pythonic Interface Definitions
https://interface.readthedocs.io/en/latest/
Apache License 2.0
111 stars 16 forks source link

TypeError: metaclass conflict with Django #12

Closed bjorntheart closed 6 days ago

bjorntheart commented 6 years ago

I get the following error when using implements() with a Django model.

class InstagramContentSource(ContentSource, implements(IContentSource)):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

Interface

class IContentSource(Interface):
    def get_id(self):
        pass

Django Model

class ContentSource(models.Model, Logger):
...

Interface Implementation

class InstagramContentSource(ContentSource, implements(IContentSource)):
    class Meta:
        proxy = True
ssanderson commented 6 years ago

What implements(IFace) actually does under the hood is construct a new instance of an ImplementsMeta metaclass, which verifies the the implementation of your interface at type construction time.

One of the downsides of using a metaclass for this is that a given class can only have one metaclass. The error you're seeing there is telling you that both Django and Interface are trying to define a metaclass for your type, and python can't use both of them.

I can think of two ways that interface could potentially try to solve this problem.

  1. The simplest thing we could do would be to add a check_implements function that could be applied as a decorator to classes that can't use implements because they need another metaclass. The API for this would look something like:
    @check_implements(IContentSource)
    class InstagramContentSource(ContentSource):
    ...

This would work by doing the same checks that implements(IContentSource) does currently. The downside of this API is that we wouldn't automatically apply interface checks to subclasses of InstagramContentSource; you'd have to remember to manually decorate your subclasses.

  1. Add an enhanced version of implements named something like implements_with_metaclass, that takes an existing metaclass (in this case, it would be the metaclass of models.Model), and dynamically generates a new metaclass that multiply-inherits from both ImplementsMeta and the additional metaclass. Internally, this would look more or less like this with_metaclasses function. The API for this would look something like:
meta = type(ContentSource)

class InstagramContentSource(ContentSource,
                             implements_with_meta(meta, IContentSource)):
    class Meta:
        proxy = True

That's a lot more verbose, and the implementation would be more complex, but the upside of this would be that implementation checks would continue to propagate through subclasses.

eeriksp commented 5 years ago

Here is another solution for this issue. Python 3.6 introduced a new method __init_subclass__ (the docs are here).

It is called automatically whenever a subclass is created and it gets in as a parameter the newly created subclass. So it has all the necessary data for performing the checks. Using this method, there is no need for a metaclass any more and thus the issue of metaclass conflicts will not arise.

I am using this approach in a similar project and are very happy with this new Python feature :)