Using ABC, PolymorphicModel, django-models gives metaclass conflict
Asked Answered
G

2

11

So far every other answer on SO answers in the exact same way: construct your metaclasses and then inherit the 'joined' version of those metaclasses, i.e.

class M_A(type): pass
class M_B(type): pass
class A(metaclass=M_A): pass
class B(metaclass=M_B): pass

class M_C(M_A, M_B): pass
class C:(A, B, metaclass=M_C): pass

But I don't know what world these people are living in, where they're constructing your own metaclasses! Obviously, one would be using classes from other libraries and unless you have a perfect handle on meta programming, how are you supposed to know whether you can just override a class's metaclass? (Clearly I do not have a handle on them yet).

My problem is:

class InterfaceToTransactions(ABC):
    def account(self):
        return None
    ...

class Category(PolymorphicModel, InterfaceToTransactions):
    def account(self):
        return self.source_account
    ...

class Income(TimeStampedModel, InterfaceToTransactions):
    def account(self):
        return self.destination_account
    ...

Which of course gives me the error: "metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases" I've tried many variations of the solution given above, the following does not work, gives the same error.

class InterfaceToTransactionsIntermediaryMeta(type(PolymorphicModel), type(InterfaceToTransactions)):
pass

class Category(PolymorphicModel, InterfaceToTransactions):
    __metaclass__ = InterfaceToTransactionsIntermediaryMeta
    ...

Nor does putting anything inside the class Meta function. I've read every single other SO question on this topic, please don't simply mark it as duplicate.

-------------------Edited 1/8/18 after accepting the solution-------

Oddly enough, if I try to makemigrations with this new configuration (the one I accepted), it starts giving the metaclass error again, but it still works during runtime. If I comment out the metaclass parts then makemigrations and migrate, it will do it successfully, but then I have to put it back in there after migrating every time.

Galloping answered 16/11, 2017 at 0:54 Comment(5)
Just for clarification, your needs it to make both of your Category and Income inherits from InterfaceToTransactions where an abstract method account is ? If this is only that, using Metaclass is a little overkill...Anfractuosity
I had shortened the definition here but there are quite a few lengthy methods common for each of the classes utilizing the interface.Galloping
@Galloping have you found a way around the migrate/makemigrations issue?Cyler
@Galloping I have noticed there is no issue if you are running python manage.py migrate on a fresh database, but subsequent runs break.Cyler
It appears that this is a known bug in Django but they have yet to agree on a good solution to it: code.djangoproject.com/ticket/25068Cyler
M
2

If you are using Python 3, you are trying to use your derived metaclass incorrectly.

And since you get "the same error", and not other possible, more subtle, error, I'd say this is what is happening.

Try just changing to:

class IntermediaryMeta(type(InterfaceToTransactions), type(PolymorphicModel)):
    pass

class Category(PolymorphicModel, InterfaceToTransactions, metaclass=IntermediaryMeta):
    ...

(At least the ABCMeta class is guaranteed to work collaboratively using super, that is enough motive to place the classe it first on the bases ) tuple)

If that yields you new and improved errors, this means that one or both of those classes can't really collaborate properly due to one of several motives. Then, the way to go is to force your inheritance tree that depends on ABCMeta not to do so, since its role is almost aesthetical in a language where everything else is for "consenting adults" like Python.

Unfortunatelly, the way to that is to use varying methods of brute-force, from safe "rewritting everything" to monkey patching ABCMeta and abstractmethod on the place were "InterfaceToTransactions" is defined to simply do nothing.

If you need to get there, and need some help, please post another question.

Sorry - this is actually the major drawbacks of using metaclasses.

Mecke answered 16/11, 2017 at 13:11 Comment(1)
That worked, including with the TimeStampedModel, although of course I had to make a separate but identically structured metaclass using TSM instead of the Polymorphic. Interestingly, the only thing that mattered was making it py3 compatible, ie the "metaclass=IntermediaryMeta" instead of the py2 style.Galloping
A
1

Unless django-polymorphic decides to inherit from abc.ABC this is going to be very difficult to achieve. A good solution would be to "manually" create your interface. For instance:

class InterfaceToTransactions:
    def account(self):
        raise NotImplementedError("Account method must be implemented.")
    ...

class Category(PolymorphicModel, InterfaceToTransactions):
    def account(self):
        return self.source_account
    ...

class Income(TimeStampedModel, InterfaceToTransactions):
    def account(self):
        return self.destination_account
    ...
Almetaalmighty answered 2/10, 2020 at 14:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.