How to Disallow Auto-named Django Migrations

When you run Django’s manage.py makemigrations , it will try to generate a name for the migration based upon its contents. For example, if you are adding a single field, it will call the migration 0002_mymodel_myfield.py . However when your migration contains more than one step, it instead uses a simple ‘auto’ name with the current date + time, e.g. 0002_auto_20200113_1837.py . You can provide the -n / --name argument to makemigrations , but developers often forget this.

Naming things is a known hard problem in programming. Having migrations with these automatic names makes managing them harder: You can’t tell which is which without opening them, and you could still mix them up if they have similar names due to being generated on the same day.

This becomes painful when:

rebasing branches

digging through history

deploying to production

In the worst case, running the wrong migration could lead to data loss!

It’s also all too easy to forget to fix the name and commit since Django doesn’t prompt you for a better name. We can guard against this with some automation!

Let’s look at three techniques to do so.

Update (2020-02-25): Originally this article just included my custom system check (#2). Thanks to wonderful feedback on Reddit and Twitter, I've included two more methods, both of which are shorter.

1. Overriding makemigrations to require -n / --name

Update (2020-02-25): Thanks to @toyg on reddit for pointing this one out.

This uses the same technique of overriding a built-in management command as I used in my post “Make Django Tests Always Rebuild the Database if It Exists”.

Add a new makemigrations command to the “core” app in your project (e.g. myapp/management/commands/makemigrations.py ) with this content:

from django.core.management.base import CommandError from django.core.management.commands.makemigrations import Command as BaseCommand class Command ( BaseCommand ): def handle ( self , * app_labels , ** options ): if options [ "name" ] is None : raise CommandError ( "Myproject customization: -n/--name is required." ) super (). handle ( * app_labels , ** options )

(Replace “Myproject” with the name of your project.)

Then running makemigrations will output a message like this:

$ python manage.py makemigrations Myproject customization: -n/--name is required.

Because this only applies to makemigrations , it automatically only affects new migrations, and not those in third party apps. Nice.

2. A Custom System Check

Update (2020-02-25): Thanks to Nikita Sobolev, this check is available in the package `django-test-migrations` from version 0.2.0+. See "Testing migration names" in its documentation.

This is a custom system check that I’ve used on a few client projects.

To add it your project, you’ll first want to add it to a module inside one of your apps. I normally write add checks.py in the project’s “core” app (whatever it’s called):

# myapp/checks.py from fnmatch import fnmatch from django.core.checks import Error def check_migration_names ( app_configs , ** kwargs ): from django.db.migrations.loader import MigrationLoader loader = MigrationLoader ( None , ignore_no_migrations = True ) loader . load_disk () errors = [] for ( app_label , migration_name ), _ in loader . disk_migrations . items (): if ( app_label , migration_name ) in IGNORED_BADLY_NAMED_MIGRATIONS : continue elif fnmatch ( migration_name , "????_auto_*" ): errors . append ( Error ( f"Migration { app_label } . { migration_name } has an automatic name." , hint = ( "Rename the migration to describe its contents, or if " + "it's from a third party app, add to " + "IGNORED_BADLY_NAMED_MIGRATIONS" ), id = "myapp.E001" , ) ) return errors IGNORED_BADLY_NAMED_MIGRATIONS = { # Use to ignore pre-existing auto-named migrations: # ('myapp', '0002_auto_20200123_1257'), }

Some notes on the code:

We need to use an inner import for MigrationLoader , since it depends on all the Django having loaded all the apps, and we will import our check before then.

, since it depends on all the Django having loaded all the apps, and we will import our check before then. We tell the migration loader to load the names of all migrations from disk and iterate over them.

We use the standard library fnmatch function to perform simple string matching on the filename. This is easier to read and write than using regular expressions.

function to perform simple string matching on the filename. This is easier to read and write than using regular expressions. We have IGNORED_BADLY_NAMED_MIGRATIONS at the bottom, a set of two-tuples like (app name, migration name). I’ve left a commented example of the expected data structure.

To run the check we need to register it in our app’s AppConfig.ready() :

# myapp/apps.py from django.apps import AppConfig from django.core import checks from myapp.checks import check_migration_names class MyappConfig ( AppConfig ): name = 'myapp' def ready ( self ): checks . register ( checks . Tags . compatibility )( check_migration_names )

…and ensure we use our AppConfig in INSTALLED_APPS :

INSTALLED_APPS = [ # ... 'myapp.apps.MyappConfig' , # ... ]

Running checks will highlight any problematic migration files:

$ python manage.py check SystemCheckError: System check identified some issues: ERRORS: ?: (myapp.E001) Migration myapp.0002_auto_20200123_1257 has an automatic name. HINT: Rename the migration to describe its contents, or if it's from a third party app, add to IGNORED_BADLY_NAMED_MIGRATIONS System check identified 1 issue (0 silenced).

Django also runs checks at the start of most manage.py commands, and in the test runner.

If you’re adding this to a project with pre-existing auto-named migrations, you will see each as an error. You should add them to IGNORED_BADLY_NAMED_MIGRATIONS , rather than renaming them. Django only knows migrations by name, so if you rename them, it will detect them as not applied and try apply them again - woops.

3. With a pre-commit Hook

Update (2020-02-25): Anthony Sottile, creator of pre-commit, pointed out this shorter technique on Twitter.

If you’re using pre-commit (and you should, it’s really good!), you can also use a hook to ban auto-generated files with much less code:

- repo : local hooks : - id : no-auto-migrations name : no auto-named migrations entry : please provide a descriptive name for migrations language : fail files : .*/migrations/.*_auto_.*\.py$ exclude : ^ (?x)^( myapp/migrations/0002_auto_20200123_1257\.py |myapp/migrations/0003_auto_20200123_1621\.py )$

This uses the fail pseudo-language to automatically fail any files matching that regex. Pretty neat!

The only downside of this approach is that you have to use a long regex in exclude to skip pre-existing badly named migrations.

Fin

I hope this helps you keep your projects’ code just a bit cleaner,

—Adam

Working on a Django project? Check out my book Speed Up Your Django Tests which covers loads of best practices so you can write faster, more accurate tests.

Subscribe via RSS, Twitter, or email: Your email address:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: django

© 2020 All rights reserved.