Search code examples
pythondjangodjango-modelsflake8

How can I enforce inheritance in my Django models?


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?


Solution

  • 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.