1
0
mirror of https://github.com/django/django.git synced 2024-12-22 09:05:43 +00:00

Fixed #35656 -- Added an autodetector attribute to the makemigrations and migrate commands.

This commit is contained in:
leondaz 2024-09-09 19:15:40 +03:00 committed by Sarah Boyce
parent dc626fbe3a
commit 06bf06a911
10 changed files with 142 additions and 4 deletions

View File

@ -16,6 +16,7 @@ from .registry import Tags, register, run_checks, tag_exists
# Import these to force registration of checks # Import these to force registration of checks
import django.core.checks.async_checks # NOQA isort:skip import django.core.checks.async_checks # NOQA isort:skip
import django.core.checks.caches # 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.compatibility.django_4_0 # NOQA isort:skip
import django.core.checks.database # NOQA isort:skip import django.core.checks.database # NOQA isort:skip
import django.core.checks.files # NOQA isort:skip import django.core.checks.files # NOQA isort:skip

View File

@ -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 []

View File

@ -12,6 +12,7 @@ class Tags:
admin = "admin" admin = "admin"
async_support = "async_support" async_support = "async_support"
caches = "caches" caches = "caches"
commands = "commands"
compatibility = "compatibility" compatibility = "compatibility"
database = "database" database = "database"
files = "files" files = "files"

View File

@ -24,6 +24,7 @@ from django.db.migrations.writer import MigrationWriter
class Command(BaseCommand): class Command(BaseCommand):
autodetector = MigrationAutodetector
help = "Creates new migration(s) for apps." help = "Creates new migration(s) for apps."
def add_arguments(self, parser): def add_arguments(self, parser):
@ -209,7 +210,7 @@ class Command(BaseCommand):
log=self.log, log=self.log,
) )
# Set up autodetector # Set up autodetector
autodetector = MigrationAutodetector( autodetector = self.autodetector(
loader.project_state(), loader.project_state(),
ProjectState.from_apps(apps), ProjectState.from_apps(apps),
questioner, questioner,
@ -461,7 +462,7 @@ class Command(BaseCommand):
# If they still want to merge it, then write out an empty # If they still want to merge it, then write out an empty
# file depending on the migrations needing merging. # file depending on the migrations needing merging.
numbers = [ numbers = [
MigrationAutodetector.parse_number(migration.name) self.autodetector.parse_number(migration.name)
for migration in merge_migrations for migration in merge_migrations
] ]
try: try:

View File

@ -15,6 +15,7 @@ from django.utils.text import Truncator
class Command(BaseCommand): class Command(BaseCommand):
autodetector = MigrationAutodetector
help = ( help = (
"Updates database schema. Manages both apps with migrations and those without." "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.") self.stdout.write(" No migrations to apply.")
# If there's changes that aren't in migrations yet, tell them # If there's changes that aren't in migrations yet, tell them
# how to fix it. # how to fix it.
autodetector = MigrationAutodetector( autodetector = self.autodetector(
executor.loader.project_state(), executor.loader.project_state(),
ProjectState.from_apps(apps), ProjectState.from_apps(apps),
) )

View File

@ -77,6 +77,7 @@ Django's system checks are organized using the following tags:
* ``async_support``: Checks asynchronous-related configuration. * ``async_support``: Checks asynchronous-related configuration.
* ``caches``: Checks cache related configuration. * ``caches``: Checks cache related configuration.
* ``compatibility``: Flags potential problems with version upgrades. * ``compatibility``: Flags potential problems with version upgrades.
* ``commands``: Checks custom management commands related configuration.
* ``database``: Checks database-related configuration issues. Database checks * ``database``: Checks database-related configuration issues. Database checks
are not run by default because they do more than static code analysis as 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 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 * **models.W047**: ``<database>`` does not support unique constraints with
nulls distinct. 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 Security
-------- --------

View File

@ -230,6 +230,10 @@ Management Commands
setting the :envvar:`HIDE_PRODUCTION_WARNING` environment variable to setting the :envvar:`HIDE_PRODUCTION_WARNING` environment variable to
``"true"``. ``"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 Migrations
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -0,0 +1,7 @@
from django.core.management.commands.makemigrations import (
Command as MakeMigrationsCommand,
)
class Command(MakeMigrationsCommand):
autodetector = int

View File

@ -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],
)

View File

@ -9,6 +9,10 @@ from unittest import mock
from django.apps import apps from django.apps import apps
from django.core.management import CommandError, call_command 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 ( from django.db import (
ConnectionHandler, ConnectionHandler,
DatabaseError, DatabaseError,
@ -19,10 +23,11 @@ from django.db import (
) )
from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.backends.utils import truncate_name 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.exceptions import InconsistentMigrationHistory
from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.recorder import MigrationRecorder
from django.test import TestCase, override_settings, skipUnlessDBFeature 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 import timezone
from django.utils.version import get_docs_version 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'." msg = "Cannot find a migration matching 'nonexistent' from app 'migrations'."
with self.assertRaisesMessage(CommandError, msg): with self.assertRaisesMessage(CommandError, msg):
call_command("optimizemigration", "migrations", "nonexistent") 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)