def _get_delete_model_migration(self, dependencies=[]): database_operations = [] state_operations = [] state_operations.append(migrations.DeleteModel(name=self.model_name)) migration = Migration('delete_model', self.source_app) migration.dependencies = dependencies migration.operations = [ migrations.SeparateDatabaseAndState( database_operations=database_operations, state_operations=state_operations, ) ] return migration
def test_has_errors(self): linter = MigrationLinter() self.assertFalse(linter.has_errors) m = Migration("0001_create_table", "app_add_not_null_column") linter.lint_migration(m) self.assertFalse(linter.has_errors) m = Migration("0002_add_new_not_null_field", "app_add_not_null_column") linter.lint_migration(m) self.assertTrue(linter.has_errors) m = Migration("0001_create_table", "app_add_not_null_column") linter.lint_migration(m) self.assertTrue(linter.has_errors)
def _gather_migrations_git(self, git_commit_id): from django.db.migrations.loader import MIGRATIONS_MODULE_NAME migrations = [] # Get changes since specified commit git_diff_command = ( "cd {0} && git diff --relative --name-only --diff-filter=AR {1}" ).format(self.django_path, git_commit_id) logger.info("Executing {0}".format(git_diff_command)) diff_process = Popen(git_diff_command, shell=True, stdout=PIPE, stderr=PIPE) for line in map(clean_bytes_to_str, diff_process.stdout.readlines()): # Only gather lines that include added migrations if ( re.search(r"/{0}/.*\.py".format(MIGRATIONS_MODULE_NAME), line) and "__init__" not in line ): app_label, name = split_migration_path(line) migrations.append(Migration(name, app_label)) diff_process.wait() if diff_process.returncode != 0: output = [] for line in map(clean_bytes_to_str, diff_process.stderr.readlines()): output.append(line) logger.error("Error while git diff command:\n{}".format("".join(output))) raise Exception("Error while executing git diff command") return migrations
def handle(self, app_label, **options): self.verbosity = options.get('verbosity') fixture_name = options.get('fixture_name') self.dry_run = False # Make sure the app they asked for exists try: apps.get_app_config(app_label) except LookupError: self.stderr.write( "App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label) # Load the current graph state. Pass in None for the connection so # the loader doesn't try to resolve replaced migrations from DB. loader = MigrationLoader(None, ignore_no_migrations=True) autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), InteractiveMigrationQuestioner(specified_apps=[app_label]), ) migration = Migration("custom", app_label) migration.operations.append(LoadFixtureMigration(fixture_name)) migration.dependencies += loader.graph.nodes.keys() changes = {app_label: [migration]} changes = autodetector.arrange_for_graph( changes=changes, graph=loader.graph, ) self.write_migration_files(changes) return
def _get_rename_table_migration(self, dependencies=[]): database_operations = [] state_operations = [] database_operations.append( migrations.AlterModelTable( self.model_name.lower(), '{}_{}'.format(self.dest_app, self.model_name.lower()))) migration = Migration('rename_table', self.source_app) migration.dependencies = dependencies migration.operations = [ migrations.SeparateDatabaseAndState( database_operations=database_operations, state_operations=state_operations) ] return migration
def handle(self, *args, **options): ops = [] for model in apps.get_models(include_auto_created=True): if not hasattr(model, 'VersionModel') or model._meta.proxy: continue ops.extend(self.build_operations(model)) if options['initial']: m = Migration('0003_triggers', 'share') m.dependencies = [('share', '0002_create_share_user')] else: ml = MigrationLoader(connection=connection) ml.build_graph() last_share_migration = [ x[1] for x in ml.graph.leaf_nodes() if x[0] == 'share' ][0] next_number = '{0:04d}'.format(int(last_share_migration[0:4]) + 1) m = Migration( '{}_update_trigger_migrations_{}'.format( next_number, datetime.datetime.now().strftime("%Y%m%d_%H%M")), 'share') m.dependencies = [('share', '0002_create_share_user'), ('share', last_share_migration)] m.operations = ops self.write_migration(m)
def plan(): """Fake migrations plan for testing purposes.""" migrations_plan = [ Migration('0001_initial', 'app1'), Migration('0002_second', 'app1'), Migration('0001_initial', 'app2'), Migration('0003_third', 'app1'), Migration('0004_fourth', 'app1'), Migration('0002_second', 'app2'), Migration('0005_fifth', 'app1'), Migration('0001_initial', 'app3'), Migration('0006_sixth', 'app1'), ] return [(migration, False) for migration in migrations_plan]
def _get_create_model_migration(self, dependencies=[]): database_operations = [] state_operations = [] model_state = self.to_state.models[self.dest_app, self.model_name.lower()] model_opts = self.to_state.apps.get_model(self.dest_app, self.model_name)._meta related_fields = {} for field in model_opts.local_fields: if field.remote_field: if field.remote_field.model: if not field.remote_field.parent_link: related_fields[field.name] = field if (getattr(field.remote_field, 'through', None) and not field.remote_field.through._meta.auto_created): related_fields[field.name] = field for field in model_opts.local_many_to_many: if field.remote_field.model: related_fields[field.name] = field if getattr(field.remote_field, 'through', None ) and not field.remote_field.through._meta.auto_created: related_fields[field.name] = field state_operations.append( migrations.CreateModel( name=model_state.name, fields=[ d for d in model_state.fields if d[0] not in related_fields ], options=model_state.options, bases=model_state.bases, managers=model_state.managers, )) migration = Migration('create_model', self.dest_app) migration.dependencies = dependencies migration.operations = [ migrations.SeparateDatabaseAndState( database_operations=database_operations, state_operations=state_operations, ) ] return migration
def handle(self, *args, **options): ops = [] for model in apps.get_models(include_auto_created=True): if not hasattr(model, 'VersionModel') or model._meta.proxy: continue ops.extend(self.build_operations(model)) if options['initial']: m = Migration('0003_triggers', 'share') m.dependencies = [('share', '0002_create_share_user')] else: ml = MigrationLoader(connection=connection) ml.build_graph() last_share_migration = [x[1] for x in ml.graph.leaf_nodes() if x[0] == 'share'][0] next_number = '{0:04d}'.format(int(last_share_migration[0:4]) + 1) m = Migration('{}_update_trigger_migrations_{}'.format(next_number, datetime.datetime.now().strftime("%Y%m%d_%H%M")), 'share') m.dependencies = [('share', '0002_create_share_user'), ('share', last_share_migration)] m.operations = ops self.write_migration(m)
def test_exclude_migration_tests(self): m = Migration("0002_add_new_not_null_field", "app_add_not_null_column") linter = MigrationLinter(exclude_migration_tests=[], database="mysql") linter.lint_migration(m) self.assertTrue(linter.has_errors) linter = MigrationLinter(exclude_migration_tests=["NOT_NULL"], database="mysql") linter.lint_migration(m) self.assertFalse(linter.has_errors)
def _get_model_fk_migrations(self, dependencies=[]): _migrations = [] model_opts = self.to_state.apps.get_model(self.dest_app, self.model_name)._meta for field in model_opts.get_fields(include_hidden=True): if field.is_relation: operations = [] operations.append( migrations.AlterField( model_name=field.related_model._meta.model_name, name=field.remote_field.name, field=field.remote_field, )) migration = Migration('alter_model_fk', field.related_model._meta.app_label) migration.dependencies = dependencies migration.operations = operations _migrations.append(migration) return _migrations
def handle(self, app_label, *models, **options): self.verbosity = int(options.get('verbosity')) self.interactive = options.get('interactive') self.dry_run = options.get('dry_run', False) if not models and '.' in app_label: app_label, models = app_label.split('.', 1) models = [models] try: apps.get_app_config(app_label) except LookupError: self.stderr.write("App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label) sys.exit(2) # We want to basically write an empty migration, but with some # extra bits. # Load the current graph state. Pass in None for the connection so # the loader doesn't try to resolve replaced migrations from DB. loader = MigrationLoader(None) # Set up autodetector autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), InteractiveMigrationQuestioner(specified_apps=[app_label]), ) changes = autodetector.arrange_for_graph({ app_label: [Migration("audit_tables", app_label)] }, loader.graph) migration = changes[app_label][0] migration.dependencies.append( ('audit', '0001_initial') ) migration.name = 'audit_%s' % ('_'.join(models[:3])) for model_name in models: model = apps.get_model(app_label, model_name) migration.operations.append(postgres.audit.operations.AuditModel(model)) self.write_migration_files(changes)
def create_runpython_migration(self, app_label, forwards_backwards, extra_functions, extra_dependencies=None): # Copy source code, so that we can uninstall this helper app # and the migrations still work. extra_func_code = "\n\n".join( inspect.getsource(f) for f in extra_functions) loader = MigrationLoader(None, ignore_no_migrations=True) autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), None, ) changes = {app_label: [Migration("custom", app_label)]} changes = autodetector.arrange_for_graph( changes=changes, graph=loader.graph, ) for app_label, app_migrations in changes.items(): for migration in app_migrations: if extra_dependencies is not None: migration.dependencies.extend(extra_dependencies) writer = MigrationWriter(migration) migration_string = writer.as_string().decode('utf-8') # Add support functions: migration_string = migration_string.replace( "\nclass Migration", forwards_backwards + "\n\n" + extra_func_code + "\n\n" + "\nclass Migration") # Add operations: migration_string = migration_string.replace( "operations = [", "operations = [\n" " migrations.RunPython(forwards, backwards),") with open(writer.path, "wb") as fh: fh.write(migration_string.encode('utf-8'))
def test_backward(self): forward = (Migration("0001_initial", "tests"), True) backward = (Migration("0001_initial", "tests"), False) self.assertNotEqual(hash_plan([forward]), hash_plan([backward]))
def apply(self, project_state, schema_editor, collect_sql=False): from awx.main.signals import disable_activity_stream with disable_activity_stream(): return Migration.apply(self, project_state, schema_editor, collect_sql)
class CacheTestCase(unittest.TestCase): def setUp(self): self.test_project_path = os.path.dirname(settings.BASE_DIR) @mock.patch( "django_migration_linter.MigrationLinter._gather_all_migrations", return_value=[ Migration("0001_create_table", "app_add_not_null_column"), Migration("0002_add_new_not_null_field", "app_add_not_null_column"), ], ) def test_cache_normal(self, *args): linter = MigrationLinter(self.test_project_path) linter.old_cache.clear() linter.old_cache.save() with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() self.assertEqual(2, analyse_sql_statements_mock.call_count) cache = linter.new_cache cache.load() self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"]) self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) self.assertListEqual( cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["errors"], [{ "err_msg": "RENAMING tables", "code": "RENAME_TABLE", "table": None, "column": None, }], ) # Start the Linter again -> should use cache now. linter = MigrationLinter(self.test_project_path) with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() analyse_sql_statements_mock.assert_not_called() self.assertTrue(linter.has_errors) @mock.patch( "django_migration_linter.MigrationLinter._gather_all_migrations", return_value=[ Migration("0001_create_table", "app_add_not_null_column"), Migration("0002_add_new_not_null_field", "app_add_not_null_column"), ], ) def test_cache_different_databases(self, *args): linter = MigrationLinter(self.test_project_path, database="mysql") linter.old_cache.clear() linter.old_cache.save() linter = MigrationLinter(self.test_project_path, database="sqlite") linter.old_cache.clear() linter.old_cache.save() with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() self.assertEqual(2, analyse_sql_statements_mock.call_count) cache = linter.new_cache cache.load() self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"]) self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) self.assertListEqual( cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["errors"], [{ "err_msg": "RENAMING tables", "code": "RENAME_TABLE", "table": None, "column": None, }], ) # Start the Linter again but with different database, should not be the same cache linter = MigrationLinter(self.test_project_path, database="mysql") with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() self.assertEqual(2, analyse_sql_statements_mock.call_count) cache = linter.new_cache cache.load() self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"]) self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) self.assertListEqual( cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["errors"], [{ "err_msg": "NOT NULL constraint on columns", "code": "NOT_NULL", "table": "app_add_not_null_column_a", "column": "new_not_null_field", }], ) self.assertTrue(linter.has_errors) @mock.patch( "django_migration_linter.MigrationLinter._gather_all_migrations", return_value=[ Migration("0001_initial", "app_ignore_migration"), Migration("0002_ignore_migration", "app_ignore_migration"), ], ) def test_cache_ignored(self, *args): linter = MigrationLinter(self.test_project_path, ignore_name_contains="0001") linter.old_cache.clear() linter.old_cache.save() with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() self.assertEqual(1, analyse_sql_statements_mock.call_count) cache = linter.new_cache cache.load() self.assertEqual("IGNORE", cache["0fab48322ba76570da1a3c193abb77b5"]["result"]) # Start the Linter again -> should use cache now. linter = MigrationLinter(self.test_project_path) with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() self.assertEqual(1, analyse_sql_statements_mock.call_count) @mock.patch( "django_migration_linter.MigrationLinter._gather_all_migrations", return_value=[ Migration("0002_add_new_not_null_field", "app_add_not_null_column") ], ) def test_cache_modified(self, *args): linter = MigrationLinter(self.test_project_path) linter.old_cache.clear() linter.old_cache.save() with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() self.assertEqual(1, analyse_sql_statements_mock.call_count) cache = linter.new_cache cache.load() self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) # Get the content of the migration file and mock the open call to append # some content to change the hash migration_path = get_migration_abspath("app_add_not_null_column", "0002_add_new_not_null_field") with open(migration_path, "rb") as f: file_content = f.read() file_content += b"# test comment" linter = MigrationLinter(self.test_project_path) with mock.patch( "django_migration_linter.migration_linter.open", mock.mock_open(read_data=file_content), ): with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() self.assertEqual(1, analyse_sql_statements_mock.call_count) cache = linter.new_cache cache.load() self.assertNotIn("19fd3ea688fc05e2cc2a6e67c0b7aa17", cache) self.assertEqual(1, len(cache)) self.assertEqual("ERR", cache["a25768641a0ad526fad199f97c303784"]["result"]) @mock.patch( "django_migration_linter.MigrationLinter._gather_all_migrations", return_value=[ Migration("0001_create_table", "app_add_not_null_column"), Migration("0002_add_new_not_null_field", "app_add_not_null_column"), ], ) def test_ignore_cached_migration(self, *args): linter = MigrationLinter(self.test_project_path) linter.old_cache.clear() linter.old_cache.save() with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() self.assertEqual(2, analyse_sql_statements_mock.call_count) cache = linter.new_cache cache.load() self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"]) self.assertEqual("ERR", cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["result"]) self.assertListEqual( cache["19fd3ea688fc05e2cc2a6e67c0b7aa17"]["errors"], [{ "err_msg": "RENAMING tables", "code": "RENAME_TABLE", "table": None, "column": None, }], ) # Start the Linter again -> should use cache now but ignore the erroneous linter = MigrationLinter( self.test_project_path, ignore_name_contains="0002_add_new_not_null_field") with mock.patch( "django_migration_linter.migration_linter.analyse_sql_statements", wraps=analyse_sql_statements, ) as analyse_sql_statements_mock: linter.lint_all_migrations() analyse_sql_statements_mock.assert_not_called() self.assertFalse(linter.has_errors) cache = linter.new_cache cache.load() self.assertEqual(1, len(cache)) self.assertEqual("OK", cache["4a3770a405738d457e2d23e17fb1f3aa"]["result"])
def handle(self, *app_labels, **options): self.verbosity = options['verbosity'] self.interactive = options['interactive'] self.dry_run = options['dry_run'] self.merge = options['merge'] self.empty = options['empty'] self.migration_name = options['name'] self.replace_all = options['replace_all'] check_changes = options['check_changes'] # Make sure the app they asked for exists app_labels = set(app_labels) bad_app_labels = set() for app_label in app_labels: try: apps.get_app_config(app_label) except LookupError: bad_app_labels.add(app_label) if bad_app_labels: for app_label in bad_app_labels: if '.' in app_label: self.stderr.write( "'%s' is not a valid app label. Did you mean '%s'?" % ( app_label, app_label.split('.')[-1], )) else: self.stderr.write( "App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label) sys.exit(2) # Load the current graph state. Pass in None for the connection so # the loader doesn't try to resolve replaced migrations from DB. loader = MigrationLoader(None, ignore_no_migrations=True) # Raise an error if any migrations are applied before their dependencies. consistency_check_labels = { config.label for config in apps.get_app_configs() } # Non-default databases are only checked if database routers used. aliases_to_check = connections if settings.DATABASE_ROUTERS else [ DEFAULT_DB_ALIAS ] for alias in sorted(aliases_to_check): connection = connections[alias] if (connection.settings_dict['ENGINE'] != 'django.db.backends.dummy' and any( # At least one model must be migrated to the database. router.allow_migrate(connection.alias, app_label, model_name=model._meta.object_name ) for app_label in consistency_check_labels for model in apps.get_app_config(app_label).get_models())): loader.check_consistent_history(connection) # Before anything else, see if there's conflicting apps and drop out # hard if there are any and they don't want to merge conflicts = loader.detect_conflicts() # If app_labels is specified, filter out conflicting migrations for unspecified apps if app_labels: conflicts = { app_label: conflict for app_label, conflict in conflicts.items() if app_label in app_labels } if conflicts and not self.merge: name_str = "; ".join("%s in %s" % (", ".join(names), app) for app, names in conflicts.items()) raise CommandError( "Conflicting migrations detected; multiple leaf nodes in the " "migration graph: (%s).\nTo fix them run " "'python manage.py makemigrations --merge'" % name_str) # If they want to merge and there's nothing to merge, then politely exit if self.merge and not conflicts: self.stdout.write("No conflicts detected to merge.") return # If they want to merge and there is something to merge, then # divert into the merge code if self.merge and conflicts: return self.handle_merge(loader, conflicts) if self.interactive: questioner = InteractiveMigrationQuestioner( specified_apps=app_labels, dry_run=self.dry_run) else: questioner = NonInteractiveMigrationQuestioner( specified_apps=app_labels, dry_run=self.dry_run) if self.replace_all: replace_list = [ migration for migration in loader.graph.nodes.values() ] temp_nodes = loader.graph.nodes loader.graph.nodes = { k: v for (k, v) in loader.graph.nodes.items() if k[0] not in app_labels } autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) loader.graph.nodes = temp_nodes else: autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) # If they want to make an empty migration, make one for each app if self.empty: if not app_labels: raise CommandError( "You must supply at least one app label when using --empty." ) # Make a fake changes() result we can pass to arrange_for_graph changes = {app: [Migration("custom", app)] for app in app_labels} changes = autodetector.arrange_for_graph( changes=changes, graph=loader.graph, migration_name=self.migration_name, ) self.write_migration_files(changes) return # Detect changes changes = autodetector.changes( graph=loader.graph, trim_to_apps=app_labels or None, convert_apps=app_labels or None, migration_name=self.migration_name, ) if not changes: # No changes? Tell them. if self.verbosity >= 1: if app_labels: if len(app_labels) == 1: self.stdout.write("No changes detected in app '%s'" % app_labels.pop()) else: self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels))) else: self.stdout.write("No changes detected") else: if self.replace_all: for app_label, app_migrations in changes.items(): for app_migration in app_migrations: app_migration.replaces = \ [ (migration.app_label, migration.name) for migration in replace_list if migration.app_label == app_label ] app_migration.dependencies = [ dependency for dependency in app_migration.dependencies if dependency not in app_migration.replaces ] self.write_migration_files(changes) if check_changes: sys.exit(1)
def handle(self, *app_labels, **options): self.verbosity = int(options.get('verbosity')) self.interactive = options.get('interactive') self.dry_run = options.get('dry_run', False) self.merge = options.get('merge', False) self.empty = options.get('empty', False) # Make sure the app they asked for exists app_labels = set(app_labels) bad_app_labels = set() for app_label in app_labels: try: apps.get_app_config(app_label) except LookupError: bad_app_labels.add(app_label) if bad_app_labels: for app_label in bad_app_labels: self.stderr.write( "App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label) sys.exit(2) # Load the current graph state. Pass in None for the connection so # the loader doesn't try to resolve replaced migrations from DB. loader = MigrationLoader(None) # Before anything else, see if there's conflicting apps and drop out # hard if there are any and they don't want to merge conflicts = loader.detect_conflicts() if conflicts and not self.merge: name_str = "; ".join("%s in %s" % (", ".join(names), app) for app, names in conflicts.items()) raise CommandError( "Conflicting migrations detected (%s).\nTo fix them run 'python manage.py makemigrations --merge'" % name_str) # If they want to merge and there's nothing to merge, then politely exit if self.merge and not conflicts: self.stdout.write("No conflicts detected to merge.") return # If they want to merge and there is something to merge, then # divert into the merge code if self.merge and conflicts: return self.handle_merge(loader, conflicts) # Set up autodetector autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), InteractiveMigrationQuestioner(specified_apps=app_labels), ) # If they want to make an empty migration, make one for each app if self.empty: if not app_labels: raise CommandError( "You must supply at least one app label when using --empty." ) # Make a fake changes() result we can pass to arrange_for_graph changes = dict( (app, [Migration("custom", app)]) for app in app_labels) changes = autodetector.arrange_for_graph(changes, loader.graph) self.write_migration_files(changes) return # Detect changes changes = autodetector.changes(graph=loader.graph, trim_to_apps=app_labels or None) # No changes? Tell them. if not changes and self.verbosity >= 1: if len(app_labels) == 1: self.stdout.write("No changes detected in app '%s'" % app_labels.pop()) elif len(app_labels) > 1: self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels))) else: self.stdout.write("No changes detected") return self.write_migration_files(changes)
def setUp(self): self.migration = Migration(app_label="tests", name="migration")
def setUpClass(cls): super().setUpClass() cls.pre_deploy = Migration(app_label="tests", name="0001") cls.pre_deploy.stage = Stage.PRE_DEPLOY cls.post_deploy = Migration(app_label="tests", name="0002") cls.post_deploy.stage = Stage.POST_DEPLOY
def test_stable(self): plan = [(Migration("0001_initial", "tests"), True)] self.assertEqual(hash_plan(plan), "a4a35230c7d1942265f1bc8f9ce53e05a50848be")
def handle(self, *app_labels, **options): self.verbosity = options['verbosity'] self.interactive = options['interactive'] self.dry_run = options['dry_run'] self.merge = options['merge'] self.empty = options['empty'] self.migration_name = options['name'] self.exit_code = options['exit_code'] check_changes = options['check_changes'] if self.exit_code: warnings.warn( "The --exit option is deprecated in favor of the --check option.", RemovedInDjango20Warning ) # Make sure the app they asked for exists app_labels = set(app_labels) bad_app_labels = set() for app_label in app_labels: try: apps.get_app_config(app_label) except LookupError: bad_app_labels.add(app_label) if bad_app_labels: for app_label in bad_app_labels: self.stderr.write("App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label) sys.exit(2) # Load the current graph state. Pass in None for the connection so # the loader doesn't try to resolve replaced migrations from DB. loader = MigrationLoader(None, ignore_no_migrations=True) # Before anything else, see if there's conflicting apps and drop out # hard if there are any and they don't want to merge conflicts = loader.detect_conflicts() # If app_labels is specified, filter out conflicting migrations for unspecified apps if app_labels: conflicts = { app_label: conflict for app_label, conflict in iteritems(conflicts) if app_label in app_labels } if conflicts and not self.merge: name_str = "; ".join( "%s in %s" % (", ".join(names), app) for app, names in conflicts.items() ) raise CommandError( "Conflicting migrations detected; multiple leaf nodes in the " "migration graph: (%s).\nTo fix them run " "'python manage.py makemigrations --merge'" % name_str ) # If they want to merge and there's nothing to merge, then politely exit if self.merge and not conflicts: self.stdout.write("No conflicts detected to merge.") return # If they want to merge and there is something to merge, then # divert into the merge code if self.merge and conflicts: return self.handle_merge(loader, conflicts) if self.interactive: questioner = InteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run) else: questioner = NonInteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run) # Set up autodetector autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) # If they want to make an empty migration, make one for each app if self.empty: if not app_labels: raise CommandError("You must supply at least one app label when using --empty.") # Make a fake changes() result we can pass to arrange_for_graph changes = { app: [Migration("custom", app)] for app in app_labels } changes = autodetector.arrange_for_graph( changes=changes, graph=loader.graph, migration_name=self.migration_name, ) self.write_migration_files(changes) return # Detect changes changes = autodetector.changes( graph=loader.graph, trim_to_apps=app_labels or None, convert_apps=app_labels or None, migration_name=self.migration_name, ) if not changes: # No changes? Tell them. if self.verbosity >= 1: if len(app_labels) == 1: self.stdout.write("No changes detected in app '%s'" % app_labels.pop()) elif len(app_labels) > 1: self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels))) else: self.stdout.write("No changes detected") if self.exit_code: sys.exit(1) else: self.write_migration_files(changes) if check_changes: sys.exit(1)
def test_app_label(self): test_app = (Migration("0001_initial", "tests"), True) other_app = (Migration("0001_initial", "other"), True) self.assertNotEqual(hash_plan([test_app]), hash_plan([other_app]))
def handle(self, *app_labels, **options): self.verbosity = options["verbosity"] self.interactive = options["interactive"] self.dry_run = options["dry_run"] self.merge = options["merge"] self.empty = options["empty"] self.migration_name = options["name"] if self.migration_name and not self.migration_name.isidentifier(): raise CommandError( "The migration name must be a valid Python identifier.") self.include_header = options["include_header"] check_changes = options["check_changes"] # Make sure the app they asked for exists app_labels = set(app_labels) has_bad_labels = False for app_label in app_labels: try: apps.get_app_config(app_label) except LookupError as err: self.stderr.write(str(err)) has_bad_labels = True if has_bad_labels: sys.exit(2) # Load the current graph state. Pass in None for the connection so # the loader doesn't try to resolve replaced migrations from DB. loader = MigrationLoader(None, ignore_no_migrations=True) # Raise an error if any migrations are applied before their dependencies. consistency_check_labels = { config.label for config in apps.get_app_configs() } # Non-default databases are only checked if database routers used. aliases_to_check = (connections if settings.DATABASE_ROUTERS else [DEFAULT_DB_ALIAS]) for alias in sorted(aliases_to_check): connection = connections[alias] if connection.settings_dict[ "ENGINE"] != "django.db.backends.dummy" and any( # At least one model must be migrated to the database. router.allow_migrate(connection.alias, app_label, model_name=model._meta.object_name ) for app_label in consistency_check_labels for model in apps.get_app_config(app_label).get_models()): loader.check_consistent_history(connection) # Before anything else, see if there's conflicting apps and drop out # hard if there are any and they don't want to merge conflicts = loader.detect_conflicts() # If app_labels is specified, filter out conflicting migrations for unspecified apps if app_labels: conflicts = { app_label: conflict for app_label, conflict in conflicts.items() if app_label in app_labels } if conflicts and not self.merge: name_str = "; ".join("%s in %s" % (", ".join(names), app) for app, names in conflicts.items()) raise CommandError( "Conflicting migrations detected; multiple leaf nodes in the " "migration graph: (%s).\nTo fix them run " "'python manage.py makemigrations --merge'" % name_str) # If they want to merge and there's nothing to merge, then politely exit if self.merge and not conflicts: self.stdout.write("No conflicts detected to merge.") return # If they want to merge and there is something to merge, then # divert into the merge code if self.merge and conflicts: return self.handle_merge(loader, conflicts) if self.interactive: questioner = InteractiveMigrationQuestioner( specified_apps=app_labels, dry_run=self.dry_run) else: questioner = NonInteractiveMigrationQuestioner( specified_apps=app_labels, dry_run=self.dry_run) # Set up autodetector autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) # If they want to make an empty migration, make one for each app if self.empty: if not app_labels: raise CommandError( "You must supply at least one app label when using --empty." ) # Make a fake changes() result we can pass to arrange_for_graph changes = {app: [Migration("custom", app)] for app in app_labels} changes = autodetector.arrange_for_graph( changes=changes, graph=loader.graph, migration_name=self.migration_name, ) self.write_migration_files(changes) return # Detect changes changes = autodetector.changes( graph=loader.graph, trim_to_apps=app_labels or None, convert_apps=app_labels or None, migration_name=self.migration_name, ) if not changes: # No changes? Tell them. if self.verbosity >= 1: if app_labels: if len(app_labels) == 1: self.stdout.write("No changes detected in app '%s'" % app_labels.pop()) else: self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels))) else: self.stdout.write("No changes detected") else: self.write_migration_files(changes) if check_changes: sys.exit(1)
def test_migration_name(self): first = (Migration("0001_initial", "tests"), True) second = (Migration("0002_second", "tests"), True) self.assertNotEqual(hash_plan([first]), hash_plan([second]))