diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index aa649176d2..739dc25297 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -76,7 +76,7 @@ def get_commands(): return commands -def call_command(name, *args, **options): +def call_command(command_name, *args, **options): """ Calls the given command, with the given options and args/kwargs. @@ -95,25 +95,25 @@ def call_command(name, *args, **options): call_command(cmd, verbosity=0, interactive=False) # Do something with cmd ... """ - if isinstance(name, BaseCommand): + if isinstance(command_name, BaseCommand): # Command object passed in. - command = name - name = command.__class__.__module__.split('.')[-1] + command = command_name + command_name = command.__class__.__module__.split('.')[-1] else: # Load the command object by name. try: - app_name = get_commands()[name] + app_name = get_commands()[command_name] except KeyError: - raise CommandError("Unknown command: %r" % name) + raise CommandError("Unknown command: %r" % command_name) if isinstance(app_name, BaseCommand): # If the command is already loaded, use it directly. command = app_name else: - command = load_command_class(app_name, name) + command = load_command_class(app_name, command_name) # Simulate argument parsing to get the option defaults (see #10080 for details). - parser = command.create_parser('', name) + parser = command.create_parser('', command_name) # Use the `dest` option name from the parser option opt_mapping = { sorted(s_opt.option_strings)[0].lstrip('-').replace('-', '_'): s_opt.dest diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index 6766434193..b8c05486fc 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -14,6 +14,7 @@ from django.db.migrations.questioner import ( NonInteractiveMigrationQuestioner, ) from django.db.migrations.state import ProjectState +from django.db.migrations.utils import get_migration_name_timestamp from django.db.migrations.writer import MigrationWriter from django.utils.deprecation import RemovedInDjango20Warning from django.utils.six import iteritems @@ -283,7 +284,11 @@ class Command(BaseCommand): subclass = type("Migration", (Migration, ), { "dependencies": [(app_label, migration.name) for migration in merge_migrations], }) - new_migration = subclass("%04i_merge" % (biggest_number + 1), app_label) + migration_name = "%04i_%s" % ( + biggest_number + 1, + self.migration_name or ("merge_%s" % get_migration_name_timestamp()) + ) + new_migration = subclass(migration_name, app_label) writer = MigrationWriter(new_migration) if not self.dry_run: diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index 85e50746db..08baf473b0 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import datetime import functools import re from itertools import chain @@ -12,7 +11,9 @@ from django.db.migrations.migration import Migration from django.db.migrations.operations.models import AlterModelOptions from django.db.migrations.optimizer import MigrationOptimizer from django.db.migrations.questioner import MigrationQuestioner -from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject +from django.db.migrations.utils import ( + COMPILED_REGEX_TYPE, RegexObject, get_migration_name_timestamp, +) from django.utils import six from .topological_sort import stable_topological_sort @@ -1154,7 +1155,7 @@ class MigrationAutodetector(object): elif len(ops) > 1: if all(isinstance(o, operations.CreateModel) for o in ops): return "_".join(sorted(o.name_lower for o in ops)) - return "auto_%s" % datetime.datetime.now().strftime("%Y%m%d_%H%M") + return "auto_%s" % get_migration_name_timestamp() @classmethod def parse_number(cls, name): diff --git a/django/db/migrations/utils.py b/django/db/migrations/utils.py index 5bea82dc51..b54d7ed120 100644 --- a/django/db/migrations/utils.py +++ b/django/db/migrations/utils.py @@ -1,3 +1,4 @@ +import datetime import re COMPILED_REGEX_TYPE = type(re.compile('')) @@ -10,3 +11,7 @@ class RegexObject(object): def __eq__(self, other): return self.pattern == other.pattern and self.flags == other.flags + + +def get_migration_name_timestamp(): + return datetime.datetime.now().strftime("%Y%m%d_%H%M") diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 4d1be3f58f..cc655f112f 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import codecs +import datetime import importlib import os import sys @@ -705,7 +706,7 @@ class MakeMigrationsTests(MigrationTestBase): # Monkeypatch interactive questioner to auto reject with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='N')): with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: - call_command("makemigrations", "migrations", merge=True, interactive=True, verbosity=0) + call_command("makemigrations", "migrations", name="merge", merge=True, interactive=True, verbosity=0) merge_file = os.path.join(migration_dir, '0003_merge.py') self.assertFalse(os.path.exists(merge_file)) @@ -717,11 +718,22 @@ class MakeMigrationsTests(MigrationTestBase): with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='y')): out = six.StringIO() with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: - call_command("makemigrations", "migrations", merge=True, interactive=True, stdout=out) + call_command("makemigrations", "migrations", name="merge", merge=True, interactive=True, stdout=out) merge_file = os.path.join(migration_dir, '0003_merge.py') self.assertTrue(os.path.exists(merge_file)) self.assertIn("Created new merge migration", force_text(out.getvalue())) + @mock.patch('django.db.migrations.utils.datetime') + def test_makemigrations_default_merge_name(self, mock_datetime): + mock_datetime.datetime.now.return_value = datetime.datetime(2016, 1, 2, 3, 4) + with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='y')): + out = six.StringIO() + with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: + call_command("makemigrations", "migrations", merge=True, interactive=True, stdout=out) + merge_file = os.path.join(migration_dir, '0003_merge_20160102_0304.py') + self.assertTrue(os.path.exists(merge_file)) + self.assertIn("Created new merge migration", force_text(out.getvalue())) + def test_makemigrations_non_interactive_not_null_addition(self): """ Tests that non-interactive makemigrations fails when a default is missing on a new not-null field. @@ -793,7 +805,7 @@ class MakeMigrationsTests(MigrationTestBase): """ out = six.StringIO() with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: - call_command("makemigrations", "migrations", merge=True, interactive=False, stdout=out) + call_command("makemigrations", "migrations", name="merge", merge=True, interactive=False, stdout=out) merge_file = os.path.join(migration_dir, '0003_merge.py') self.assertTrue(os.path.exists(merge_file)) output = force_text(out.getvalue()) @@ -809,7 +821,10 @@ class MakeMigrationsTests(MigrationTestBase): """ out = six.StringIO() with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: - call_command("makemigrations", "migrations", dry_run=True, merge=True, interactive=False, stdout=out) + call_command( + "makemigrations", "migrations", name="merge", dry_run=True, + merge=True, interactive=False, stdout=out, + ) merge_file = os.path.join(migration_dir, '0003_merge.py') self.assertFalse(os.path.exists(merge_file)) output = force_text(out.getvalue()) @@ -825,8 +840,10 @@ class MakeMigrationsTests(MigrationTestBase): """ out = six.StringIO() with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: - call_command("makemigrations", "migrations", dry_run=True, merge=True, interactive=False, - stdout=out, verbosity=3) + call_command( + "makemigrations", "migrations", name="merge", dry_run=True, + merge=True, interactive=False, stdout=out, verbosity=3, + ) merge_file = os.path.join(migration_dir, '0003_merge.py') self.assertFalse(os.path.exists(merge_file)) output = force_text(out.getvalue()) @@ -928,7 +945,7 @@ class MakeMigrationsTests(MigrationTestBase): out = six.StringIO() with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='N')): with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: - call_command("makemigrations", "migrations", merge=True, stdout=out) + call_command("makemigrations", "migrations", name="merge", merge=True, stdout=out) merge_file = os.path.join(migration_dir, '0003_merge.py') # This will fail if interactive is False by default self.assertFalse(os.path.exists(merge_file)) @@ -959,7 +976,7 @@ class MakeMigrationsTests(MigrationTestBase): with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='y')): out = six.StringIO() with self.temporary_migration_module(app_label="migrated_app") as migration_dir: - call_command("makemigrations", "migrated_app", merge=True, interactive=True, stdout=out) + call_command("makemigrations", "migrated_app", name="merge", merge=True, interactive=True, stdout=out) merge_file = os.path.join(migration_dir, '0003_merge.py') self.assertFalse(os.path.exists(merge_file)) self.assertIn("No conflicts detected to merge.", out.getvalue())