def ask_should_keep_migration(): questioner = InteractiveMigrationQuestioner() msg = """\nThe migration linter detected that this migration is not backward compatible. - If you keep the migration, you will want to fix the issue or ignore the migration. - By default, the newly created migration file will be deleted. Do you want to keep the migration? [y/N]""" return questioner._boolean_input(msg, False)
def handle_merge(self, loader, conflicts): """ Handles merging together conflicted migrations interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner() else: questioner = MigrationQuestioner(defaults={"ask_merge": True}) for app_label, migration_names in conflicts.items(): # Grab out the migrations in question, and work out their # common ancestor. merge_migrations = [] for migration_name in migration_names: migration = loader.get_migration(app_label, migration_name) migration.ancestry = loader.graph.forwards_plan((app_label, migration_name)) merge_migrations.append(migration) all_items_equal = lambda seq: all(item == seq[0] for item in seq[1:]) merge_migrations_generations = zip(*[m.ancestry for m in merge_migrations]) common_ancestor_count = sum( 1 for common_ancestor_generation in takewhile(all_items_equal, merge_migrations_generations) ) if not common_ancestor_count: raise ValueError("Could not find common ancestor of %s" % migration_names) # Now work out the operations along each divergent branch for migration in merge_migrations: migration.branch = migration.ancestry[common_ancestor_count:] migrations_ops = ( loader.get_migration(node_app, node_name).operations for node_app, node_name in migration.branch ) migration.merged_operations = sum(migrations_ops, []) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING("Merging %s" % app_label)) for migration in merge_migrations: self.stdout.write(self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: self.stdout.write(" - %s\n" % operation.describe()) if questioner.ask_merge(app_label): # 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) for migration in merge_migrations] try: biggest_number = max([x for x in numbers if x is not None]) except ValueError: biggest_number = 1 subclass = type( "Migration", (Migration,), {"dependencies": [(app_label, migration.name) for migration in merge_migrations]}, ) new_migration = subclass("%04i_merge" % (biggest_number + 1), app_label) writer = MigrationWriter(new_migration) with open(writer.path, "wb") as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write("\nCreated new merge migration %s" % writer.path)
def handle_merge(self, loader, conflicts): """ Handles merging together conflicted migrations interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner() else: questioner = MigrationQuestioner(defaults={'ask_merge': True}) for app_label, migration_names in conflicts.items(): # Grab out the migrations in question, and work out their # common ancestor. merge_migrations = [] for migration_name in migration_names: migration = loader.get_migration(app_label, migration_name) migration.ancestry = loader.graph.forwards_plan((app_label, migration_name)) merge_migrations.append(migration) all_items_equal = lambda seq: all(item == seq[0] for item in seq[1:]) merge_migrations_generations = zip(*[m.ancestry for m in merge_migrations]) common_ancestor_count = sum(1 for common_ancestor_generation in takewhile(all_items_equal, merge_migrations_generations)) if not common_ancestor_count: raise ValueError("Could not find common ancestor of %s" % migration_names) # Now work out the operations along each divergent branch for migration in merge_migrations: migration.branch = migration.ancestry[common_ancestor_count:] migrations_ops = (loader.get_migration(node_app, node_name).operations for node_app, node_name in migration.branch) migration.merged_operations = sum(migrations_ops, []) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING("Merging %s" % app_label)) for migration in merge_migrations: self.stdout.write(self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: self.stdout.write(" - %s\n" % operation.describe()) if questioner.ask_merge(app_label): # 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) for migration in merge_migrations ] try: biggest_number = max([x for x in numbers if x is not None]) except ValueError: biggest_number = 1 subclass = type("Migration", (Migration, ), { "dependencies": [(app_label, migration.name) for migration in merge_migrations], }) new_migration = subclass("%04i_merge" % (biggest_number + 1), app_label) writer = MigrationWriter(new_migration) with open(writer.path, "wb") as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write("\nCreated new merge migration %s" % writer.path)
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 handle(self, *args, **kwargs): changed = set() self.stdout.write("Checking...") for db in settings.DATABASES.keys(): try: executor = MigrationExecutor(connections[db]) except OperationalError: self.stdout.write("Unable to check migrations: cannot connect to database '{}'.\n".format(db)) sys.exit(1) all_apps = apps.app_configs.keys() questioner = InteractiveMigrationQuestioner(specified_apps=all_apps, dry_run=True) autodetector = MigrationAutodetector( executor.loader.project_state(), ProjectState.from_apps(apps), questioner, ) changed.update(autodetector.changes(graph=executor.loader.graph, convert_apps=all_apps).keys()) if changed: self.stdout.write( "Apps with model changes but no corresponding migration file: {!r}\n".format( list(changed) ) ) sys.exit(1) else: self.stdout.write("All migration files present.\n") sys.exit(0)
class QuestionerHelperMethodsTests(SimpleTestCase): def setUp(self): self.prompt = OutputWrapper(StringIO()) self.questioner = InteractiveMigrationQuestioner( prompt_output=self.prompt) @mock.patch('builtins.input', return_value='datetime.timedelta(days=1)') def test_questioner_default_timedelta(self, mock_input): value = self.questioner._ask_default() self.assertEqual(value, datetime.timedelta(days=1)) @mock.patch('builtins.input', return_value='') def test_questioner_default_no_user_entry(self, mock_input): value = self.questioner._ask_default( default='datetime.timedelta(days=1)') self.assertEqual(value, datetime.timedelta(days=1)) @mock.patch('builtins.input', side_effect=['', 'exit']) def test_questioner_no_default_no_user_entry(self, mock_input): with self.assertRaises(SystemExit): self.questioner._ask_default() self.assertIn( "Please enter some code, or 'exit' (without quotes) to exit.", self.prompt.getvalue(), ) @mock.patch('builtins.input', side_effect=['bad code', 'exit']) def test_questioner_no_default_bad_user_entry_code(self, mock_input): with self.assertRaises(SystemExit): self.questioner._ask_default() self.assertIn('Invalid input: ', self.prompt.getvalue()) @mock.patch('builtins.input', side_effect=['', 'n']) def test_questioner_no_default_no_user_entry_boolean(self, mock_input): value = self.questioner._boolean_input('Proceed?') self.assertIs(value, False) @mock.patch('builtins.input', return_value='') def test_questioner_default_no_user_entry_boolean(self, mock_input): value = self.questioner._boolean_input('Proceed?', default=True) self.assertIs(value, True) @mock.patch('builtins.input', side_effect=[10, 'garbage', 1]) def test_questioner_bad_user_choice(self, mock_input): question = 'Make a choice:' value = self.questioner._choice_input(question, choices='abc') expected_msg = (f'{question}\n' f' 1) a\n' f' 2) b\n' f' 3) c\n') self.assertIn(expected_msg, self.prompt.getvalue()) self.assertEqual(value, 1)
class QuestionerHelperMethodsTests(SimpleTestCase): def setUp(self): self.prompt = OutputWrapper(StringIO()) self.questioner = InteractiveMigrationQuestioner(prompt_output=self.prompt) @mock.patch("builtins.input", return_value="datetime.timedelta(days=1)") def test_questioner_default_timedelta(self, mock_input): value = self.questioner._ask_default() self.assertEqual(value, datetime.timedelta(days=1)) @mock.patch("builtins.input", return_value="") def test_questioner_default_no_user_entry(self, mock_input): value = self.questioner._ask_default(default="datetime.timedelta(days=1)") self.assertEqual(value, datetime.timedelta(days=1)) @mock.patch("builtins.input", side_effect=["", "exit"]) def test_questioner_no_default_no_user_entry(self, mock_input): with self.assertRaises(SystemExit): self.questioner._ask_default() self.assertIn( "Please enter some code, or 'exit' (without quotes) to exit.", self.prompt.getvalue(), ) @mock.patch("builtins.input", side_effect=["bad code", "exit"]) def test_questioner_no_default_bad_user_entry_code(self, mock_input): with self.assertRaises(SystemExit): self.questioner._ask_default() self.assertIn("Invalid input: ", self.prompt.getvalue()) @mock.patch("builtins.input", side_effect=["", "n"]) def test_questioner_no_default_no_user_entry_boolean(self, mock_input): value = self.questioner._boolean_input("Proceed?") self.assertIs(value, False) @mock.patch("builtins.input", return_value="") def test_questioner_default_no_user_entry_boolean(self, mock_input): value = self.questioner._boolean_input("Proceed?", default=True) self.assertIs(value, True) @mock.patch("builtins.input", side_effect=[10, "garbage", 1]) def test_questioner_bad_user_choice(self, mock_input): question = "Make a choice:" value = self.questioner._choice_input(question, choices="abc") expected_msg = f"{question}\n" f" 1) a\n" f" 2) b\n" f" 3) c\n" self.assertIn(expected_msg, self.prompt.getvalue()) self.assertEqual(value, 1)
def get_actions_to_perform(self, model, field): actions = [ 'add field with default', 'update existing rows', 'set not null for field', 'drop default', ] # Checking maybe this column already exists # if so asking user what to do next column_info = self.get_column_info(model, field) if column_info is not None: existed_nullable, existed_type, existed_default = column_info questioner = InteractiveMigrationQuestioner() question_template = ( 'It look like column "{}" in table "{}" already exist with following ' 'parameters: TYPE: "{}", DEFAULT: "{}", NULLABLE: "{}".') question = question_template.format( field.name, model._meta.db_table, existed_type, existed_default, existed_nullable, ) choices = ( 'abort migration', 'drop column and run migration from beginning', 'manually choose action to start from', 'show how many rows still need to be updated', 'mark operation as successful and proceed to next operation', 'drop column and run migration from standard SchemaEditor', ) result = questioner._choice_input(question, choices) if result == 1: sys.exit(1) elif result == 2: self.remove_field(model, field) elif result == 3: question = 'Now choose from which action process should continue' result = questioner._choice_input(question, actions) actions = actions[result - 1:] elif result == 4: question = 'Rows in table where column is null: "{}"' need_to_update = self.need_to_update(model=model, field=field) questioner._choice_input(question.format(need_to_update), ('Continue', )) return self.get_actions_to_perform(model, field) elif result == 5: actions = [] elif result == 6: self.remove_field(model, field) super(ZeroDownTimeMixin, self).add_field(model, field) actions = [] return actions
def test_no_migration_left(self): loader = MigrationLoader(None, ignore_no_migrations=True) conflicts = loader.detect_conflicts() app_labels = ['cities_light'] autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), InteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=True), ) changes = autodetector.changes( graph=loader.graph, trim_to_apps=app_labels or None, convert_apps=app_labels or None, ) assert 'cities_light' not in changes
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 get_actions_to_perform(self, model, field): actions = self.ADD_FIELD_WITH_DEFAULT_ACTIONS # Checking maybe this column already exists # if so asking user what to do next column_info = self.get_column_info(model, field) if column_info is not None: existed_nullable, existed_type, existed_default = column_info questioner = InteractiveMigrationQuestioner() question = self.RETRY_QUESTION_TEMPLATE.format( field.name, model._meta.db_table, existed_type, existed_default, existed_nullable, ) result = questioner._choice_input(question, self.RETRY_CHOICES) if result == 1: sys.exit(1) elif result == 2: self.remove_field(model, field) elif result == 3: question = 'Now choose from which action process should continue' result = questioner._choice_input(question, actions) actions = actions[result - 1:] elif result == 4: question = 'Rows in table where column is null: "{}"' need_to_update = self.need_to_update(model=model, field=field) questioner._choice_input(question.format(need_to_update), ('Continue', )) return self.get_actions_to_perform(model, field) elif result == 5: actions = [] elif result == 6: self.remove_field(model, field) super(ZeroDownTimeMixin, self).add_field(model, field) actions = [] return actions
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. Takes a connection, but it's not used # (makemigrations doesn't look at the database state). # Also make sure the graph is built without unmigrated apps shoehorned in. loader = MigrationLoader(connections[DEFAULT_DB_ALIAS], load=False) loader.build_graph(ignore_unmigrated=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 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.graph.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 test_ask_not_null_alteration_not_provided(self, mock): questioner = InteractiveMigrationQuestioner() with captured_stdout(): question = questioner.ask_not_null_alteration('field_name', 'model_name') self.assertEqual(question, NOT_PROVIDED)
def test_timedelta_default(self, mock): questioner = InteractiveMigrationQuestioner() with captured_stdout(): value = questioner._ask_default() self.assertEqual(value, datetime.timedelta(days=1))
def handle_merge(self, loader, conflicts): """ Handles merging together conflicted migrations interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner() else: questioner = MigrationQuestioner(defaults={'ask_merge': True}) for app_label, migration_names in conflicts.items(): # Grab out the migrations in question, and work out their # common ancestor. merge_migrations = [] for migration_name in migration_names: migration = loader.get_migration(app_label, migration_name) migration.ancestry = loader.graph.forwards_plan((app_label, migration_name)) merge_migrations.append(migration) common_ancestor = None for level in zip(*[m.ancestry for m in merge_migrations]): if reduce(operator.eq, level): common_ancestor = level[0] else: break if common_ancestor is None: raise ValueError("Could not find common ancestor of %s" % migration_names) # Now work out the operations along each divergent branch for migration in merge_migrations: migration.branch = migration.ancestry[ (migration.ancestry.index(common_ancestor) + 1): ] migration.merged_operations = [] for node_app, node_name in migration.branch: migration.merged_operations.extend( loader.get_migration(node_app, node_name).operations ) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING("Merging %s" % app_label)) for migration in merge_migrations: self.stdout.write(self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: self.stdout.write(" - %s\n" % operation.describe()) if questioner.ask_merge(app_label): # 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) for migration in merge_migrations ] try: biggest_number = max([x for x in numbers if x is not None]) except ValueError: biggest_number = 1 subclass = type("Migration", (Migration, ), { "dependencies": [(app_label, migration.name) for migration in merge_migrations], }) new_migration = subclass("%04i_merge" % (biggest_number + 1), app_label) writer = MigrationWriter(new_migration) if not self.dry_run: # Write the merge migrations file to the disk with open(writer.path, "wb") as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write("\nCreated new merge migration %s" % writer.path) elif self.verbosity == 3: # Alternatively, makemigrations --merge --dry-run --verbosity 3 # will output the merge migrations to stdout rather than saving # the file to the disk. self.stdout.write(self.style.MIGRATE_HEADING( "Full merge migrations file '%s':" % writer.filename) + "\n" ) self.stdout.write("%s\n" % writer.as_string())
import os
def test_timedelta_default(self, mock): questioner = InteractiveMigrationQuestioner() with captured_stdout(): value = questioner._ask_default() self.assertEqual(value, datetime.timedelta(days=1))
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) # 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. Takes a connection, but it's not used # (makemigrations doesn't look at the database state). loader = MigrationLoader(connections[DEFAULT_DB_ALIAS]) # 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) # Detect changes autodetector = MigrationAutodetector( loader.graph.project_state(), ProjectState.from_apps(apps), InteractiveMigrationQuestioner(specified_apps=app_labels), ) 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 directory_created = {} for app_label, app_migrations in changes.items(): if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label) + "\n") for migration in app_migrations: # Describe the migration writer = MigrationWriter(migration) if self.verbosity >= 1: self.stdout.write(" %s:\n" % (self.style.MIGRATE_LABEL(writer.filename),)) for operation in migration.operations: self.stdout.write(" - %s\n" % operation.describe()) # Write it if not self.dry_run: migrations_directory = os.path.dirname(writer.path) if not directory_created.get(app_label, False): if not os.path.isdir(migrations_directory): os.mkdir(migrations_directory) init_path = os.path.join(migrations_directory, "__init__.py") if not os.path.isfile(init_path): open(init_path, "w").close() # We just do this once per app directory_created[app_label] = True migration_string = writer.as_string() with open(writer.path, "wb") as fh: fh.write(migration_string)
def handle(self, *app_labels, **options): self.verbosity = options['verbosity'] self.interactive = options['interactive'] self.merge = options['merge'] self.change_name = options['name'] self.dry_run = 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 changes from DB. loader = ChangeLoader(None, ignore_no_changes=True) # Raise an error if any changes 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 changes 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 changes detected; multiple leaf nodes in the ' 'changes graph: (%s).\nTo fix them run ' "'python manage.py makechange --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) # To make an empty change, make one for each app 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: [Change('custom', app)] for app in app_labels } autodetector = ChangeAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) changes = autodetector.arrange_for_graph( changes=changes, graph=loader.graph, change_name=self.change_name, ) self.write_changes_files(changes)
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 setUp(self): self.prompt = OutputWrapper(StringIO()) self.questioner = InteractiveMigrationQuestioner( prompt_output=self.prompt)
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 handle_merge(self, loader, conflicts): """ Handles merging together conflicted migrations interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner() else: questioner = MigrationQuestioner(defaults={"ask_merge": True}) for app_label, migration_names in conflicts.items(): # Grab out the migrations in question, and work out their # common ancestor. merge_migrations = [] for migration_name in migration_names: migration = loader.get_migration(app_label, migration_name) migration.ancestry = [ mig for mig in loader.graph.forwards_plan((app_label, migration_name)) if mig[0] == migration.app_label ] merge_migrations.append(migration) def all_items_equal(seq): return all(item == seq[0] for item in seq[1:]) merge_migrations_generations = zip(*(m.ancestry for m in merge_migrations)) common_ancestor_count = sum( 1 for common_ancestor_generation in takewhile( all_items_equal, merge_migrations_generations)) if not common_ancestor_count: raise ValueError("Could not find common ancestor of %s" % migration_names) # Now work out the operations along each divergent branch for migration in merge_migrations: migration.branch = migration.ancestry[common_ancestor_count:] migrations_ops = (loader.get_migration(node_app, node_name).operations for node_app, node_name in migration.branch) migration.merged_operations = sum(migrations_ops, []) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING("Merging %s" % app_label)) for migration in merge_migrations: self.stdout.write( self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: self.stdout.write(" - %s" % operation.describe()) if questioner.ask_merge(app_label): # 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) for migration in merge_migrations ] try: biggest_number = max(x for x in numbers if x is not None) except ValueError: biggest_number = 1 subclass = type( "Migration", (Migration, ), { "dependencies": [(app_label, migration.name) for migration in merge_migrations], }, ) 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, self.include_header) if not self.dry_run: # Write the merge migrations file to the disk with open(writer.path, "w", encoding="utf-8") as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write("\nCreated new merge migration %s" % writer.path) elif self.verbosity == 3: # Alternatively, makemigrations --merge --dry-run --verbosity 3 # will output the merge migrations to stdout rather than saving # the file to the disk. self.stdout.write( self.style.MIGRATE_HEADING( "Full merge migrations file '%s':" % writer.filename)) self.stdout.write(writer.as_string())
def test_ask_not_null_alteration_not_provided(self, mock): questioner = InteractiveMigrationQuestioner( prompt_output=OutputWrapper(StringIO())) question = questioner.ask_not_null_alteration('field_name', 'model_name') self.assertEqual(question, NOT_PROVIDED)
def handle_merge(self, loader, conflicts): """ Handles merging together conflicted migrations interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner() else: questioner = MigrationQuestioner(defaults={'ask_merge': True}) for app_label, migration_names in conflicts.items(): # Grab out the migrations in question, and work out their # common ancestor. merge_migrations = [] for migration_name in migration_names: migration = loader.get_migration(app_label, migration_name) migration.ancestry = [ mig for mig in loader.graph.forwards_plan((app_label, migration_name)) if mig[0] == migration.app_label ] merge_migrations.append(migration) def all_items_equal(seq): return all(item == seq[0] for item in seq[1:]) merge_migrations_generations = zip(*(m.ancestry for m in merge_migrations)) common_ancestor_count = sum(1 for common_ancestor_generation in takewhile(all_items_equal, merge_migrations_generations)) if not common_ancestor_count: raise ValueError("Could not find common ancestor of %s" % migration_names) # Now work out the operations along each divergent branch for migration in merge_migrations: migration.branch = migration.ancestry[common_ancestor_count:] migrations_ops = (loader.get_migration(node_app, node_name).operations for node_app, node_name in migration.branch) migration.merged_operations = sum(migrations_ops, []) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING("Merging %s" % app_label)) for migration in merge_migrations: self.stdout.write(self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: self.stdout.write(" - %s\n" % operation.describe()) if questioner.ask_merge(app_label): # 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) for migration in merge_migrations ] try: biggest_number = max(x for x in numbers if x is not None) except ValueError: biggest_number = 1 subclass = type("Migration", (Migration,), { "dependencies": [(app_label, migration.name) for migration in merge_migrations], }) 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: # Write the merge migrations file to the disk with open(writer.path, "w", encoding='utf-8') as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write("\nCreated new merge migration %s" % writer.path) elif self.verbosity == 3: # Alternatively, makemigrations --merge --dry-run --verbosity 3 # will output the merge migrations to stdout rather than saving # the file to the disk. self.stdout.write(self.style.MIGRATE_HEADING( "Full merge migrations file '%s':" % writer.filename) + "\n" ) self.stdout.write("%s\n" % writer.as_string())
def handle_merge(self, loader, conflicts): """ Handles merging together conflicted changes interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner() else: questioner = MigrationQuestioner(defaults={'ask_merge': True}) for app_label, change_names in conflicts.items(): # Grab out the changes in question, and work out their # common ancestor. merge_changes = [] for change_name in change_names: change = loader.get_change(app_label, change_name) change.ancestry = [ mig for mig in loader.graph.forwards_plan((app_label, change_name)) if mig[0] == change.app_label ] merge_changes.append(change) def all_items_equal(seq): return all(item == seq[0] for item in seq[1:]) merge_changes_generations = zip(*[m.ancestry for m in merge_changes]) common_ancestor_count = sum(1 for common_ancestor_generation in takewhile(all_items_equal, merge_changes_generations)) if not common_ancestor_count: raise ValueError('Could not find common ancestor of %s' % change_names) # Now work out the operations along each divergent branch for change in merge_changes: change.branch = change.ancestry[common_ancestor_count:] changes_ops = (loader.get_change(node_app, node_name).operations for node_app, node_name in change.branch) change.merged_operations = sum(changes_ops, []) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING('Merging %s' % app_label)) for change in merge_changes: self.stdout.write(self.style.MIGRATE_LABEL(' Branch %s' % change.name)) for operation in change.merged_operations: self.stdout.write(' - %s\n' % operation.describe()) if questioner.ask_merge(app_label): # If they still want to merge it, then write out an empty # file depending on the changes needing merging. numbers = [ ChangeAutodetector.parse_number(change.name) for change in merge_changes ] try: biggest_number = max(x for x in numbers if x is not None) except ValueError: biggest_number = 1 subclass = type('Change', (Change, ), { 'dependencies': [(app_label, change.name) for change in merge_changes], }) change_name = '%04i_%s' % ( biggest_number + 1, self.change_name or ('merge_%s' % get_migration_name_timestamp()) ) new_change = subclass(change_name, app_label) writer = ChangeWriter(new_change) if not self.dry_run: # Write the merge changes file to the disk with io.open(writer.path, 'w', encoding='utf-8') as fh: fh.write(writer.as_string()) if self.verbosity > 0: self.stdout.write('\nCreated new merge change %s' % writer.path) elif self.verbosity == 3: # Alternatively, makechanges --merge --dry-run --verbosity 3 # will output the merge changes to stdout rather than saving # the file to the disk. self.stdout.write(self.style.MIGRATE_HEADING( "Full merge changes file '%s':" % writer.filename) + '\n' ) self.stdout.write('%s\n' % writer.as_string())