def test_total_deconstruct(self):
        loader = MigrationLoader(None, load=True, ignore_no_migrations=True)
        loader.disk_migrations = {t: v for t, v in loader.disk_migrations.items() if t[0] != 'testapp'}
        app_labels = {"testapp"}
        questioner = NonInteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=True)
        autodetector = MigrationAutodetector(
            loader.project_state(),
            ProjectState.from_apps(apps),
            questioner,
        )
        # Detect changes
        changes = autodetector.changes(
            graph=loader.graph,
            trim_to_apps=app_labels or None,
            convert_apps=app_labels or None,
            migration_name="my_fake_migration_for_test_deconstruct",
        )
        self.assertGreater(len(changes), 0)
        for app_label, app_migrations in changes.items():
            for migration in app_migrations:
                # Describe the migration
                writer = MigrationWriter(migration)

                migration_string = writer.as_string()
                self.assertNotEqual(migration_string, "")
Beispiel #2
0
    def test_load(self):
        """
        Makes sure the loader can load the migrations for the test apps,
        and then render them out to a new Apps.
        """
        # Load and test the plan
        migration_loader = MigrationLoader(connection)
        self.assertEqual(
            migration_loader.graph.forwards_plan(("migrations", "0002_second")),
            [
                ("migrations", "0001_initial"),
                ("migrations", "0002_second"),
            ],
        )
        # Now render it out!
        project_state = migration_loader.project_state(("migrations", "0002_second"))
        self.assertEqual(len(project_state.models), 2)

        author_state = project_state.models["migrations", "author"]
        self.assertEqual(
            [x for x, y in author_state.fields],
            ["id", "name", "slug", "age", "rating"]
        )

        book_state = project_state.models["migrations", "book"]
        self.assertEqual(
            [x for x, y in book_state.fields],
            ["id", "author"]
        )

        # Ensure we've included unmigrated apps in there too
        self.assertIn("basic", project_state.real_apps)
Beispiel #3
0
    def basedir(self):
        migrations_package_name = MigrationLoader(None, load=False).migrations_module(self.migration.app_label)

        if migrations_package_name is None:
            raise ValueError(
                "Django can't create migrations for app '%s' because "
                "migrations have been disabled via the MIGRATION_MODULES "
                "setting." % self.migration.app_label
            )

        # See if we can import the migrations module directly
        try:
            migrations_module = import_module(migrations_package_name)
        except ImportError:
            pass
        else:
            try:
                return upath(module_dir(migrations_module))
            except ValueError:
                pass

        # Alright, see if it's a direct submodule of the app
        app_config = apps.get_app_config(self.migration.app_label)
        maybe_app_name, _, migrations_package_basename = migrations_package_name.rpartition(".")
        if app_config.name == maybe_app_name:
            return os.path.join(app_config.path, migrations_package_basename)

        # In case of using MIGRATION_MODULES setting and the custom package
        # doesn't exist, create one, starting from an existing package
        existing_dirs, missing_dirs = migrations_package_name.split("."), []
        while existing_dirs:
            missing_dirs.insert(0, existing_dirs.pop(-1))
            try:
                base_module = import_module(".".join(existing_dirs))
            except ImportError:
                continue
            else:
                try:
                    base_dir = upath(module_dir(base_module))
                except ValueError:
                    continue
                else:
                    break
        else:
            raise ValueError(
                "Could not locate an appropriate location to create "
                "migrations package %s. Make sure the toplevel "
                "package exists and can be imported." %
                migrations_package_name)

        final_dir = os.path.join(base_dir, *missing_dirs)
        if not os.path.isdir(final_dir):
            os.makedirs(final_dir)
        for missing_dir in missing_dirs:
            base_dir = os.path.join(base_dir, missing_dir)
            with open(os.path.join(base_dir, "__init__.py"), "w"):
                pass

        return final_dir
Beispiel #4
0
 def test_readonly_database(self):
     """
     check_consistent_history() ignores read-only databases, possibly
     without a django_migrations table.
     """
     with mock.patch.object(MigrationRecorder, 'ensure_schema', side_effect=MigrationSchemaMissing()):
         loader = MigrationLoader(connection=None)
         loader.check_consistent_history(connection)
Beispiel #5
0
 def test_check_consistent_history(self):
     loader = MigrationLoader(connection=None)
     loader.check_consistent_history(connection)
     recorder = MigrationRecorder(connection)
     recorder.record_applied('migrations', '0002_second')
     msg = "Migration migrations.0002_second is applied before its dependency migrations.0001_initial"
     with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
         loader.check_consistent_history(connection)
    def write_migration(self, migration):
        loader = MigrationLoader(None, ignore_no_migrations=True)
        autodetector = MigrationAutodetector(loader.project_state(), ProjectState.from_apps(apps),)
        changes = autodetector.arrange_for_graph(changes={'share': [migration]}, graph=loader.graph,)

        for m in changes['share']:
            writer = MigrationWriter(m)
            with open(writer.path, 'wb') as fp:
                fp.write(writer.as_string())
Beispiel #7
0
    def test_loading_squashed_complex_multi_apps(self):
        loader = MigrationLoader(connection)
        loader.build_graph()

        plan = set(loader.graph.forwards_plan(("app1", "4_auto")))
        expected_plan = set(
            [("app1", "4_auto"), ("app1", "2_squashed_3"), ("app2", "1_squashed_2"), ("app1", "1_auto")]
        )
        self.assertEqual(plan, expected_plan)
 def _get_apps_for_migration(self, migration_states):
     loader = MigrationLoader(connection)
     full_names = []
     for app_name, migration_name in migration_states:
         if migration_name != 'zero':
             migration_name = loader.get_migration_by_prefix(
                 app_name, migration_name).name
             full_names.append((app_name, migration_name))
     state = loader.project_state(full_names)
     return state.apps
 def _get_apps_for_migration(self, migration_states):
     loader = MigrationLoader(connection)
     full_names = []
     for app_name, migration_name in migration_states:
         migration_name = loader.get_migration_by_prefix(app_name, migration_name).name
         full_names.append((app_name, migration_name))
     state = loader.project_state(full_names)
     if django.VERSION < (1, 8):
         state.render()
     return state.apps
Beispiel #10
0
 def test_loading_squashed(self):
     "Tests loading a squashed migration"
     migration_loader = MigrationLoader(connection)
     recorder = MigrationRecorder(connection)
     # Loading with nothing applied should just give us the one node
     self.assertEqual(len([x for x in migration_loader.graph.nodes if x[0] == "migrations"]), 1)
     # However, fake-apply one migration and it should now use the old two
     recorder.record_applied("migrations", "0001_initial")
     migration_loader.build_graph()
     self.assertEqual(len([x for x in migration_loader.graph.nodes if x[0] == "migrations"]), 2)
     recorder.flush()
Beispiel #11
0
    def test_loading_squashed_complex_multi_apps(self):
        loader = MigrationLoader(connection)
        loader.build_graph()

        plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
        expected_plan = {
            ('app1', '1_auto'),
            ('app2', '1_squashed_2'),
            ('app1', '2_squashed_3'),
            ('app1', '4_auto'),
        }
        self.assertEqual(plan, expected_plan)
Beispiel #12
0
 def test_check_consistent_history_squashed(self):
     """
     MigrationLoader.check_consistent_history() should ignore unapplied
     squashed migrations that have all of their `replaces` applied.
     """
     loader = MigrationLoader(connection=None)
     recorder = MigrationRecorder(connection)
     recorder.record_applied('migrations', '0001_initial')
     recorder.record_applied('migrations', '0002_second')
     loader.check_consistent_history(connection)
     recorder.record_applied('migrations', '0003_third')
     loader.check_consistent_history(connection)
Beispiel #13
0
    def test_django_17_migrations(self):

        from django.apps import apps
        from django.db.migrations.loader import MigrationLoader
        from django.db.migrations.autodetector import MigrationAutodetector
        from django.db.migrations.state import ProjectState
        from django.db.migrations.questioner import MigrationQuestioner
        app_labels = set(app.label for app in apps.get_app_configs()
                         if app.name.startswith('wagtail.'))
        for app_label in app_labels:
            apps.get_app_config(app_label.split('.')[-1])
        loader = MigrationLoader(None, ignore_no_migrations=True)

        conflicts = dict(
            (app_label, conflict)
            for app_label, conflict in iteritems(loader.detect_conflicts())
            if app_label in app_labels
        )

        if conflicts:
            name_str = "; ".join("%s in %s" % (", ".join(names), app)
                                 for app, names in conflicts.items())
            self.fail("Conflicting migrations detected (%s)." % name_str)

        autodetector = MigrationAutodetector(
            loader.project_state(),
            ProjectState.from_apps(apps),
            MigrationQuestioner(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,
        )

        if changes:
            apps = ', '.join(apps.get_app_config(label).name
                             for label in changes.keys())
            migrations = '\n'.join((
                '  {migration}\n{changes}'.format(
                    migration=migration,
                    changes='\n'.join('    {0}'.format(operation.describe())
                                      for operation in migration.operations))
                for (_, migrations) in changes.items()
                for migration in migrations))

            self.fail('Model changes with no migrations detected:\n%s' % migrations)
Beispiel #14
0
    def test_load_unmigrated_dependency(self):
        """
        Makes sure the loader can load migrations with a dependency on an unmigrated app.
        """
        # Load and test the plan
        migration_loader = MigrationLoader(connection)
        self.assertEqual(
            migration_loader.graph.forwards_plan(("migrations", "0001_initial")),
            [("contenttypes", "0001_initial"), ("auth", "0001_initial"), ("migrations", "0001_initial")],
        )
        # Now render it out!
        project_state = migration_loader.project_state(("migrations", "0001_initial"))
        self.assertEqual(len([m for a, m in project_state.models if a == "migrations"]), 1)

        book_state = project_state.models["migrations", "book"]
        self.assertEqual([x for x, y in book_state.fields], ["id", "user"])
Beispiel #15
0
    def test_loading_squashed_complex_multi_apps_partially_applied(self):
        loader = MigrationLoader(connection)
        recorder = MigrationRecorder(connection)
        recorder.record_applied('app1', '1_auto')
        recorder.record_applied('app1', '2_auto')
        loader.build_graph()

        plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
        plan = plan - loader.applied_migrations
        expected_plan = {
            ('app2', '1_squashed_2'),
            ('app1', '3_auto'),
            ('app1', '4_auto'),
        }

        self.assertEqual(plan, expected_plan)
Beispiel #16
0
    def path(self):
        migrations_package_name = MigrationLoader.migrations_module(self.migration.app_label)
        # See if we can import the migrations module directly
        try:
            migrations_module = import_module(migrations_package_name)

            # Python 3 fails when the migrations directory does not have a
            # __init__.py file
            if not hasattr(migrations_module, '__file__'):
                raise ImportError

            basedir = os.path.dirname(upath(migrations_module.__file__))
        except ImportError:
            app_config = apps.get_app_config(self.migration.app_label)
            migrations_package_basename = migrations_package_name.split(".")[-1]

            # Alright, see if it's a direct submodule of the app
            if '%s.%s' % (app_config.name, migrations_package_basename) == migrations_package_name:
                basedir = os.path.join(app_config.path, migrations_package_basename)
            else:
                # In case of using MIGRATION_MODULES setting and the custom
                # package doesn't exist, create one.
                package_dirs = migrations_package_name.split(".")
                create_path = os.path.join(upath(sys.path[0]), *package_dirs)
                if not os.path.isdir(create_path):
                    os.makedirs(create_path)
                for i in range(1, len(package_dirs) + 1):
                    init_dir = os.path.join(upath(sys.path[0]), *package_dirs[:i])
                    init_path = os.path.join(init_dir, "__init__.py")
                    if not os.path.isfile(init_path):
                        open(init_path, "w").close()
                return os.path.join(create_path, self.filename)
        return os.path.join(basedir, self.filename)
def get_installed_app_labels_with_migrations():
    """ Get the app labels, because settings.INSTALLED_APPS doesn't necessarily give us the labels. 
        Remove django.contrib.contenttypes because we want it to run before us. 
        Return list of tuples like ('admin', '__first__') 
    """
    from django.apps import apps
    apps_with_migrations = []
    for app in apps.get_app_configs():
        if app.label == 'contenttypes': continue # Ignore the contenttypes app

        migrations_module = MigrationLoader.migrations_module(app.label)
        try:
            module = import_module(migrations_module)
        except ImportError:
            continue

        if not hasattr(module, "__path__"):
            continue

        # Make sure there are python files in the migration folder
        has_files = bool(x for x in os.listdir(module.__path__[0]) if x.endswith(".py"))
        if not has_files:
            continue

        apps_with_migrations.append(app.label)

    return [(x, '__first__') for x in apps_with_migrations]
def get_installed_app_labels_with_migrations():
    """ Get the app labels, because settings.INSTALLED_APPS doesn't necessarily give us the labels.
        Remove django.contrib.contenttypes because we want it to run before us.
        Return list of tuples like ('admin', '__first__')
    """
    from django.apps import apps
    apps_with_migrations = []
    for app in apps.get_app_configs():
        if app.label == 'contenttypes':
            continue  # Ignore the contenttypes app

        migrations_module = MigrationLoader.migrations_module(app.label)
        try:
            # Django 1.11 changed the return value of the migrations_module call to a 2-element
            # tuple.  The actual module is the first entry
            if isinstance(migrations_module, tuple):
                migrations_module = migrations_module[0]

            module = import_module(migrations_module)
        except ImportError:
            continue

        if not hasattr(module, "__path__"):
            continue

        # Make sure there are python files in the migration folder (other than the init file)
        has_files = any(
            x for x in os.listdir(module.__path__[0]) if x.endswith(".py") and x != "__init__.py"
        )
        if not has_files:
            continue

        apps_with_migrations.append(app.label)

    return [(x, '__first__') for x in apps_with_migrations]
    def __iter__(self):
        l = MigrationLoader(self.conn)
        for app, migrations in self.migrations.iteritems():
            yield "-- Application: " + app
            migrations.sort()
            for name in migrations:
                yield "-- Migration: " + name
                state = l.project_state((app, name), at_end=False)
                mig = l.graph.nodes[(app, name)]
                self.commented = False
                if not len(mig.operations):
                    yield "-- Blank migration"
                    yield """INSERT INTO "django_migrations" ("app", "name", "applied") VALUES ('{}', '{}', now());""".format(app, name)
                    yield ""
                    continue

                for op in mig.operations:
                    # reject mutating changes
                    if op.__class__ in DANGEROUS_OPS:
                        yield "-- DANGEROUS OPERATION FOUND. : {}".format(
                            op.__class__.__name__
                        )
                        self.commented = True
                        break
                try:
                    se = self.conn.schema_editor(collect_sql=True)
                    se.deferred_sql = []
                    # Hack!!! do not drop default on column creation
                    se.skip_default = lambda x: True
                    mig.apply(state, se, collect_sql=True)
                    lines = se.collected_sql + se.deferred_sql
                except:
                    yield "-- GOT AN EXCEPTION!"
                else:
                    if not lines:
                        yield "-- NO SQL MIGRATION HERE"
                        self.commented = True
                    else:
                        yield self.format("BEGIN;")
                    for line in lines:
                        yield self.format(line)
                        #yield ""
                    yield self.format("""INSERT INTO "django_migrations" ("app", "name", "applied") VALUES ('{}', '{}', now());""".format(app, name))
                    if lines:
                        yield self.format("COMMIT;")
                yield ""
    def handle(self, *app_labels, **options):
        self.verbosity = int(options.get('verbosity'))
        self.interactive = options.get('interactive')
        self.dry_run = False

        self.loader = MigrationLoader(None, ignore_no_migrations=True)

        for app_label in app_labels:
            self.handle_app(app_label)
    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)
Beispiel #23
0
    def test_loading_squashed_ref_squashed(self):
        "Tests loading a squashed migration with a new migration referencing it"
        r"""
        The sample migrations are structured like this:

        app_1       1 --> 2 ---------------------*--> 3        *--> 4
                     \                          /             /
                      *-------------------*----/--> 2_sq_3 --*
                       \                 /    /
        =============== \ ============= / == / ======================
        app_2            *--> 1_sq_2 --*    /
                          \                /
                           *--> 1 --> 2 --*

        Where 2_sq_3 is a replacing migration for 2 and 3 in app_1,
        as 1_sq_2 is a replacing migration for 1 and 2 in app_2.
        """

        loader = MigrationLoader(connection)
        recorder = MigrationRecorder(connection)
        self.addCleanup(recorder.flush)

        # Load with nothing applied: both migrations squashed.
        loader.build_graph()
        plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
        plan = plan - loader.applied_migrations
        expected_plan = {
            ('app1', '1_auto'),
            ('app2', '1_squashed_2'),
            ('app1', '2_squashed_3'),
            ('app1', '4_auto'),
        }
        self.assertEqual(plan, expected_plan)

        # Fake-apply a few from app1: unsquashes migration in app1.
        recorder.record_applied('app1', '1_auto')
        recorder.record_applied('app1', '2_auto')
        loader.build_graph()
        plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
        plan = plan - loader.applied_migrations
        expected_plan = {
            ('app2', '1_squashed_2'),
            ('app1', '3_auto'),
            ('app1', '4_auto'),
        }
        self.assertEqual(plan, expected_plan)

        # Fake-apply one from app2: unsquashes migration in app2 too.
        recorder.record_applied('app2', '1_auto')
        loader.build_graph()
        plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
        plan = plan - loader.applied_migrations
        expected_plan = {
            ('app2', '2_auto'),
            ('app1', '3_auto'),
            ('app1', '4_auto'),
        }
        self.assertEqual(plan, expected_plan)
    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)
Beispiel #25
0
 def test_name_match(self):
     "Tests prefix name matching"
     migration_loader = MigrationLoader(connection)
     self.assertEqual(migration_loader.get_migration_by_prefix("migrations", "0001").name, "0001_initial")
     with self.assertRaises(AmbiguityError):
         migration_loader.get_migration_by_prefix("migrations", "0")
     with self.assertRaises(KeyError):
         migration_loader.get_migration_by_prefix("migrations", "blarg")
    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
Beispiel #27
0
 def path(self):
     migrations_module_name = MigrationLoader.migrations_module(self.migration.app_label)
     app_module = cache.get_app(self.migration.app_label)
     # See if we can import the migrations module directly
     try:
         migrations_module = import_module(migrations_module_name)
         basedir = os.path.dirname(migrations_module.__file__)
     except ImportError:
         # Alright, see if it's a direct submodule of the app
         oneup = ".".join(migrations_module_name.split(".")[:-1])
         app_oneup = ".".join(app_module.__name__.split(".")[:-1])
         if oneup == app_oneup:
             basedir = os.path.join(os.path.dirname(app_module.__file__), migrations_module_name.split(".")[-1])
         else:
             raise ImportError("Cannot open migrations module %s for app %s" % (migrations_module_name, self.migration.app_label))
     return os.path.join(basedir, self.filename)
Beispiel #28
0
    def path(self):
        migrations_package_name = MigrationLoader.migrations_module(self.migration.app_label)
        # See if we can import the migrations module directly
        try:
            migrations_module = import_module(migrations_package_name)
            basedir = os.path.dirname(migrations_module.__file__)
        except ImportError:
            app_config = apps.get_app_config(self.migration.app_label)
            migrations_package_basename = migrations_package_name.split(".")[-1]

            # Alright, see if it's a direct submodule of the app
            if '%s.%s' % (app_config.name, migrations_package_basename) == migrations_package_name:
                basedir = os.path.join(app_config.path, migrations_package_basename)
            else:
                raise ImportError("Cannot open migrations module %s for app %s" % (migrations_package_name, self.migration.app_label))
        return os.path.join(basedir, self.filename)
    def setUp(self):
        from django.db.migrations.executor import MigrationExecutor
        from django.db.migrations.loader import MigrationLoader
        from django.db import connection

        self.migration_executor = MigrationExecutor(connection)
        self.migration_loader = MigrationLoader(connection)
        self.state_0001 = self.migration_loader.project_state(("altersortedmanytomanyfield_tests", "0001_initial"))
        self.state_0002 = self.migration_loader.project_state(
            ("altersortedmanytomanyfield_tests", "0002_alter_m2m_fields")
        )
        self.state_0001_apps = get_apps_from_state(self.state_0001)
        self.state_0002_apps = get_apps_from_state(self.state_0002)

        # Make sure we are at the latest migration when starting the test.
        with capture_stdout():
            call_command("migrate", "altersortedmanytomanyfield_tests")
    def setUp(self):
        from django.db.migrations.executor import MigrationExecutor
        from django.db.migrations.loader import MigrationLoader
        from django.db import connection

        self.migration_executor = MigrationExecutor(connection)
        self.migration_loader = MigrationLoader(connection)
        self.state_0001 = self.migration_loader.project_state(
            ('altersortedmanytomanyfield_tests', '0001_initial'))
        self.state_0002 = self.migration_loader.project_state(
            ('altersortedmanytomanyfield_tests', '0002_alter_m2m_fields'))
        self.state_0001_apps = self.state_0001.render()
        self.state_0002_apps = self.state_0002.render()

        # Make sure we are at the latest migration when starting the test.
        with capture_stdout():
            call_command('migrate', 'altersortedmanytomanyfield_tests')
class TestAlterSortedManyToManyFieldOperation(TransactionTestCase):
    def setUp(self):
        from django.db.migrations.executor import MigrationExecutor
        from django.db.migrations.loader import MigrationLoader
        from django.db import connection

        self.migration_executor = MigrationExecutor(connection)
        self.migration_loader = MigrationLoader(connection)
        self.state_0001 = self.migration_loader.project_state(
            ('altersortedmanytomanyfield_tests', '0001_initial'))
        self.state_0002 = self.migration_loader.project_state(
            ('altersortedmanytomanyfield_tests', '0002_alter_m2m_fields'))
        self.state_0001_apps = self.state_0001.apps
        self.state_0002_apps = self.state_0002.apps

        # Make sure we are at the latest migration when starting the test.
        with capture_stdout():
            call_command('migrate', 'altersortedmanytomanyfield_tests')

    def test_apply_migrations_backwards(self):
        with capture_stdout():
            call_command('migrate', 'altersortedmanytomanyfield_tests', '0001')

    def test_operation_m2m_to_sorted_m2m(self):
        # Let's start with state after 0001
        with capture_stdout():
            call_command('migrate', 'altersortedmanytomanyfield_tests', '0001')

        Target = self.state_0001_apps.get_model(
            'altersortedmanytomanyfield_tests', 'target')
        M2MToSortedM2M = self.state_0001_apps.get_model(
            'altersortedmanytomanyfield_tests', 'm2mtosortedm2m')

        t1 = Target.objects.create(pk=1)
        t2 = Target.objects.create(pk=2)
        t3 = Target.objects.create(pk=3)

        field = M2MToSortedM2M._meta.get_field('m2m')
        through_model = field.remote_field.through
        # No ordering is in place.
        self.assertTrue(not through_model._meta.ordering)

        instance = M2MToSortedM2M.objects.create(pk=1)
        instance.m2m.add(t3)
        instance.m2m.add(t1)
        instance.m2m.add(t2)

        # We cannot assume any particular order now.

        # Migrate to state 0002, then we should be able to apply order.
        with capture_stdout():
            call_command('migrate', 'altersortedmanytomanyfield_tests', '0002')

        Target = self.state_0002_apps.get_model(
            'altersortedmanytomanyfield_tests', 'target')
        M2MToSortedM2M = self.state_0002_apps.get_model(
            'altersortedmanytomanyfield_tests', 'm2mtosortedm2m')

        t1 = Target.objects.get(pk=1)
        t2 = Target.objects.get(pk=2)
        t3 = Target.objects.get(pk=3)

        field = M2MToSortedM2M._meta.get_field('m2m')
        through_model = field.remote_field.through
        # Now, ordering is there.
        self.assertTrue(list(through_model._meta.ordering), ['sort_value'])

        instance = M2MToSortedM2M.objects.get(pk=1)
        self.assertEqual(list(instance.m2m.order_by('pk')), [t1, t2, t3])

        m2m_set(instance, "m2m", [t3, t1, t2])

        self.assertEqual(list(instance.m2m.all()), [t3, t1, t2])

        instance.m2m.remove(t1)
        instance.m2m.remove(t2)
        instance.m2m.add(t2)
        instance.m2m.add(t1)

        self.assertEqual(list(instance.m2m.all()), [t3, t2, t1])
Beispiel #32
0
    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,
                                           dry_run=self.dry_run),
        )

        # 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)
Beispiel #33
0
 def test_load_empty_dir(self):
     with override_settings(
             MIGRATION_MODULES={
                 "migrations": "migrations.faulty_migrations.namespace"
             }):
         MigrationLoader(connection)
Beispiel #34
0
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, ZULIP_PATH)
from scripts.lib.setup_path import setup_path
from scripts.lib.zulip_tools import DEPLOYMENTS_DIR, assert_not_running_as_root, parse_version_from
from version import ZULIP_VERSION as new_version

assert_not_running_as_root()
setup_path()
os.environ["DJANGO_SETTINGS_MODULE"] = "zproject.settings"

import django
from django.db import connection
from django.db.migrations.loader import MigrationLoader

django.setup()
loader = MigrationLoader(connection)
missing = set(loader.applied_migrations)
for key, migration in loader.disk_migrations.items():
    missing.discard(key)
    missing.difference_update(migration.replaces)
if not missing:
    sys.exit(0)

current_version = parse_version_from(os.path.join(DEPLOYMENTS_DIR, "current"))
logging.error(
    "This is not an upgrade -- the current deployment (version %s) "
    "contains %s database migrations which %s (version %s) does not.",
    current_version,
    len(missing),
    ZULIP_PATH,
    new_version,
Beispiel #35
0
def generate_changesets_text(connection,
                             app_names=None,
                             author=None,
                             fake=False,
                             skip_errors=False,
                             indent=''):

    author = author or os.getlogin()

    loader = MigrationLoader(connection)
    graph = loader.graph

    if app_names:
        validate_app_names(loader, app_names)

        targets = [key for key in graph.leaf_nodes() if key[0] in app_names]
    else:
        targets = graph.leaf_nodes()
    plan = []
    seen = set()

    # Generate the plan
    for target in targets:
        for migration in graph.forwards_plan(target):
            if migration not in seen:
                node = graph.node_map[migration]
                plan.append(node)
                seen.add(migration)

    to_generate = []

    for node in plan:
        if node.key not in loader.applied_migrations:
            to_generate.append(node)

    executor = MigrationExecutor(connection)

    def cdata_lines(lines, indentation_level=2):
        indent_str = indentation_level * indent
        separator = u'\n%s' % indent_str
        if len(lines) > 1:
            return u'\n%s%s\n' % (indent_str, separator.join(lines))
        else:
            return lines[0]

    outputs = []

    changesett = Template(
        '$indent<changeSet author="$author" id="$id">$body$indent</changeSet>')
    cdatat = Template('<![CDATA[$body]]>')
    sqlt = Template('$indent$indent<sql>$body</sql>')
    rollbackt = Template(
        '$indent$indent<rollback>\n$indent$body\n$indent$indent</rollback>')

    forwardt = Template('insert into django_migrations (app, name, applied) '
                        'values (\'$app_label\', \'$migration_name\', now())')
    backwardt = Template('delete from django_migrations where '
                         'app=\'$app_label\' and name=\'$migration_name\'')

    for app_label, name in to_generate:
        migration = executor.loader.get_migration_by_prefix(app_label, name)
        targets = [(app_label, migration.name)]
        forward_plan = [(executor.loader.graph.nodes[targets[0]], False)]
        backward_plan = [(executor.loader.graph.nodes[targets[0]], True)]

        try:
            sql_forward = executor.collect_sql(forward_plan)
        except Exception as ex:
            if skip_errors:
                sql_forward = ['-- skipped due to exception: %s' % ex]
            else:
                raise

        try:
            sql_backward = executor.collect_sql(backward_plan)
        except Exception as ex:
            if skip_errors:
                sql_backward = ['-- skipped due to exception: %s' % ex]
            else:
                raise

        changeset_id = u'django-%s-%s' % (app_label, migration.name)

        if fake:
            changeset_id += '-faked'

        ctx = {
            'indent': indent,
            'id': changeset_id,
            'author': author,
            'app_label': app_label,
            'migration_name': migration.name,
        }

        def rendertpl(tpl, **extra):
            tplctx = dict(ctx)
            tplctx.update(extra)
            return tpl.substitute(tplctx)

        changeset_parts = []

        if not fake:
            changeset_parts.append(u'\n' + rendertpl(
                sqlt,
                body=rendertpl(
                    cdatat,
                    body=cdata_lines(sql_forward, indentation_level=2) +
                    indent * 2)))

        changeset_parts.append(rendertpl(sqlt, body=rendertpl(forwardt)))

        rollback_parts = []

        if not fake:
            rollback_parts.append(
                rendertpl(
                    sqlt,
                    body=rendertpl(
                        cdatat,
                        body=cdata_lines(sql_backward, indentation_level=3) +
                        indent * 3)))

        rollback_parts.append(indent +
                              rendertpl(sqlt, body=rendertpl(backwardt)))

        sep = u'\n'
        changeset_parts.append(
            rendertpl(rollbackt, body=sep.join(rollback_parts)))
        outputs.append(
            rendertpl(changesett, body=sep.join(changeset_parts) + u'\n'))
    return u'\n'.join(outputs)
Beispiel #36
0
class Command(BaseCommand):
    def handle(self, *apps, **options):
        self._censor_cache = {}
        self._censor_enabled = bool(options['censor'])
        if self._censor_enabled:
            random.seed(options['random_seed'])
        self.graph = MigrationLoader(None).graph
        comment = options['comment']
        self.picture = Digraph(comment=comment)
        save_loc = options['filename']
        self._render(save_loc)

    def add_arguments(self, parser):
        parser.add_argument('--comment',
                            help='optional comments/captions for the picture')
        parser.add_argument('--censor',
                            action='store_true',
                            help='censor node names (e.g. for publishing)')
        parser.add_argument('--random-seed',
                            default=0,
                            type=int,
                            help='random seed (default: %(default)s)')
        parser.add_argument('filename',
                            nargs='?',
                            help='a filename to write GraphViz contents to')

    def _create_digraph(self):
        nodes = sorted(self.graph.nodes.values(), key=self._get_tuple)
        for node in nodes:
            self._add_node(node)
        for node in nodes:
            self._add_dependencies(node)

    @staticmethod
    def _censor(text):
        res = []
        for word in text.split('_'):
            if word not in ('auto', 'initial', 'squashed'):
                chars = []
                for c in word:
                    if c not in '0123456789_':
                        c = chr(random.randint(ord('a'), ord('z')))
                    chars.append(c)
                word = ''.join(chars)
            res.append(word)
        return '_'.join(res)

    def _censor_using_cache(self, text):
        try:
            return self._censor_cache[text]
        except KeyError:
            while True:
                censored = self._censor(text)
                if censored not in self._censor_cache.values():
                    break
            self._censor_cache[text] = censored
            return censored

    def _style_label(self, tupled_node):
        if self._censor_enabled:
            tupled_node = [self._censor_using_cache(e) for e in tupled_node]

        return '/'.join(tupled_node)

    @staticmethod
    def _get_tuple(node):
        return (node.app_label, node.name)

    def _add_node(self, node):
        node_label = self._style_label(self._get_tuple(node))
        self.picture.node(node_label, node_label)

    def _add_edges(self, nodeTo, nodeFrom):
        self.picture.edge(self._style_label(nodeFrom),
                          self._style_label(nodeTo))

    def _add_dependencies(self, node):
        for dep in node.dependencies:
            if dep[1] == '__first__':
                self._add_edges(
                    self.graph.root_nodes(dep[0])[0], self._get_tuple(node))
            elif dep[1] == '__latest__':
                self._add_edges(
                    self.graph.leaf_nodes(dep[0])[0], self._get_tuple(node))
            else:
                self._add_edges(dep, self._get_tuple(node))

    def _render(self, save_loc):
        self._create_digraph()
        if save_loc:
            self.picture.render(save_loc, view=False)
        else:
            with NamedTemporaryFile() as temp:
                self.picture.render(temp.name, view=True)
    def handle(self, **options):
        self.verbosity = options['verbosity']
        self.interactive = options['interactive']
        self.dry_run = options['dry_run']
        app_label = options['app_label']
        squashed_migration_name = options['squashed_migration_name']

        # Load the current graph state, check the app and migration they asked for exists
        loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
        if app_label not in loader.migrated_apps:
            raise CommandError(
                "App '%s' does not have migrations (so delete_squashed_migrations on "
                "it makes no sense)" % app_label)

        squashed_migration = None
        if squashed_migration_name:
            squashed_migration = self.find_migration(loader, app_label,
                                                     squashed_migration_name)
            if not squashed_migration.replaces:
                raise CommandError(
                    "The migration %s %s is not a squashed migration." %
                    (squashed_migration.app_label, squashed_migration.name))
        else:
            leaf_nodes = loader.graph.leaf_nodes(app=app_label)
            migration = loader.get_migration(*leaf_nodes[0])
            previous_migrations = [
                loader.get_migration(al, mn)
                for al, mn in loader.graph.forwards_plan((migration.app_label,
                                                          migration.name))
                if al == migration.app_label
            ]
            migrations = previous_migrations + [migration]
            for migration in migrations:
                if migration.replaces:
                    squashed_migration = migration
                    break

            if not squashed_migration:
                raise CommandError(
                    "Cannot find a squashed migration in app '%s'." %
                    (app_label))

        files_to_delete = []
        for al, mn in squashed_migration.replaces:
            try:
                migration = loader.disk_migrations[al, mn]
            except KeyError:
                if self.verbosity > 0:
                    self.stderr.write(
                        "Couldn't find migration file for %s %s\n" % (al, mn))
            else:
                pyc_file = inspect.getfile(migration.__class__)
                files_to_delete.append(pyc_file)
                if pyc_file.endswith(PYC):
                    py_file = py_from_pyc(pyc_file)
                    files_to_delete.append(py_file)

        # Tell them what we're doing and optionally ask if we should proceed
        if self.verbosity > 0 or self.interactive:
            self.stdout.write(
                self.style.MIGRATE_HEADING("Will delete the following files:"))
            for fn in files_to_delete:
                self.stdout.write(" - %s" % fn)

            if not self.confirm():
                return

        for fn in files_to_delete:
            try:
                if not self.dry_run:
                    os.remove(fn)
            except OSError:
                if self.verbosity > 0:
                    self.stderr.write("Couldn't delete %s\n" % (fn, ))

        # Try and delete replaces only if it's all on one line
        squashed_migration_fn = inspect.getfile(squashed_migration.__class__)
        if squashed_migration_fn.endswith(PYC):
            squashed_migration_fn = py_from_pyc(squashed_migration_fn)
        with open(squashed_migration_fn) as fp:
            squashed_migration_lines = list(fp)

        delete_lines = []
        for i, line in enumerate(squashed_migration_lines):
            if REPLACES_REGEX.match(line):
                delete_lines.append(i)
                if i > 0 and squashed_migration_lines[i - 1].strip() == '':
                    delete_lines.insert(0, i - 1)
                break
        if not delete_lines:
            raise CommandError(("Couldn't find 'replaces =' line in file %s. "
                                "Please finish cleaning up manually.") %
                               (squashed_migration_fn, ))

        if self.verbosity > 0 or self.interactive:
            self.stdout.write(
                self.style.MIGRATE_HEADING(
                    "Will delete line %s%s from file %s" %
                    (delete_lines[0], ' and ' + str(delete_lines[1])
                     if len(delete_lines) > 1 else "", squashed_migration_fn)))

            if not self.confirm():
                return

        for line_num in sorted(delete_lines, reverse=True):
            del squashed_migration_lines[line_num]

        with open(squashed_migration_fn, 'w') as fp:
            if not self.dry_run:
                fp.write("".join(squashed_migration_lines))
Beispiel #38
0
    def handle(self, **options):

        self.verbosity = options['verbosity']
        self.interactive = options['interactive']
        app_label = options['app_label']
        start_migration_name = options['start_migration_name']
        migration_name = options['migration_name']
        no_optimize = options['no_optimize']
        squashed_name = options['squashed_name']
        include_header = options['include_header']
        # Validate app_label.
        try:
            apps.get_app_config(app_label)
        except LookupError as err:
            raise CommandError(str(err))
        # Load the current graph state, check the app and migration they asked for exists
        loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
        if app_label not in loader.migrated_apps:
            raise CommandError(
                "App '%s' does not have migrations (so squashmigrations on "
                "it makes no sense)" % app_label
            )

        migration = self.find_migration(loader, app_label, migration_name)

        # Work out the list of predecessor migrations
        migrations_to_squash = [
            loader.get_migration(al, mn)
            for al, mn in loader.graph.forwards_plan((migration.app_label, migration.name))
            if al == migration.app_label
        ]

        if start_migration_name:
            start_migration = self.find_migration(loader, app_label, start_migration_name)
            start = loader.get_migration(start_migration.app_label, start_migration.name)
            try:
                start_index = migrations_to_squash.index(start)
                migrations_to_squash = migrations_to_squash[start_index:]
            except ValueError:
                raise CommandError(
                    "The migration '%s' cannot be found. Maybe it comes after "
                    "the migration '%s'?\n"
                    "Have a look at:\n"
                    "  python manage.py showmigrations %s\n"
                    "to debug this issue." % (start_migration, migration, app_label)
                )

        # Tell them what we're doing and optionally ask if we should proceed
        if self.verbosity > 0 or self.interactive:
            self.stdout.write(self.style.MIGRATE_HEADING("Will squash the following migrations:"))
            for migration in migrations_to_squash:
                self.stdout.write(" - %s" % migration.name)

            if self.interactive:
                answer = None
                while not answer or answer not in "yn":
                    answer = input("Do you wish to proceed? [yN] ")
                    if not answer:
                        answer = "n"
                        break
                    else:
                        answer = answer[0].lower()
                if answer != "y":
                    return

        # Load the operations from all those migrations and concat together,
        # along with collecting external dependencies and detecting
        # double-squashing
        operations = []
        dependencies = set()
        # We need to take all dependencies from the first migration in the list
        # as it may be 0002 depending on 0001
        first_migration = True
        for smigration in migrations_to_squash:
            if smigration.replaces:
                raise CommandError(
                    "You cannot squash squashed migrations! Please transition "
                    "it to a normal migration first: "
                    "https://docs.djangoproject.com/en/%s/topics/migrations/#squashing-migrations" % get_docs_version()
                )
            operations.extend(smigration.operations)
            for dependency in smigration.dependencies:
                if isinstance(dependency, SwappableTuple):
                    if settings.AUTH_USER_MODEL == dependency.setting:
                        dependencies.add(("__setting__", "AUTH_USER_MODEL"))
                    else:
                        dependencies.add(dependency)
                elif dependency[0] != smigration.app_label or first_migration:
                    dependencies.add(dependency)
            first_migration = False

        if no_optimize:
            if self.verbosity > 0:
                self.stdout.write(self.style.MIGRATE_HEADING("(Skipping optimization.)"))
            new_operations = operations
        else:
            if self.verbosity > 0:
                self.stdout.write(self.style.MIGRATE_HEADING("Optimizing..."))

            optimizer = MigrationOptimizer()
            new_operations = optimizer.optimize(operations, migration.app_label)

            if self.verbosity > 0:
                if len(new_operations) == len(operations):
                    self.stdout.write("  No optimizations possible.")
                else:
                    self.stdout.write(
                        "  Optimized from %s operations to %s operations." %
                        (len(operations), len(new_operations))
                    )

        # Work out the value of replaces (any squashed ones we're re-squashing)
        # need to feed their replaces into ours
        replaces = []
        for migration in migrations_to_squash:
            if migration.replaces:
                replaces.extend(migration.replaces)
            else:
                replaces.append((migration.app_label, migration.name))

        # Make a new migration with those operations
        subclass = type("Migration", (migrations.Migration,), {
            "dependencies": dependencies,
            "operations": new_operations,
            "replaces": replaces,
        })
        if start_migration_name:
            if squashed_name:
                # Use the name from --squashed-name.
                prefix, _ = start_migration.name.split('_', 1)
                name = '%s_%s' % (prefix, squashed_name)
            else:
                # Generate a name.
                name = '%s_squashed_%s' % (start_migration.name, migration.name)
            new_migration = subclass(name, app_label)
        else:
            name = '0001_%s' % (squashed_name or 'squashed_%s' % migration.name)
            new_migration = subclass(name, app_label)
            new_migration.initial = True

        # Write out the new migration file
        writer = MigrationWriter(new_migration, include_header)
        with open(writer.path, "w", encoding='utf-8') as fh:
            fh.write(writer.as_string())

        if self.verbosity > 0:
            self.stdout.write(self.style.MIGRATE_HEADING("Created new squashed migration %s" % writer.path))
            self.stdout.write("  You should commit this migration but leave the old ones in place;")
            self.stdout.write("  the new migration will be used for new installs. Once you are sure")
            self.stdout.write("  all instances of the codebase have applied the migrations you squashed,")
            self.stdout.write("  you can delete them.")
            if writer.needs_manual_porting:
                self.stdout.write(self.style.MIGRATE_HEADING("Manual porting required"))
                self.stdout.write("  Your migrations contained functions that must be manually copied over,")
                self.stdout.write("  as we could not safely copy their implementation.")
                self.stdout.write("  See the comment at the top of the squashed migration for details.")
Beispiel #39
0
    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)
Beispiel #40
0
    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 crud_project 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 crud_project
        if self.empty:
            if not app_labels:
                raise CommandError("You must supply at least one crud_project 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 crud_project '%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)
Beispiel #41
0
    def test_loading_squashed_complex(self):
        "Tests loading a complex set of squashed migrations"

        loader = MigrationLoader(connection)
        recorder = MigrationRecorder(connection)
        self.addCleanup(recorder.flush)

        def num_nodes():
            plan = set(loader.graph.forwards_plan(('migrations', '7_auto')))
            return len(plan - loader.applied_migrations.keys())

        # Empty database: use squashed migration
        loader.build_graph()
        self.assertEqual(num_nodes(), 5)

        # Starting at 1 or 2 should use the squashed migration too
        self.record_applied(recorder, 'migrations', '1_auto')
        loader.build_graph()
        self.assertEqual(num_nodes(), 4)

        self.record_applied(recorder, 'migrations', '2_auto')
        loader.build_graph()
        self.assertEqual(num_nodes(), 3)

        # However, starting at 3 to 5 cannot use the squashed migration
        self.record_applied(recorder, 'migrations', '3_auto')
        loader.build_graph()
        self.assertEqual(num_nodes(), 4)

        self.record_applied(recorder, 'migrations', '4_auto')
        loader.build_graph()
        self.assertEqual(num_nodes(), 3)

        # Starting at 5 to 7 we are past the squashed migrations.
        self.record_applied(recorder, 'migrations', '5_auto')
        loader.build_graph()
        self.assertEqual(num_nodes(), 2)

        self.record_applied(recorder, 'migrations', '6_auto')
        loader.build_graph()
        self.assertEqual(num_nodes(), 1)

        self.record_applied(recorder, 'migrations', '7_auto')
        loader.build_graph()
        self.assertEqual(num_nodes(), 0)
Beispiel #42
0
 def test_load_import_error(self):
     with override_settings(
             MIGRATION_MODULES={"migrations": "import_error_package"}):
         with self.assertRaises(ImportError):
             MigrationLoader(connection)
Beispiel #43
0
    def handle(self, *app_labels, **options):
        self.verbosity = options['verbosity']
        self.interactive = options['interactive']
        self.dry_run = options['dry_run']
        self.empty = options['empty']
        self.migration_name = options['name']

        # 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)

        # 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)

        questioner = NonInteractiveMigrationQuestioner(
            specified_apps=app_labels, dry_run=self.dry_run)
        ps = ProjectState.from_apps(apps)
        autodetector = MigrationAutodetector(
            ps,
            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,
        )

        self.write_migration_files(changes)
Beispiel #44
0
    def test_loading_squashed_complex(self):
        "Tests loading a complex set of squashed migrations"

        loader = MigrationLoader(connection)
        recorder = MigrationRecorder(connection)

        def num_nodes():
            plan = set(loader.graph.forwards_plan(('migrations', '7_auto')))
            return len(plan - loader.applied_migrations)

        # Empty database: use squashed migration
        loader.build_graph()
        self.assertEqual(num_nodes(), 5)

        # Starting at 1 or 2 should use the squashed migration too
        recorder.record_applied("migrations", "1_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 4)

        recorder.record_applied("migrations", "2_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 3)

        # However, starting at 3 to 5 cannot use the squashed migration
        recorder.record_applied("migrations", "3_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 4)

        recorder.record_applied("migrations", "4_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 3)

        # Starting at 5 to 7 we are passed the squashed migrations
        recorder.record_applied("migrations", "5_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 2)

        recorder.record_applied("migrations", "6_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 1)

        recorder.record_applied("migrations", "7_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 0)

        recorder.flush()
Beispiel #45
0
def check_for_migrations(app_config, connection):
    # Inner import, else tests imports it too early as it needs settings
    from django.db.migrations.loader import MigrationLoader
    loader = MigrationLoader(connection)
    if app_config.label in loader.migrated_apps:
        raise CommandError("App '%s' has migrations. Only the sqlmigrate and sqlflush commands can be used when an app has migrations." % app_config.label)
Beispiel #46
0
    def basedir(self):
        migrations_package_name, _ = MigrationLoader.migrations_module(
            self.migration.app_label)

        if migrations_package_name is None:
            raise ValueError(
                "Django can't create migrations for app '%s' because "
                "migrations have been disabled via the MIGRATION_MODULES "
                "setting." % self.migration.app_label)

        # See if we can import the migrations module directly
        try:
            migrations_module = import_module(migrations_package_name)
        except ImportError:
            pass
        else:
            try:
                return module_dir(migrations_module)
            except ValueError:
                pass

        # Alright, see if it's a direct submodule of the app
        app_config = apps.get_app_config(self.migration.app_label)
        (
            maybe_app_name,
            _,
            migrations_package_basename,
        ) = migrations_package_name.rpartition(".")
        if app_config.name == maybe_app_name:
            return os.path.join(app_config.path, migrations_package_basename)

        # In case of using MIGRATION_MODULES setting and the custom package
        # doesn't exist, create one, starting from an existing package
        existing_dirs, missing_dirs = migrations_package_name.split("."), []
        while existing_dirs:
            missing_dirs.insert(0, existing_dirs.pop(-1))
            try:
                base_module = import_module(".".join(existing_dirs))
            except (ImportError, ValueError):
                continue
            else:
                try:
                    base_dir = module_dir(base_module)
                except ValueError:
                    continue
                else:
                    break
        else:
            raise ValueError(
                "Could not locate an appropriate location to create "
                "migrations package %s. Make sure the toplevel "
                "package exists and can be imported." %
                migrations_package_name)

        final_dir = os.path.join(base_dir, *missing_dirs)
        os.makedirs(final_dir, exist_ok=True)
        for missing_dir in missing_dirs:
            base_dir = os.path.join(base_dir, missing_dir)
            with open(os.path.join(base_dir, "__init__.py"), "w"):
                pass

        return final_dir
Beispiel #47
0
def get_loader():
    """Thin wrapper to lazily get a Migration Loader, only when it's needed
    and only once"""
    return MigrationLoader(connection)
def loader():
    return MigrationLoader(None, ignore_no_migrations=True)
Beispiel #49
0
    def handle(self, *args, **options):
        self.model_name = options['model_name']
        self.source_app = options['source_app']
        self.dest_app = options['dest_app']

        # make sure the apps exist
        app_labels = set([self.source_app, self.dest_app])
        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(
                    self.style.ERROR(
                        "App '{}' could not be found. Is it in INSTALLED_APPS?"
                        .format(app_label)))
            sys.exit(2)

        if len(app_labels) == 1:
            self.stderr.write(
                self.style.ERROR(
                    "Cannot move '{}' within the same app '{}'.".format(
                        self.model_name, self.dest_app)))
            sys.exit(2)

        # load the current graph
        loader = MigrationLoader(None, ignore_no_migrations=True)

        questioner = NonInteractiveMigrationQuestioner()

        self.from_state = loader.project_state()
        self.to_state = ProjectState.from_apps(apps)

        autodetector = MigrationAutodetector(
            self.from_state,
            self.to_state,
            questioner,
        )

        _migrations = []
        rename_table = self._get_rename_table_migration()
        _migrations.append(rename_table)
        create_model = self._get_create_model_migration([
            (rename_table.app_label, rename_table.name),
        ])
        _migrations.append(create_model)
        model_fk = self._get_model_fk_migrations([
            (create_model.app_label, create_model.name),
        ])
        delete_model_deps = [
            (rename_table.app_label, rename_table.name),
            (create_model.app_label, create_model.name),
        ]
        for fk_migration in model_fk:
            _migrations.append(fk_migration)
            delete_model_deps.append(
                (fk_migration.app_label, fk_migration.name), )
        delete_model = self._get_delete_model_migration(delete_model_deps)
        _migrations.append(delete_model)

        changes = {}
        for migration in _migrations:
            changes.setdefault(migration.app_label, []).append(migration)
        changes = autodetector.arrange_for_graph(
            changes=changes,
            graph=loader.graph,
        )
        self.write_migration_files(changes)

        self.stdout.write(self.style.SUCCESS("Done!"))
Beispiel #50
0
    def handle(self, *args, **options):
        verbosity = options["verbosity"]
        app_label = options["app_label"]
        migration_name = options["migration_name"]
        check = options["check"]

        # Validate app_label.
        try:
            apps.get_app_config(app_label)
        except LookupError as err:
            raise CommandError(str(err))

        # Load the current graph state.
        loader = MigrationLoader(None)
        if app_label not in loader.migrated_apps:
            raise CommandError(f"App '{app_label}' does not have migrations.")
        # Find a migration.
        try:
            migration = loader.get_migration_by_prefix(app_label, migration_name)
        except AmbiguityError:
            raise CommandError(
                f"More than one migration matches '{migration_name}' in app "
                f"'{app_label}'. Please be more specific."
            )
        except KeyError:
            raise CommandError(
                f"Cannot find a migration matching '{migration_name}' from app "
                f"'{app_label}'."
            )

        # Optimize the migration.
        optimizer = MigrationOptimizer()
        new_operations = optimizer.optimize(migration.operations, migration.app_label)
        if len(migration.operations) == len(new_operations):
            if verbosity > 0:
                self.stdout.write("No optimizations possible.")
            return
        else:
            if verbosity > 0:
                self.stdout.write(
                    "Optimizing from %d operations to %d operations."
                    % (len(migration.operations), len(new_operations))
                )
            if check:
                sys.exit(1)

        # Set the new migration optimizations.
        migration.operations = new_operations

        # Write out the optimized migration file.
        writer = MigrationWriter(migration)
        migration_file_string = writer.as_string()
        if writer.needs_manual_porting:
            if migration.replaces:
                raise CommandError(
                    "Migration will require manual porting but is already a squashed "
                    "migration.\nTransition to a normal migration first: "
                    "https://docs.djangoproject.com/en/%s/topics/migrations/"
                    "#squashing-migrations" % get_docs_version()
                )
            # Make a new migration with those operations.
            subclass = type(
                "Migration",
                (migrations.Migration,),
                {
                    "dependencies": migration.dependencies,
                    "operations": new_operations,
                    "replaces": [(migration.app_label, migration.name)],
                },
            )
            optimized_migration_name = "%s_optimized" % migration.name
            optimized_migration = subclass(optimized_migration_name, app_label)
            writer = MigrationWriter(optimized_migration)
            migration_file_string = writer.as_string()
            if verbosity > 0:
                self.stdout.write(
                    self.style.MIGRATE_HEADING("Manual porting required") + "\n"
                    "  Your migrations contained functions that must be manually "
                    "copied over,\n"
                    "  as we could not safely copy their implementation.\n"
                    "  See the comment at the top of the optimized migration for "
                    "details."
                )
                if shutil.which("black"):
                    self.stdout.write(
                        self.style.WARNING(
                            "Optimized migration couldn't be formatted using the "
                            '"black" command. You can call it manually.'
                        )
                    )
        with open(writer.path, "w", encoding="utf-8") as fh:
            fh.write(migration_file_string)
        run_formatters([writer.path])

        if verbosity > 0:
            self.stdout.write(
                self.style.MIGRATE_HEADING(f"Optimized migration {writer.path}")
            )
Beispiel #51
0
    def interleaved_migrations_test(self):
#        from django.apps import apps
#         unreleased = {}
#         for appconf in apps.get_app_configs():
#             mpath = Path(appconf.path) / 'migrations'
#             for pyfile in mpath.glob('*.py'):
#                 if pyfile.name == '__init__.py':
#                     continue
#                 mmod = import_module('%s.migrations.%s' % (appconf.name, pyfile.stem))
#                 for n,v in mmod.__dict__.items():
#                     if isinstance(v, type) and issubclass(v, migrations.Migration):
#                         migration = v
#                         self.runner.coverage_data['migration']['present'][migration.__module__] = {'operations':[]}
#                         d = self.runner.coverage_data['migration']['present'][migration.__module__]
#                         for n,v in migration.__dict__.items():
#                             if n == 'operations':
#                                 for op in v:
#                                     cl = op.__class__
#                                     if issubclass(cl, ModelOperation) or issubclass(cl, FieldOperation):
#                                         d['operations'].append('schema')
#                                     elif issubclass(cl, Operation):
#                                         d['operations'].append('data')
#                                     else:
#                                         raise RuntimeError("Found unexpected operation type in migration: %s" % (op))

        # Clear this setting, otherwise we won't see any migrations
        settings.MIGRATION_MODULES = {}
        # Save information here, for later write to file
        info = self.runner.coverage_data['migration']['present']
        # Get migrations
        loader = MigrationLoader(None, ignore_no_migrations=True)
        graph = loader.graph
        targets = graph.leaf_nodes()
        seen = set()
        opslist = []
        for target in targets:
            #debug.show('target')
            for migration in graph.forwards_plan(target):
                if migration not in seen:
                    node = graph.node_map[migration]
                    #debug.show('node')
                    seen.add(migration)
                    ops = []
                    # get the actual migration object
                    migration = loader.graph.nodes[migration]
                    for op in migration.operations:
                        cl = op.__class__
                        if issubclass(cl, ModelOperation) or issubclass(cl, FieldOperation):
                            ops.append(('schema', cl.__name__))
                        elif issubclass(cl, Operation):
                            ops.append(('data', cl.__name__))
                        else:
                            raise RuntimeError("Found unexpected operation type in migration: %s" % (op))
                    info[migration.__module__] = {'operations': ops}
                    opslist.append((migration, node, ops))
        # Compare the migrations we found to those present in the latest
        # release, to see if we have any unreleased migrations
        latest_coverage_version = self.runner.coverage_master["version"]
        if 'migration' in self.runner.coverage_master[latest_coverage_version]:
            release_data = self.runner.coverage_master[latest_coverage_version]['migration']['present']
        else:
            release_data = {}
        unreleased = []
        for migration, node, ops in opslist:
            if not migration.__module__ in release_data:
                for op, nm in ops:
                    unreleased.append((node, op, nm))
        # gather the transitions in operation types.  We'll allow 1
        # transition, but not 2 or more.
        mixed = [ unreleased[i] for i in range(1,len(unreleased)) if unreleased[i][1] != unreleased[i-1][1] ]
        if len(mixed) > 1:
            raise self.failureException('Found interleaved schema and data operations in unreleased migrations;'
                ' please see if they can be re-ordered with all schema migrations before the data migrations:\n'
                +('\n'.join(['    %-6s:  %-12s, %s (%s)'% (op, node.key[0], node.key[1], nm) for (node, op, nm) in unreleased ])))
Beispiel #52
0
    def test_loading_squashed_erroneous(self):
        "Tests loading a complex but erroneous set of squashed migrations"

        loader = MigrationLoader(connection)
        recorder = MigrationRecorder(connection)
        self.addCleanup(recorder.flush)

        def num_nodes():
            plan = set(loader.graph.forwards_plan(('migrations', '7_auto')))
            return len(plan - loader.applied_migrations.keys())

        # Empty database: use squashed migration
        loader.build_graph()
        self.assertEqual(num_nodes(), 5)

        # Starting at 1 or 2 should use the squashed migration too
        recorder.record_applied("migrations", "1_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 4)

        recorder.record_applied("migrations", "2_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 3)

        # However, starting at 3 or 4, nonexistent migrations would be needed.
        msg = (
            "Migration migrations.6_auto depends on nonexistent node ('migrations', '5_auto'). "
            "Django tried to replace migration migrations.5_auto with any of "
            "[migrations.3_squashed_5] but wasn't able to because some of the replaced "
            "migrations are already applied.")

        recorder.record_applied("migrations", "3_auto")
        with self.assertRaisesMessage(NodeNotFoundError, msg):
            loader.build_graph()

        recorder.record_applied("migrations", "4_auto")
        with self.assertRaisesMessage(NodeNotFoundError, msg):
            loader.build_graph()

        # Starting at 5 to 7 we are passed the squashed migrations
        recorder.record_applied("migrations", "5_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 2)

        recorder.record_applied("migrations", "6_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 1)

        recorder.record_applied("migrations", "7_auto")
        loader.build_graph()
        self.assertEqual(num_nodes(), 0)
Beispiel #53
0
    def handle(self, **options):

        self.verbosity = options.get('verbosity')
        self.interactive = options.get('interactive')
        app_label = options['app_label']
        migration_name = options['migration_name']
        no_optimize = options['no_optimize']

        # Load the current graph state, check the app and migration they asked for exists
        loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
        if app_label not in loader.migrated_apps:
            raise CommandError(
                "App '%s' does not have migrations (so squashmigrations on "
                "it makes no sense)" % app_label
            )
        try:
            migration = loader.get_migration_by_prefix(app_label, migration_name)
        except AmbiguityError:
            raise CommandError(
                "More than one migration matches '%s' in app '%s'. Please be "
                "more specific." % (migration_name, app_label)
            )
        except KeyError:
            raise CommandError(
                "Cannot find a migration matching '%s' from app '%s'." %
                (migration_name, app_label)
            )

        # Work out the list of predecessor migrations
        migrations_to_squash = [
            loader.get_migration(al, mn)
            for al, mn in loader.graph.forwards_plan((migration.app_label, migration.name))
            if al == migration.app_label
        ]

        # Tell them what we're doing and optionally ask if we should proceed
        if self.verbosity > 0 or self.interactive:
            self.stdout.write(self.style.MIGRATE_HEADING("Will squash the following migrations:"))
            for migration in migrations_to_squash:
                self.stdout.write(" - %s" % migration.name)

            if self.interactive:
                answer = None
                while not answer or answer not in "yn":
                    answer = six.moves.input("Do you wish to proceed? [yN] ")
                    if not answer:
                        answer = "n"
                        break
                    else:
                        answer = answer[0].lower()
                if answer != "y":
                    return

        # Load the operations from all those migrations and concat together,
        # along with collecting external dependencies and detecting
        # double-squashing
        operations = []
        dependencies = set()
        for smigration in migrations_to_squash:
            if smigration.replaces:
                raise CommandError(
                    "You cannot squash squashed migrations! Please transition "
                    "it to a normal migration first: "
                    "https://docs.djangoproject.com/en/%s/topics/migrations/#squashing-migrations" % get_docs_version()
                )
            operations.extend(smigration.operations)
            for dependency in smigration.dependencies:
                if isinstance(dependency, SwappableTuple):
                    if settings.AUTH_USER_MODEL == dependency.setting:
                        dependencies.add(("__setting__", "AUTH_USER_MODEL"))
                    else:
                        dependencies.add(dependency)
                elif dependency[0] != smigration.app_label:
                    dependencies.add(dependency)

        if no_optimize:
            if self.verbosity > 0:
                self.stdout.write(self.style.MIGRATE_HEADING("(Skipping optimization.)"))
            new_operations = operations
        else:
            if self.verbosity > 0:
                self.stdout.write(self.style.MIGRATE_HEADING("Optimizing..."))

            optimizer = MigrationOptimizer()
            new_operations = optimizer.optimize(operations, migration.app_label)

            if self.verbosity > 0:
                if len(new_operations) == len(operations):
                    self.stdout.write("  No optimizations possible.")
                else:
                    self.stdout.write(
                        "  Optimized from %s operations to %s operations." %
                        (len(operations), len(new_operations))
                    )

        # Work out the value of replaces (any squashed ones we're re-squashing)
        # need to feed their replaces into ours
        replaces = []
        for migration in migrations_to_squash:
            if migration.replaces:
                replaces.extend(migration.replaces)
            else:
                replaces.append((migration.app_label, migration.name))

        # Make a new migration with those operations
        subclass = type("Migration", (migrations.Migration, ), {
            "dependencies": dependencies,
            "operations": new_operations,
            "replaces": replaces,
        })
        new_migration = subclass("0001_squashed_%s" % migration.name, app_label)

        # Write out the new migration file
        writer = MigrationWriter(new_migration)
        with open(writer.path, "wb") as fh:
            fh.write(writer.as_string())

        if self.verbosity > 0:
            self.stdout.write(self.style.MIGRATE_HEADING("Created new squashed migration %s" % writer.path))
            self.stdout.write("  You should commit this migration but leave the old ones in place;")
            self.stdout.write("  the new migration will be used for new installs. Once you are sure")
            self.stdout.write("  all instances of the codebase have applied the migrations you squashed,")
            self.stdout.write("  you can delete them.")
            if writer.needs_manual_porting:
                self.stdout.write(self.style.MIGRATE_HEADING("Manual porting required"))
                self.stdout.write("  Your migrations contained functions that must be manually copied over,")
                self.stdout.write("  as we could not safely copy their implementation.")
                self.stdout.write("  See the comment at the top of the squashed migration for details.")
Beispiel #54
0
 def test_load_module_file(self):
     with override_settings(
             MIGRATION_MODULES={
                 "migrations": "migrations.faulty_migrations.file"
             }):
         MigrationLoader(connection)
Beispiel #55
0
class MigrationLinter(object):
    def __init__(
        self,
        path=None,
        ignore_name_contains=None,
        ignore_name=None,
        include_name_contains=None,
        include_name=None,
        include_apps=None,
        exclude_apps=None,
        database=DEFAULT_DB_ALIAS,
        cache_path=DEFAULT_CACHE_PATH,
        no_cache=False,
        only_applied_migrations=False,
        only_unapplied_migrations=False,
        exclude_migration_tests=None,
        quiet=None,
        warnings_as_errors=False,
        no_output=False,
    ):
        # Store parameters and options
        self.django_path = path
        self.ignore_name_contains = ignore_name_contains
        self.ignore_name = ignore_name or tuple()
        self.include_name_contains = include_name_contains
        self.include_name = include_name or tuple()
        self.include_apps = include_apps
        self.exclude_apps = exclude_apps
        self.exclude_migration_tests = exclude_migration_tests or []
        self.database = database or DEFAULT_DB_ALIAS
        self.cache_path = cache_path or DEFAULT_CACHE_PATH
        self.no_cache = no_cache
        self.only_applied_migrations = only_applied_migrations
        self.only_unapplied_migrations = only_unapplied_migrations
        self.quiet = quiet or []
        self.warnings_as_errors = warnings_as_errors
        self.no_output = no_output

        # Initialise counters
        self.reset_counters()

        # Initialise cache. Read from old, write to new to prune old entries.
        if self.should_use_cache():
            self.old_cache = Cache(self.django_path, self.database, self.cache_path)
            self.new_cache = Cache(self.django_path, self.database, self.cache_path)
            self.old_cache.load()

        # Initialise migrations
        from django.db.migrations.loader import MigrationLoader

        self.migration_loader = MigrationLoader(
            connection=connections[self.database], load=True
        )

    def reset_counters(self):
        self.nb_valid = 0
        self.nb_ignored = 0
        self.nb_warnings = 0
        self.nb_erroneous = 0
        self.nb_total = 0

    def should_use_cache(self):
        return self.django_path and not self.no_cache

    def lint_all_migrations(
        self,
        app_label=None,
        migration_name=None,
        git_commit_id=None,
        migrations_file_path=None,
    ):
        # Collect migrations
        migrations_list = self.read_migrations_list(migrations_file_path)
        if git_commit_id:
            migrations = self._gather_migrations_git(git_commit_id, migrations_list)
        else:
            migrations = self._gather_all_migrations(migrations_list)

        # Lint those migrations
        sorted_migrations = sorted(
            migrations, key=lambda migration: (migration.app_label, migration.name)
        )

        specific_target_migration = (
            self.migration_loader.get_migration_by_prefix(app_label, migration_name)
            if app_label and migration_name
            else None
        )

        for m in sorted_migrations:
            if app_label and migration_name:
                if m == specific_target_migration:
                    self.lint_migration(m)
            elif app_label:
                if m.app_label == app_label:
                    self.lint_migration(m)
            else:
                self.lint_migration(m)

        if self.should_use_cache():
            self.new_cache.save()

    def lint_migration(self, migration):
        app_label = migration.app_label
        migration_name = migration.name
        operations = migration.operations
        self.nb_total += 1

        md5hash = self.get_migration_hash(app_label, migration_name)

        if self.should_ignore_migration(app_label, migration_name, operations):
            self.print_linting_msg(
                app_label, migration_name, "IGNORE", MessageType.IGNORE
            )
            self.nb_ignored += 1
            return

        if self.should_use_cache() and md5hash in self.old_cache:
            self.lint_cached_migration(app_label, migration_name, md5hash)
            return

        sql_statements = self.get_sql(app_label, migration_name)
        errors, ignored, warnings = analyse_sql_statements(
            sql_statements,
            settings.DATABASES[self.database]["ENGINE"],
            self.exclude_migration_tests,
        )

        err, ignored_data, warnings_data = self.analyse_data_migration(migration)
        if err:
            errors += err
        if ignored_data:
            ignored += ignored_data
        if warnings_data:
            warnings += warnings_data

        if self.warnings_as_errors:
            errors += warnings
            warnings = []

        # Fixme: have a more generic approach to handling errors/warnings/ignored/ok?
        if errors:
            self.print_linting_msg(app_label, migration_name, "ERR", MessageType.ERROR)
            self.nb_erroneous += 1
            self.print_errors(errors)
            if warnings:
                self.print_warnings(warnings)
            value_to_cache = {"result": "ERR", "errors": errors, "warnings": warnings}
        elif warnings:
            self.print_linting_msg(
                app_label, migration_name, "WARNING", MessageType.WARNING
            )
            self.nb_warnings += 1
            self.print_warnings(warnings)
            value_to_cache = {"result": "WARNING", "warnings": warnings}
            # Fixme: not displaying ignored errors, when
        else:
            if ignored:
                self.print_linting_msg(
                    app_label, migration_name, "OK (ignored)", MessageType.IGNORE
                )
                self.print_errors(ignored)
            else:
                self.print_linting_msg(app_label, migration_name, "OK", MessageType.OK)
            self.nb_valid += 1
            value_to_cache = {"result": "OK"}

        if self.should_use_cache():
            self.new_cache[md5hash] = value_to_cache

    @staticmethod
    def get_migration_hash(app_label, migration_name):
        hash_md5 = hashlib.md5()
        with open(get_migration_abspath(app_label, migration_name), "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                hash_md5.update(chunk)
        return hash_md5.hexdigest()

    def lint_cached_migration(self, app_label, migration_name, md5hash):
        cached_value = self.old_cache[md5hash]
        if cached_value["result"] == "IGNORE":
            self.print_linting_msg(
                app_label, migration_name, "IGNORE (cached)", MessageType.IGNORE
            )
            self.nb_ignored += 1
        elif cached_value["result"] == "OK":
            self.print_linting_msg(
                app_label, migration_name, "OK (cached)", MessageType.OK
            )
            self.nb_valid += 1
        elif cached_value["result"] == "WARNING":
            self.print_linting_msg(
                app_label, migration_name, "WARNING (cached)", MessageType.WARNING
            )
            self.nb_warnings += 1
            self.print_warnings(cached_value["warnings"])
        else:
            self.print_linting_msg(
                app_label, migration_name, "ERR (cached)", MessageType.ERROR
            )
            self.nb_erroneous += 1
            if "errors" in cached_value:
                self.print_errors(cached_value["errors"])
            if "warnings" in cached_value and cached_value["warnings"]:
                self.print_warnings(cached_value["warnings"])

        self.new_cache[md5hash] = cached_value

    def print_linting_msg(self, app_label, migration_name, msg, lint_result):
        if lint_result.value in self.quiet:
            return
        if not self.no_output:
            print("({0}, {1})... {2}".format(app_label, migration_name, msg))

    def print_errors(self, errors):
        if MessageType.ERROR.value in self.quiet:
            return
        for err in errors:
            error_str = "\t{0}".format(err["msg"])
            if err.get("table"):
                error_str += " (table: {0}".format(err["table"])
                if err.get("column"):
                    error_str += ", column: {0}".format(err["column"])
                error_str += ")"
            if not self.no_output:
                print(error_str)

    def print_warnings(self, warnings):
        if MessageType.WARNING.value in self.quiet:
            return

        for warning_details in warnings:
            warn_str = "\t{}".format(warning_details["msg"])
            if not self.no_output:
                print(warn_str)

    def print_summary(self):
        if self.no_output:
            return
        print("*** Summary ***")
        print("Valid migrations: {}/{}".format(self.nb_valid, self.nb_total))
        print("Erroneous migrations: {}/{}".format(self.nb_erroneous, self.nb_total))
        print("Migrations with warnings: {}/{}".format(self.nb_warnings, self.nb_total))
        print("Ignored migrations: {}/{}".format(self.nb_ignored, self.nb_total))

    @property
    def has_errors(self):
        return self.nb_erroneous > 0

    def get_sql(self, app_label, migration_name):
        logger.info(
            "Calling sqlmigrate command {} {}".format(app_label, migration_name)
        )
        dev_null = open(os.devnull, "w")
        try:
            sql_statement = call_command(
                "sqlmigrate",
                app_label,
                migration_name,
                database=self.database,
                stdout=dev_null,
            )
        except (ValueError, ProgrammingError):
            logger.warning(
                (
                    "Error while executing sqlmigrate on (%s, %s). "
                    "Continuing execution with empty SQL."
                ),
                app_label,
                migration_name,
            )
            sql_statement = ""
        return sql_statement.splitlines()

    @staticmethod
    def is_migration_file(filename):
        from django.db.migrations.loader import MIGRATIONS_MODULE_NAME

        return (
            re.search(r"/{0}/.*\.py".format(MIGRATIONS_MODULE_NAME), filename)
            and "__init__" not in filename
        )

    @classmethod
    def read_migrations_list(cls, migrations_file_path):
        """
        Returning an empty list is different from returning None here.
        None: no file was specified and we should consider all migrations
        Empty list: no migration found in the file and we should consider no migration
        """
        if not migrations_file_path:
            return None

        migrations = []
        try:
            with open(migrations_file_path, "r") as file:
                for line in file:
                    if cls.is_migration_file(line):
                        app_label, name = split_migration_path(line)
                        migrations.append((app_label, name))
        except IOError:
            logger.exception("Migrations list path not found %s", migrations_file_path)
            raise Exception("Error while reading migrations list file")

        if not migrations:
            logger.info(
                "No valid migration paths found in the migrations file %s",
                migrations_file_path,
            )
        return migrations

    def _gather_migrations_git(self, git_commit_id, migrations_list=None):
        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 self.is_migration_file(line):
                app_label, name = split_migration_path(line)
                if migrations_list is None or (app_label, name) in migrations_list:
                    if (app_label, name) in self.migration_loader.disk_migrations:
                        migration = self.migration_loader.disk_migrations[
                            app_label, name
                        ]
                        migrations.append(migration)
                    else:
                        logger.info(
                            "Found migration file (%s, %s) "
                            "that is not present in loaded migration.",
                            app_label,
                            name,
                        )
        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 _gather_all_migrations(self, migrations_list=None):
        for (
            (app_label, name),
            migration,
        ) in self.migration_loader.disk_migrations.items():
            if app_label not in DJANGO_APPS_WITH_MIGRATIONS:  # Prune Django apps
                if migrations_list is None or (app_label, name) in migrations_list:
                    yield migration

    def should_ignore_migration(self, app_label, migration_name, operations=()):
        return (
            (self.include_apps and app_label not in self.include_apps)
            or (self.exclude_apps and app_label in self.exclude_apps)
            or any(isinstance(o, IgnoreMigration) for o in operations)
            or (
                self.ignore_name_contains
                and self.ignore_name_contains in migration_name
            )
            or (
                self.include_name_contains
                and self.include_name_contains not in migration_name
            )
            or (migration_name in self.ignore_name)
            or (self.include_name and migration_name not in self.include_name)
            or (
                self.only_applied_migrations
                and (app_label, migration_name)
                not in self.migration_loader.applied_migrations
            )
            or (
                self.only_unapplied_migrations
                and (app_label, migration_name)
                in self.migration_loader.applied_migrations
            )
        )

    def analyse_data_migration(self, migration):
        errors = []
        ignored = []
        warnings = []

        for operation in migration.operations:
            if isinstance(operation, RunPython):
                op_errors, op_ignored, op_warnings = self.lint_runpython(operation)
            elif isinstance(operation, RunSQL):
                op_errors, op_ignored, op_warnings = self.lint_runsql(operation)
            else:
                op_errors, op_ignored, op_warnings = [], [], []

            if op_errors:
                errors += op_errors
            if op_ignored:
                ignored += op_ignored
            if op_warnings:
                warnings += op_warnings

        return errors, ignored, warnings

    def lint_runpython(self, runpython):
        function_name = runpython.code.__name__
        error = []
        ignored = []
        warning = []

        # Detect warning on missing reverse operation
        if not runpython.reversible:
            issue = {
                "code": "RUNPYTHON_REVERSIBLE",
                "msg": "'{}': RunPython data migration is not reversible".format(
                    function_name
                ),
            }
            if issue["code"] in self.exclude_migration_tests:
                ignored.append(issue)
            else:
                warning.append(issue)

        # Detect warning for argument naming convention
        args_spec = (
            inspect.getargspec(runpython.code)
            if PY2
            else inspect.getfullargspec(runpython.code)
        )
        if tuple(args_spec.args) != EXPECTED_DATA_MIGRATION_ARGS:
            issue = {
                "code": "RUNPYTHON_ARGS_NAMING_CONVENTION",
                "msg": (
                    "'{}': By convention, "
                    "RunPython names the two arguments: apps, schema_editor"
                ).format(function_name),
            }
            if issue["code"] in self.exclude_migration_tests:
                ignored.append(issue)
            else:
                warning.append(issue)

        # Detect wrong model imports
        # Forward
        issues = self.get_runpython_model_import_issues(runpython.code)
        for issue in issues:
            if issue["code"] in self.exclude_migration_tests:
                ignored.append(issue)
            else:
                error.append(issue)

        # Backward
        if runpython.reversible:
            issues = self.get_runpython_model_import_issues(runpython.reverse_code)
            for issue in issues:
                if issue and issue["code"] in self.exclude_migration_tests:
                    ignored.append(issue)
                else:
                    error.append(issue)

        # Detect warning if model variable name is not the same as model class
        issues = self.get_runpython_model_variable_naming_issues(runpython.code)
        for issue in issues:
            if issue and issue["code"] in self.exclude_migration_tests:
                ignored.append(issue)
            else:
                warning.append(issue)

        if runpython.reversible:
            issues = self.get_runpython_model_variable_naming_issues(
                runpython.reverse_code
            )
            for issue in issues:
                if issue and issue["code"] in self.exclude_migration_tests:
                    ignored.append(issue)
                else:
                    warning.append(issue)

        return error, ignored, warning

    @staticmethod
    def get_runpython_model_import_issues(code):
        model_object_regex = re.compile(r"[^a-zA-Z]?([a-zA-Z0-9]+?)\.objects")

        function_name = code.__name__
        source_code = inspect.getsource(code)

        called_models = model_object_regex.findall(source_code)
        issues = []
        for model in called_models:
            has_get_model_call = (
                re.search(
                    r"{}.*= +\w+\.get_model\(".format(model),
                    source_code,
                )
                is not None
            )
            if not has_get_model_call:
                issues.append(
                    {
                        "code": "RUNPYTHON_MODEL_IMPORT",
                        "msg": (
                            "'{}': Could not find an 'apps.get_model(\"...\", \"{}\")' "
                            "call. Importing the model directly is incorrect for "
                            "data migrations."
                        ).format(function_name, model),
                    }
                )
        return issues

    @staticmethod
    def get_runpython_model_variable_naming_issues(code):
        model_object_regex = re.compile(r"[^a-zA-Z]?([a-zA-Z0-9]+?)\.objects")

        function_name = code.__name__
        source_code = inspect.getsource(code)

        called_models = model_object_regex.findall(source_code)
        issues = []
        for model in called_models:
            has_same_model_name = (
                re.search(
                    r"{model}.*= +\w+?\.get_model\([^)]+?\.{model}.*?\)".format(
                        model=model
                    ),
                    source_code,
                    re.MULTILINE | re.DOTALL,
                )
                is not None
                or re.search(
                    r"{model}.*= +\w+?\.get_model\([^)]+?,[^)]*?{model}.*?\)".format(
                        model=model
                    ),
                    source_code,
                    re.MULTILINE | re.DOTALL,
                )
                is not None
            )
            if not has_same_model_name:
                issues.append(
                    {
                        "code": "RUNPYTHON_MODEL_VARIABLE_NAME",
                        "msg": (
                            "'{}': Model variable name {} is different from the "
                            "model class name that was found in the "
                            "apps.get_model(...) call."
                        ).format(function_name, model),
                    }
                )
        return issues

    def lint_runsql(self, runsql):
        error = []
        ignored = []
        warning = []

        # Detect warning on missing reverse operation
        if not runsql.reversible:
            issue = {
                "code": "RUNSQL_REVERSIBLE",
                "msg": "RunSQL data migration is not reversible",
            }
            if issue["code"] in self.exclude_migration_tests:
                ignored.append(issue)
            else:
                warning.append(issue)

        # Put the SQL in our SQL analyser
        if runsql.sql != RunSQL.noop:
            sql_statements = []
            if isinstance(runsql.sql, (list, tuple)):
                for sql in runsql.sql:
                    params = None
                    if isinstance(sql, (list, tuple)):
                        elements = len(sql)
                        if elements == 2:
                            sql, params = sql
                        else:
                            raise ValueError("Expected a 2-tuple but got %d" % elements)
                        sql_statements.append(sql % params)
                    else:
                        sql_statements.append(sql)
            else:
                sql_statements.append(runsql.sql)

            sql_errors, sql_ignored, sql_warnings = analyse_sql_statements(
                sql_statements,
                settings.DATABASES[self.database]["ENGINE"],
                self.exclude_migration_tests,
            )
            if sql_errors:
                error += sql_errors
            if sql_ignored:
                ignored += sql_ignored
            if sql_warnings:
                warning += sql_warnings

        # And analysse the reverse SQL
        if runsql.reversible and runsql.reverse_sql != RunSQL.noop:
            sql_statements = []
            if isinstance(runsql.reverse_sql, (list, tuple)):
                for sql in runsql.reverse_sql:
                    params = None
                    if isinstance(sql, (list, tuple)):
                        elements = len(sql)
                        if elements == 2:
                            sql, params = sql
                        else:
                            raise ValueError("Expected a 2-tuple but got %d" % elements)
                        sql_statements.append(sql % params)
                    else:
                        sql_statements.append(sql)
            else:
                sql_statements.append(runsql.reverse_sql)

            sql_errors, sql_ignored, sql_warnings = analyse_sql_statements(
                sql_statements,
                settings.DATABASES[self.database]["ENGINE"],
                self.exclude_migration_tests,
            )
            if sql_errors:
                error += sql_errors
            if sql_ignored:
                ignored += sql_ignored
            if sql_warnings:
                warning += sql_warnings

        return error, ignored, warning