CNK's Blog

Trimming Django Migration Cruft

Django creates migrations for Django model changes that do not alter the database, for example, changes to help text or verbose names. In most cases when I see a migration for a change I am pretty sure doesn’t run any SQL, I check my assumption using python manage.py sqlmigrate <app> <migration_name>, and if it does not produce any SQL, then I edit the most recent migration to have touched that column to match the “changes” Django wants to make. For the most part that isn’t difficult but it is sometimes annoying. Other people have a similar opinion and one of them shared the following code on a Slack channel I am on.

WARNING: I have included the code as it was from the shared file, but my application had some data migrations with RunPython commands that invoke related_name. So in our application, we deleted the code below that removed attributes in MIGRATION_IGNORE_RELATED_FIELD_ATTRS.

  # app/management/commands/__init__.py

  """
  Django creates redundant migrations for Django model changes that do not alter the database.
  Here we patch Django's migration machinery to ignore attrs.

  The management commands `makemigrations` and `migrate` will ignore the attrs defined in:

      - MIGRATION_IGNORE_MODEL_ATTRS
      - MIGRATION_IGNORE_FIELD_ATTRS
      - MIGRATION_IGNORE_FILE_FIELD_ATTRS
      - MIGRATION_IGNORE_RELATED_FIELD_ATTRS

  This will reduce the number of migrations and therefore speed-up development
  """

  import logging
  from functools import wraps

  from django.db.migrations.operations import AlterModelOptions
  from django.db.models import Field, FileField
  from django.db.models.fields.related import RelatedField

  logger = logging.getLogger(__name__)

  MIGRATION_IGNORE_MODEL_ATTRS = ["verbose_name", "verbose_name_plural"]
  MIGRATION_IGNORE_FIELD_ATTRS = ["validators", "choices", "help_text", "verbose_name"]
  MIGRATION_IGNORE_FILE_FIELD_ATTRS = ["upload_to", "storage"]
  MIGRATION_IGNORE_RELATED_FIELD_ATTRS = ["related_name", "related_query_name"]

  for attr in MIGRATION_IGNORE_MODEL_ATTRS:
      logger.info(f"Model {attr} attr will be ignored.")

  for attr in MIGRATION_IGNORE_FIELD_ATTRS:
      logger.info(f"Field {attr} attr will be ignored.")

  for attr in MIGRATION_IGNORE_FILE_FIELD_ATTRS:
      logger.info(f"File field {attr} attr will be ignored.")

  # We found we needed the related field info so did not remove these
  # for attr in MIGRATION_IGNORE_RELATED_FIELD_ATTRS:
  #     logger.info(f"Related field {attr} attr will be ignored.")


  def patch_ignored_model_attrs(cls):
      for attr in MIGRATION_IGNORE_MODEL_ATTRS:
          cls.ALTER_OPTION_KEYS.remove(attr)


  def patch_field_deconstruct(old_func):
      @wraps(old_func)
      def deconstruct_with_ignored_attrs(self):
          name, path, args, kwargs = old_func(self)
          for attr in MIGRATION_IGNORE_FIELD_ATTRS:
              kwargs.pop(attr, None)
          return name, path, args, kwargs

      return deconstruct_with_ignored_attrs


  def patch_file_field_deconstruct(old_func):
      @wraps(old_func)
      def deconstruct_with_ignored_attrs(self):
          name, path, args, kwargs = old_func(self)
          for attr in MIGRATION_IGNORE_FILE_FIELD_ATTRS:
              kwargs.pop(attr, None)
          return name, path, args, kwargs

      return deconstruct_with_ignored_attrs


  def patch_related_field_deconstruct(old_func):
      @wraps(old_func)
      def deconstruct_with_ignored_attrs(self):
          name, path, args, kwargs = old_func(self)
          for attr in MIGRATION_IGNORE_RELATED_FIELD_ATTRS:
              kwargs.pop(attr, None)
          return name, path, args, kwargs

      return deconstruct_with_ignored_attrs


  Field.deconstruct = patch_field_deconstruct(Field.deconstruct)
  FileField.deconstruct = patch_file_field_deconstruct(FileField.deconstruct)
  # We found we needed the related field info so did not remove these fields
  # RelatedField.deconstruct = patch_related_field_deconstruct(RelatedField.deconstruct)
  patch_ignored_model_attrs(AlterModelOptions)

And now, create override files for the two manage commands we need to load our patches: migrate and makemigrations.

  # app/management/commands/makemigrations.py

  """
  Override of Django makemigrations. When we use this version, we
  will load the __init__ file above that patches models.Field.
  """

  from django.core.management.commands.makemigrations import Command  # noqa
  # app/management/commands/migrate.py

  """
  Override of Django migrate. When we use this version, we
  will load the __init__ file above that patches models.Field.
  """

  from django.core.management.commands.migrate import Command  # noqa