Formater automatiquement les migrations Django avec black

Ami·e qui écrit du code avec le framework Django, peut-être t'es-tu mis à utiliser le fantastique formateur de code black qui permet de se libérer les neurones d'un peu de bikesheding ?

Ton éditeur est bien configuré pour reformater ton code avec black automatiquement… Oui mais voilà… Quand tu génères tes migrations de base de donnée avec ./manage.py makemigrations, tu dois repasser derrière pour formater le fichier .py de migration généré 😭.

Voici une solution pour que makemigrations produise une migration correctement formatée au sens de black.

Alors déjà, si tu utilises Django en version 4.1 ou supérieure, il n'y a rien à faire, c'est inclus. Bonne journée 😘…

Dans le cas contraire, la bricole suivante permet de faire passer black automatiquement sur les migrations créées.

Surcharger la commande ./manage.py makemigrations

On va étendre le comportement de la commande makemigrations pour qu'elle inclue le reformatage.

Crée un fichier makemigrations.py à l'endroit suivant de ton projet Django (imaginons que le projet se nomme « monprojet ») :

monprojet/
├── management
   ├── commands
      ├── __init__.py
      └── makemigrations.py
   └── __init__.py

[]

(NB: il faut créer les dossiers management, commands et les fichiers (vides) __init__.py si ils n'existent pas déjà).

Détail : on est en train de créer une commande django-admin, et comme elle possède le même nom que la commande makemigrations inclue avec Django, la notre va la « remplacer ».

… Avec le contenu suivant :

import glob
import subprocess

from django.core.management.commands.makemigrations import (
    Command as CoreMakeMigrationsCommand,
)


def black(filepath: str):
    """Calls black for a given path/wildcard

    assume black is installed and in PATH, will fail on exception otherwise…

    :param filepath: can be an unix glob including wildcard in file paths.
    """
    paths_list = glob.glob(filepath)
    subprocess.check_call(["black", "-q", *paths_list])


class Command(CoreMakeMigrationsCommand):
    def write_migration_files(self, changes):
        ret = super(Command, self).write_migration_files(changes)
        for app_name, migrations in changes.items():
            for migration in migrations:
                black(f"{app_name}/migrations/{migration.name}.py")

        return ret

    def handle_merge(self, *args, **kwargs):
        super().handle_merge(*args, **kwargs)
        # There is nowhere to get reliably the name of generated file
        # So call on all "migrations" folders.
        black(f"*/migrations")

Détail : on se contente d'hériter le fonctionnement de la commande originale en lui adjoignant une exécution de black par fichier de migration généré.

Il suffit ensuite d'utiliser ./manage.py makemigrations comme d'habitude, joie 😎.


  1. par exemple moi j'utilise uniquement les LTS, donc à la date de rédaction de cette article, la 3.2