summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorleondaz <ahmeddark369@gmail.com>2024-09-09 19:15:40 +0300
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-10-15 16:39:12 +0200
commit06bf06a911695c5c84f746742f764c040e237ece (patch)
tree3f54dfb3f6889c3f5d6a676e5be8556c0fe303ed
parentdc626fbe3ae0225b765df71d08fab02971dc6c6f (diff)
Fixed #35656 -- Added an autodetector attribute to the makemigrations and migrate commands.
-rw-r--r--django/core/checks/__init__.py1
-rw-r--r--django/core/checks/commands.py28
-rw-r--r--django/core/checks/registry.py1
-rw-r--r--django/core/management/commands/makemigrations.py5
-rw-r--r--django/core/management/commands/migrate.py3
-rw-r--r--docs/ref/checks.txt9
-rw-r--r--docs/releases/5.2.txt4
-rw-r--r--tests/check_framework/custom_commands_app/management/commands/makemigrations.py7
-rw-r--r--tests/check_framework/test_commands.py25
-rw-r--r--tests/migrations/test_commands.py63
10 files changed, 142 insertions, 4 deletions
diff --git a/django/core/checks/__init__.py b/django/core/checks/__init__.py
index 998ab9dee2..2502450cdf 100644
--- a/django/core/checks/__init__.py
+++ b/django/core/checks/__init__.py
@@ -16,6 +16,7 @@ from .registry import Tags, register, run_checks, tag_exists
# 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
diff --git a/django/core/checks/commands.py b/django/core/checks/commands.py
new file mode 100644
index 0000000000..eee1e937e8
--- /dev/null
+++ b/django/core/checks/commands.py
@@ -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 []
diff --git a/django/core/checks/registry.py b/django/core/checks/registry.py
index 146b28f65e..3139fc3ef4 100644
--- a/django/core/checks/registry.py
+++ b/django/core/checks/registry.py
@@ -12,6 +12,7 @@ class Tags:
admin = "admin"
async_support = "async_support"
caches = "caches"
+ commands = "commands"
compatibility = "compatibility"
database = "database"
files = "files"
diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py
index a4e4d520e6..d5d3466201 100644
--- a/django/core/management/commands/makemigrations.py
+++ b/django/core/management/commands/makemigrations.py
@@ -24,6 +24,7 @@ from django.db.migrations.writer import MigrationWriter
class Command(BaseCommand):
+ autodetector = MigrationAutodetector
help = "Creates new migration(s) for apps."
def add_arguments(self, parser):
@@ -209,7 +210,7 @@ class Command(BaseCommand):
log=self.log,
)
# Set up autodetector
- autodetector = MigrationAutodetector(
+ autodetector = self.autodetector(
loader.project_state(),
ProjectState.from_apps(apps),
questioner,
@@ -461,7 +462,7 @@ class Command(BaseCommand):
# 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:
diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py
index 5e6b19c095..fa420ee6e3 100644
--- a/django/core/management/commands/migrate.py
+++ b/django/core/management/commands/migrate.py
@@ -15,6 +15,7 @@ from django.utils.text import Truncator
class Command(BaseCommand):
+ autodetector = MigrationAutodetector
help = (
"Updates database schema. Manages both apps with migrations and those without."
)
@@ -329,7 +330,7 @@ class Command(BaseCommand):
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),
)
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index d78a6f76b2..2308a854c7 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -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
@@ -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
--------
diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt
index eabc27c277..78c96688cf 100644
--- a/docs/releases/5.2.txt
+++ b/docs/releases/5.2.txt
@@ -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
~~~~~~~~~~
diff --git a/tests/check_framework/custom_commands_app/management/commands/makemigrations.py b/tests/check_framework/custom_commands_app/management/commands/makemigrations.py
new file mode 100644
index 0000000000..a6494cba4c
--- /dev/null
+++ b/tests/check_framework/custom_commands_app/management/commands/makemigrations.py
@@ -0,0 +1,7 @@
+from django.core.management.commands.makemigrations import (
+ Command as MakeMigrationsCommand,
+)
+
+
+class Command(MakeMigrationsCommand):
+ autodetector = int
diff --git a/tests/check_framework/test_commands.py b/tests/check_framework/test_commands.py
new file mode 100644
index 0000000000..a51db77402
--- /dev/null
+++ b/tests/check_framework/test_commands.py
@@ -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],
+ )
diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py
index cab2906ed1..724c88a28f 100644
--- a/tests/migrations/test_commands.py
+++ b/tests/migrations/test_commands.py
@@ -9,6 +9,10 @@ from unittest import mock
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,
@@ -19,10 +23,11 @@ from django.db import (
)
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
@@ -3296,3 +3301,59 @@ class OptimizeMigrationTests(MigrationTestBase):
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)