Skip to content

Commit

Permalink
Fixed #35656 -- Added an autodetector attribute to the makemigrations…
Browse files Browse the repository at this point in the history
… and migrate commands.
  • Loading branch information
LeOndaz authored and sarahboyce committed Oct 15, 2024
1 parent dc626fb commit 06bf06a
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 4 deletions.
1 change: 1 addition & 0 deletions django/core/checks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# Import these to force registration of checks
import django.core.checks.async_checks # NOQA isort:skip
import django.core.checks.caches # NOQA isort:skip
import django.core.checks.commands # NOQA isort:skip
import django.core.checks.compatibility.django_4_0 # NOQA isort:skip
import django.core.checks.database # NOQA isort:skip
import django.core.checks.files # NOQA isort:skip
Expand Down
28 changes: 28 additions & 0 deletions django/core/checks/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.core.checks import Error, Tags, register


@register(Tags.commands)
def migrate_and_makemigrations_autodetector(**kwargs):
from django.core.management import get_commands, load_command_class

commands = get_commands()

make_migrations = load_command_class(commands["makemigrations"], "makemigrations")
migrate = load_command_class(commands["migrate"], "migrate")

if make_migrations.autodetector is not migrate.autodetector:
return [
Error(
"The migrate and makemigrations commands must have the same "
"autodetector.",
hint=(
f"makemigrations.Command.autodetector is "
f"{make_migrations.autodetector.__name__}, but "
f"migrate.Command.autodetector is "
f"{migrate.autodetector.__name__}."
),
id="commands.E001",
)
]

return []
1 change: 1 addition & 0 deletions django/core/checks/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Tags:
admin = "admin"
async_support = "async_support"
caches = "caches"
commands = "commands"
compatibility = "compatibility"
database = "database"
files = "files"
Expand Down
5 changes: 3 additions & 2 deletions django/core/management/commands/makemigrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@


class Command(BaseCommand):
autodetector = MigrationAutodetector
help = "Creates new migration(s) for apps."

def add_arguments(self, parser):
Expand Down Expand Up @@ -209,7 +210,7 @@ def handle(self, *app_labels, **options):
log=self.log,
)
# Set up autodetector
autodetector = MigrationAutodetector(
autodetector = self.autodetector(
loader.project_state(),
ProjectState.from_apps(apps),
questioner,
Expand Down Expand Up @@ -461,7 +462,7 @@ def all_items_equal(seq):
# If they still want to merge it, then write out an empty
# file depending on the migrations needing merging.
numbers = [
MigrationAutodetector.parse_number(migration.name)
self.autodetector.parse_number(migration.name)
for migration in merge_migrations
]
try:
Expand Down
3 changes: 2 additions & 1 deletion django/core/management/commands/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@


class Command(BaseCommand):
autodetector = MigrationAutodetector
help = (
"Updates database schema. Manages both apps with migrations and those without."
)
Expand Down Expand Up @@ -329,7 +330,7 @@ def handle(self, *args, **options):
self.stdout.write(" No migrations to apply.")
# If there's changes that aren't in migrations yet, tell them
# how to fix it.
autodetector = MigrationAutodetector(
autodetector = self.autodetector(
executor.loader.project_state(),
ProjectState.from_apps(apps),
)
Expand Down
9 changes: 9 additions & 0 deletions docs/ref/checks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Django's system checks are organized using the following tags:
* ``async_support``: Checks asynchronous-related configuration.
* ``caches``: Checks cache related configuration.
* ``compatibility``: Flags potential problems with version upgrades.
* ``commands``: Checks custom management commands related configuration.
* ``database``: Checks database-related configuration issues. Database checks
are not run by default because they do more than static code analysis as
regular checks do. They are only run by the :djadmin:`migrate` command or if
Expand Down Expand Up @@ -428,6 +429,14 @@ Models
* **models.W047**: ``<database>`` does not support unique constraints with
nulls distinct.

Management Commands
-------------------

The following checks verify custom management commands are correctly configured:

* **commands.E001**: The ``migrate`` and ``makemigrations`` commands must have
the same ``autodetector``.

Security
--------

Expand Down
4 changes: 4 additions & 0 deletions docs/releases/5.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ Management Commands
setting the :envvar:`HIDE_PRODUCTION_WARNING` environment variable to
``"true"``.

* The :djadmin:`makemigrations` and :djadmin:`migrate` commands have a new
``Command.autodetector`` attribute for subclasses to override in order to use
a custom autodetector class.

Migrations
~~~~~~~~~~

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.core.management.commands.makemigrations import (
Command as MakeMigrationsCommand,
)


class Command(MakeMigrationsCommand):
autodetector = int
25 changes: 25 additions & 0 deletions tests/check_framework/test_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.core import checks
from django.core.checks import Error
from django.test import SimpleTestCase
from django.test.utils import isolate_apps, override_settings, override_system_checks


@isolate_apps("check_framework.custom_commands_app", attr_name="apps")
@override_settings(INSTALLED_APPS=["check_framework.custom_commands_app"])
@override_system_checks([checks.commands.migrate_and_makemigrations_autodetector])
class CommandCheckTests(SimpleTestCase):
def test_migrate_and_makemigrations_autodetector_different(self):
expected_error = Error(
"The migrate and makemigrations commands must have the same "
"autodetector.",
hint=(
"makemigrations.Command.autodetector is int, but "
"migrate.Command.autodetector is MigrationAutodetector."
),
id="commands.E001",
)

self.assertEqual(
checks.run_checks(app_configs=self.apps.get_app_configs()),
[expected_error],
)
63 changes: 62 additions & 1 deletion tests/migrations/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

from django.apps import apps
from django.core.management import CommandError, call_command
from django.core.management.commands.makemigrations import (
Command as MakeMigrationsCommand,
)
from django.core.management.commands.migrate import Command as MigrateCommand
from django.db import (
ConnectionHandler,
DatabaseError,
Expand All @@ -19,10 +23,11 @@
)
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.backends.utils import truncate_name
from django.db.migrations.autodetector import MigrationAutodetector
from django.db.migrations.exceptions import InconsistentMigrationHistory
from django.db.migrations.recorder import MigrationRecorder
from django.test import TestCase, override_settings, skipUnlessDBFeature
from django.test.utils import captured_stdout, extend_sys_path
from django.test.utils import captured_stdout, extend_sys_path, isolate_apps
from django.utils import timezone
from django.utils.version import get_docs_version

Expand Down Expand Up @@ -3296,3 +3301,59 @@ def test_unknown_prefix(self):
msg = "Cannot find a migration matching 'nonexistent' from app 'migrations'."
with self.assertRaisesMessage(CommandError, msg):
call_command("optimizemigration", "migrations", "nonexistent")


class CustomMigrationCommandTests(MigrationTestBase):
@override_settings(
MIGRATION_MODULES={"migrations": "migrations.test_migrations"},
INSTALLED_APPS=["migrations.migrations_test_apps.migrated_app"],
)
@isolate_apps("migrations.migrations_test_apps.migrated_app")
def test_makemigrations_custom_autodetector(self):
class CustomAutodetector(MigrationAutodetector):
def changes(self, *args, **kwargs):
return []

class CustomMakeMigrationsCommand(MakeMigrationsCommand):
autodetector = CustomAutodetector

class NewModel(models.Model):
class Meta:
app_label = "migrated_app"

out = io.StringIO()
command = CustomMakeMigrationsCommand(stdout=out)
call_command(command, "migrated_app", stdout=out)
self.assertIn("No changes detected", out.getvalue())

@override_settings(INSTALLED_APPS=["migrations.migrations_test_apps.migrated_app"])
@isolate_apps("migrations.migrations_test_apps.migrated_app")
def test_migrate_custom_autodetector(self):
class CustomAutodetector(MigrationAutodetector):
def changes(self, *args, **kwargs):
return []

class CustomMigrateCommand(MigrateCommand):
autodetector = CustomAutodetector

class NewModel(models.Model):
class Meta:
app_label = "migrated_app"

out = io.StringIO()
command = CustomMigrateCommand(stdout=out)

out = io.StringIO()
try:
call_command(command, verbosity=0)
call_command(command, stdout=out, no_color=True)
command_stdout = out.getvalue().lower()
self.assertEqual(
"operations to perform:\n"
" apply all migrations: migrated_app\n"
"running migrations:\n"
" no migrations to apply.\n",
command_stdout,
)
finally:
call_command(command, "migrated_app", "zero", verbosity=0)

0 comments on commit 06bf06a

Please sign in to comment.