How can I enforce inheritance in my Django models?

Question:

In my Django app, I have an abstract model called MyModel that has e.g. created_at and updated_at fields. I want all the models in my project to subclass MyModel rather than using django.db.models.Model directly.

We have several developers on our app, so I want to use some sort of linter or CI check to enforce that this happens. How can I do this?

Asked By: tao_oat

||

Answers:

As mentioned by Willem Van Onsem in their comment you can write your own checks using the System check framework.

Assuming we have the following models in an app named "checktest" with Parent being the model that all models should inherit from:

from django.db import models


class Parent(models.Model):
    class Meta:
        abstract = True


# This should raise an error
class Foo(models.Model):
    pass


# This should be fine
class Bar(Parent):
    pass

We’ll write a check as follows in a file checktest/custom_checks.py, note that the list APPS_TO_TEST contains the names of the apps whose models should inherit from the parent class:

from django.apps import apps
from django.core.checks import Error, register, Tags
from .models import Parent

# List of apps that should inherit from Parent
APPS_TO_TEST = ["checktest"]


@register(Tags.models)
def model_must_inherit(app_configs, **kwargs):
    errors = []
    for app in APPS_TO_TEST:
        models = apps.get_app_config(app).get_models()
        for model in models:
            if not issubclass(model, Parent):
                errors.append(Error(
                    f"Model {model.__name__} does not inherit from Parent",
                    hint="Models must inherit from the Parent class",
                    obj=model,
                    id="checktest.E001"
                ))
    return errors

In the app configs ready method we’ll import the above file so that the check will get run:

from django.apps import AppConfig


class ChecktestConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'checktest'

    def ready(self) -> None:
        from . import custom_checks

Now whenever we run commands like runserver or migrate the checks will get implicitly run. In a CI environment you can explicitly run the checks using the check command.

Answered By: Abdul Aziz Barkat