Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inheriting from both ABC and django.db.models.Model raises metaclass exception

I am trying to implement a Django data model class, which is also an interface class, using Python 3. My reason for doing so is, I'm writing a base class for my colleague, and need him to implement three methods in all of the classes he derives from mine. I am trying to give him a simplified way to use the functionality of a system I've designed. But, he must override a few methods to supply the system with enough information to execute the code in his inherited classes.

I know this is wrong, because it's throwing exceptions, but I'd like to have a class like the following example:

from django.db import models
from abc import ABC, abstractmethod

class AlgorithmTemplate(ABC, models.Model):
    name = models.CharField(max_length=32)

    @abstractmethod
    def data_subscriptions(self):
        """
        This method returns a list of topics this class will subscribe to using websockets

        NOTE: This method MUST be overriden!
        
        :rtype: list
        """

I understand I could avoid inheriting from the ABC class, but I'd like to use it for reasons I won't bore you with here.

The Problem


After including a class, like the one above, into my project and running python manage.py makemigrations I get the error: TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases. I have searched Stack Overflow, but have only find solutions like the following one:

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

I've read the following posts:

Using ABC, PolymorphicModel, django-models gives metaclass conflict

Resolving metaclass conflicts

And I've tried many variations of those solutions, but I still get the dreaded metaclass exception. Help me Obi-Wan Kenobi, you're my only hope. :-)

like image 256
MikeyE Avatar asked Apr 29 '18 10:04

MikeyE


2 Answers

I had the same need and found this. I've altered the code for clarity and completeness. Basically you need an extra class which you can use for all your model interfaces.

import abc

from django.db import models


class AbstractModelMeta(abc.ABCMeta, type(models.Model)):
    pass


class AbstractModel(models.Model, metaclass=AbstractModelMeta):    
    # You may have common fields here.

    class Meta:
        abstract = True

    @abc.abstractmethod
    def must_implement(self):
        pass


class MyModel(AbstractModel):
    code = models.CharField("code", max_length=10, unique=True)

    class Meta:
        app_label = 'my_app'


test = MyModel(code='test')
> TypeError: Can't instantiate abstract class MyModel with abstract methods must_implement

Now you have the best of both worlds.

like image 118
steven2308 Avatar answered Oct 02 '22 11:10

steven2308


I found a solution that worked for me, so thought I would post it here in case it helps someone else. I decided to not inherit from the ABC class, and instead just raise an exception in the "abstract" methods (the ones the derived class must implement). I did find helpful information in the Django docs, describing using Django data models as an Abstract base class and also Multi-table inheritance.

Django Data Model as an Abstract Base Class


Quoted from the docs:

Abstract base classes are useful when you want to put some common information into a number of other models. You write your base class and put abstract=True in the Meta class. This model will then not be used to create any database table. Instead, when it is used as a base class for other models, its fields will be added to those of the child class.

An example:

from django.db import models

class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True

class Student(CommonInfo):
    home_group = models.CharField(max_length=5)

The Student model will have three fields: name, age and home_group. The CommonInfo model cannot be used as a normal Django model, since it is an abstract base class. It does not generate a database table or have a manager, and cannot be instantiated or saved directly.

Fields inherited from abstract base classes can be overridden with another field or value, or be removed with None.


Multi-table Inheritance with a Django Data Model


My understanding of "multi-table inheritance" is, you can define a data model and then also use it as a base class for a second data model. The second data model will inherit all the fields from the 1st model, plus its own fields.

Quoted from the docs:

The second type of model inheritance supported by Django is when each model in the hierarchy is a model all by itself. Each model corresponds to its own database table and can be queried and created individually. The inheritance relationship introduces links between the child model and each of its parents (via an automatically-created OneToOneField). For example:

from django.db import models

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

All of the fields of Place will also be available in Restaurant, although the data will reside in a different database table. So these are both possible:

>>> Place.objects.filter(name="Bob's Cafe")
>>> Restaurant.objects.filter(name="Bob's Cafe")
like image 24
MikeyE Avatar answered Oct 02 '22 11:10

MikeyE