def get_app(app_label, emptyOK=False):
    """Return the app with the given label.

    This returns the app from the app registry on Django >= 1.7, and from
    the old-style cache on Django < 1.7.

        app_label (str):
            The label for the app containing the models.

        emptyOK (bool, optional):
            Impacts the return value if the app has no models in it.

    Returns:
        module:
        The app module, if available.

        If the app module is available, but the models module is not and
        ``emptyOK`` is set, this will return ``None``. Otherwise, if modules
        are not available, this will raise
        :py:exc:`~django.core.exceptions.ImproperlyConfigured`.

    Raises:
        django.core.exceptions.ImproperlyConfigured:
            The app module was not found, or it was found but a models module
            was not and ``emptyOK`` was ``False``.
    """
    if apps:
        # Django >= 1.7
        try:
            models_module = apps.get_app_config(app_label).models_module
        except LookupError as e:
            # Convert this to an ImproperlyConfigured.
            raise ImproperlyConfigured(*e.args)

        if models_module is None and not emptyOK:
            # This is the exact error that Django 1.6 provided.
            raise ImproperlyConfigured(
                'App with label %s is missing a models.py module.'
                % app_label)

        return models_module
    else:
        # Django < 1.7
        return cache.get_app(app_label, emptyOK) class EvolutionsSource(object):
    """The source for an app's evolutions."""

    #: The evolutions are provided by the app.
    APP = 'app'

    #: The evolutions are built-in to Django Evolution.
    BUILTIN = 'builtin'

    #: The evolutions are provided custom by the project.
    PROJECT = 'project' class UpgradeMethod(object):
    """Upgrade methods available for an application."""

    #: The app is upgraded through Django Evolution.
    EVOLUTIONS = 'evolutions'

    #: The app is upgraded through Django Migrations.
    MIGRATIONS = 'migrations' class Version(models.Model):
    signature = SignatureField()
    when = models.DateTimeField(default=now)

    objects = VersionManager()

    def is_hinted(self):
        """Return whether this is a hinted version.

        Hinted versions store a signature without any accompanying evolutions.

        Returns:
            bool:
            ``True`` if this is a hinted evolution. ``False`` if it's based on
            explicit evolutions.
        """
        return not self.evolutions.exists()

    def __str__(self):
        if self.is_hinted():
            return 'Hinted version, updated on %s' % self.when

        return 'Stored version, updated on %s' % self.when

    class Meta:
        ordering = ('-when',)
        db_table = 'django_project_version' class AddField(BaseModelFieldMutation):
    """A mutation that adds a field to a model."""

    simulation_failure_error = (
        'Cannot add the field "%(field_name)s" to model '
        '"%(app_label)s.%(model_name)s".'
    )

    def __init__(self, model_name, field_name, field_type, initial=None,
                 **field_attrs):
        """Initialize the mutation.

        Args:
            model_name (unicode):
                The name of the model to add the field to.

            field_name (unicode):
                The name of the new field.

            field_type (cls):
                The field class to use. This must be a subclass of
                :py:class:`django.db.models.Field`.

            initial (object, optional):
                The initial value for the field. This is required if non-null.

            **field_attrs (dict):
                Attributes to set on the field.
        """
        super(AddField, self).__init__(model_name, field_name)

        self.field_type = field_type
        self.field_attrs = field_attrs
        self.initial = initial

    def get_hint_params(self):
        """Return parameters for the mutation's hinted evolution.

        Returns:
            list of unicode:
            A list of parameter strings to pass to the mutation's constructor
            in a hinted evolution.
        """
        params = [
            self.serialize_attr(key, value)
            for key, value in six.iteritems(self.field_attrs)
        ]

        if self.initial is not None:
            params.append(self.serialize_attr('initial', self.initial))

        return [
            self.serialize_value(self.model_name),
            self.serialize_value(self.field_name),
            self.serialize_value(self.field_type),
        ] + sorted(params)

    def simulate(self, simulation):
        """Simulate the mutation.

        This will alter the database schema to add the specified field.

        Args:
            simulation (Simulation):
                The state for the simulation.

        Raises:
            django_evolution.errors.SimulationFailure:
                The simulation failed. The reason is in the exception's
                message.
        """
        model_sig = simulation.get_model_sig(self.model_name)

        if model_sig.get_field_sig(self.field_name) is not None:
            simulation.fail('A field with this name already exists.')

        if (not issubclass(self.field_type, models.ManyToManyField) and
            not self.field_attrs.get('null')
            and self.initial is None):
            simulation.fail('A non-null initial value must be specified in '
                            'the mutation.')

        field_attrs = self.field_attrs.copy()
        related_model = field_attrs.pop('related_model', None)

        field_sig = FieldSignature(field_name=self.field_name,
                                   field_type=self.field_type,
                                   field_attrs=field_attrs,
                                   related_model=related_model)
        model_sig.add_field_sig(field_sig)

    def mutate(self, mutator, model):
        """Schedule a field addition on the mutator.

        This will instruct the mutator to add a new field on a model. It will
        be scheduled and later executed on the database, if not optimized out.

        Args:
            mutator (django_evolution.mutators.ModelMutator):
                The mutator to perform an operation on.

            model (MockModel):
                The model being mutated.
        """
        if issubclass(self.field_type, models.ManyToManyField):
            self.add_m2m_table(mutator, model)
        else:
            self.add_column(mutator, model)

    def add_column(self, mutator, model):
        """Add a standard column to the model.

        Args:
            mutator (django_evolution.mutators.ModelMutator):
                The mutator to perform an operation on.

            model (MockModel):
                The model being mutated.
        """
        field = self._create_field(mutator, model)

        mutator.add_column(self, field, self.initial)

    def add_m2m_table(self, mutator, model):
        """Add a ManyToMany column to the model and an accompanying table.

        Args:
            mutator (django_evolution.mutators.ModelMutator):
                The mutator to perform an operation on.

            model (MockModel):
                The model being mutated.
        """
        field = self._create_field(mutator, model)

        related_app_label, related_model_name = \
            self.field_attrs['related_model'].split('.')
        related_sig = (
            mutator.project_sig
            .get_app_sig(related_app_label)
            .get_model_sig(related_model_name)
        )
        related_model = MockModel(project_sig=mutator.project_sig,
                                  app_name=related_app_label,
                                  model_name=related_model_name,
                                  model_sig=related_sig,
                                  db_name=mutator.database)
        related = MockRelated(related_model=related_model,
                              model=model,
                              field=field)

        if hasattr(field, '_get_m2m_column_name'):
            # Django < 1.2
            field.m2m_column_name = \
                partial(field._get_m2m_column_name, related)
            field.m2m_reverse_name = \
                partial(field._get_m2m_reverse_name, related)

            field.m2m_column_name = \
                partial(field._get_m2m_attr, related, 'column')
            field.m2m_reverse_name = \
                partial(field._get_m2m_reverse_attr, related, 'column')

        mutator.add_sql(self, mutator.evolver.add_m2m_table(model, field))

    def _create_field(self, mutator, parent_model):
        """Create a new field to add to the model.

        Args:
            mutator (django_evolution.mutators.ModelMutator):
                The mutator to perform an operation on.

            parent_model (django_evolution.mock_models.MockModel):
                The model to add the field to.

        Returns:
            django.db.models.Field:
            The newly-created field.
        """
        field_attrs = self.field_attrs.copy()
        related_model = field_attrs.pop('related_model', None)

        return create_field(project_sig=mutator.project_sig,
                            field_name=self.field_name,
                            field_type=self.field_type,
                            field_attrs=field_attrs,
                            parent_model=parent_model,
                            related_model=related_model) class ChangeField(BaseModelFieldMutation):
    """A mutation that changes attributes on a field on a model."""

    simulation_failure_error = (
        'Cannot change the field "%(field_name)s" on model '
        '"%(app_label)s.%(model_name)s".'
    )

    def __init__(self, model_name, field_name, initial=None, **field_attrs):
        """Initialize the mutation.

        Args:
            model_name (unicode):
                The name of the model containing the field to change.

            field_name (unicode):
                The name of the field to change.

            initial (object, optional):
                The initial value for the field. This is required if non-null.

            **field_attrs (dict):
                Attributes to set on the field.
        """
        super(ChangeField, self).__init__(model_name, field_name)

        self.field_attrs = field_attrs
        self.initial = initial

    def get_hint_params(self):
        """Return parameters for the mutation's hinted evolution.

        Returns:
            list of unicode:
            A list of parameter strings to pass to the mutation's constructor
            in a hinted evolution.
        """
        params = [
            self.serialize_attr(attr_name, attr_value)
            for attr_name, attr_value in six.iteritems(self.field_attrs)
        ] + [
            self.serialize_attr('initial', self.initial),
        ]

        return [
            self.serialize_value(self.model_name),
            self.serialize_value(self.field_name),
        ] + sorted(params)

    def simulate(self, simulation):
        """Simulate the mutation.

        This will alter the database schema to change attributes for the
        specified field.

        Args:
            simulation (Simulation):
                The state for the simulation.

        Raises:
            django_evolution.errors.SimulationFailure:
                The simulation failed. The reason is in the exception's
                message.
        """
        field_sig = simulation.get_field_sig(self.model_name, self.field_name)
        field_sig.field_attrs.update(self.field_attrs)

        if ('null' in self.field_attrs and not self.field_attrs['null'] and
            not issubclass(field_sig.field_type, models.ManyToManyField) and
            self.initial is None):
            simulation.fail('A non-null initial value needs to be specified '
                            'in the mutation.')

    def mutate(self, mutator, model):
        """Schedule a field change on the mutator.

        This will instruct the mutator to change attributes on a field on a
        model. It will be scheduled and later executed on the database, if not
        optimized out.

        Args:
            mutator (django_evolution.mutators.ModelMutator):
                The mutator to perform an operation on.

            model (MockModel):
                The model being mutated.
        """
        field_sig = mutator.model_sig.get_field_sig(self.field_name)
        field = model._meta.get_field(self.field_name)

        for attr_name in six.iterkeys(self.field_attrs):
            if attr_name not in mutator.evolver.supported_change_attrs:
                raise EvolutionNotImplementedError(
                    "ChangeField does not support modifying the '%s' "
                    "attribute on '%s.%s'."
                    % (attr_name, self.model_name, self.field_name))

        new_field_attrs = {}

        for attr_name, attr_value in six.iteritems(self.field_attrs):
            old_attr_value = field_sig.get_attr_value(attr_name)

            # Avoid useless SQL commands if nothing has changed.
            if old_attr_value != attr_value:
                new_field_attrs[attr_name] = {
                    'old_value': old_attr_value,
                    'new_value': attr_value,
                }

        if new_field_attrs:
            mutator.change_column(self, field, new_field_attrs) class RenameModel(BaseModelMutation):
    """A mutation that renames a model."""

    simulation_failure_error = \
        'Cannot rename the model "%(app_label)s.%(model_name)s".'

    def __init__(self, old_model_name, new_model_name, db_table):
        """Initialize the mutation.

        Args:
            old_model_name (unicode):
                The old (existing) name of the model to rename.

            new_model_name (unicode):
                The new name for the model.

            db_table (unicode):
                The table name in the database for this model.
        """
        super(RenameModel, self).__init__(old_model_name)

        self.old_model_name = old_model_name
        self.new_model_name = new_model_name
        self.db_table = db_table

    def get_hint_params(self):
        """Return parameters for the mutation's hinted evolution.

        Returns:
            list of unicode:
            A list of parameter strings to pass to the mutation's constructor
            in a hinted evolution.
        """
        params = [
            self.serialize_value(self.old_model_name),
            self.serialize_value(self.new_model_name),
        ]

        if self.db_table:
            params.append(self.serialize_attr('db_table', self.db_table)),

        return params

    def simulate(self, simulation):
        """Simulate the mutation.

        This will alter the database schema to rename the specified model.

        Args:
            simulation (Simulation):
                The state for the simulation.

        Raises:
            django_evolution.errors.SimulationFailure:
                The simulation failed. The reason is in the exception's
                message.
        """
        app_sig = simulation.get_app_sig()

        model_sig = simulation.get_model_sig(self.old_model_name).clone()
        model_sig.model_name = self.new_model_name
        model_sig.table_name = self.db_table

        app_sig.remove_model_sig(self.old_model_name)
        app_sig.add_model_sig(model_sig)

        old_related_model = '%s.%s' % (simulation.app_label,
                                       self.old_model_name)
        new_related_model = '%s.%s' % (simulation.app_label,
                                       self.new_model_name)

        for cur_app_sig in simulation.project_sig.app_sigs:
            for cur_model_sig in cur_app_sig.model_sigs:
                for cur_field_sig in cur_model_sig.field_sigs:
                    if cur_field_sig.related_model == old_related_model:
                        cur_field_sig.related_model = new_related_model

    def mutate(self, mutator, model):
        """Schedule a model rename on the mutator.

        This will instruct the mutator to rename a model. It will be scheduled
        and later executed on the database, if not optimized out.

        Args:
            mutator (django_evolution.mutators.ModelMutator):
                The mutator to perform an operation on.

            model (MockModel):
                The model being mutated.
        """
        old_model_sig = mutator.model_sig

        new_model_sig = old_model_sig.clone()
        new_model_sig.model_name = self.new_model_name
        new_model_sig.table_name = self.db_table

        new_model = MockModel(project_sig=mutator.project_sig,
                              app_name=mutator.app_label,
                              model_name=self.new_model_name,
                              model_sig=new_model_sig,
                              db_name=mutator.database)

        mutator.add_sql(
            self,
            mutator.evolver.rename_table(new_model,
                                         old_model_sig.table_name,
                                         new_model_sig.table_name)) class MigrationsTestsMixin(object):
    """Mixin for test suites that work with migrations.

    This will ensure that no test migrations are marked as applied before
    the tests run.
    """

    needs_evolution_models = True

    def setUp(self):
        super(MigrationsTestsMixin, self).setUp()

        if supports_migrations:
            connection = connections[DEFAULT_DB_ALIAS]

            for app_label in ('tests', 'migrations_app', 'migrations_app2'):
                unrecord_applied_migrations(connection=connection,
                                            app_label=app_label) class TestCase(DjangoTestCase):
    """Base class for all Django Evolution test cases."""

    ws_re = re.compile(r'\s+')

    needs_evolution_models = False

    # Allow for diffs for large dictionary structures, to help debug
    # signature failures.
    maxDiff = 10000

    # The list of databases we may test against, required by Django 2.2+.
    databases = ['default', 'db_multi']

    default_database_name = 'default'
    sql_mapping_key = None

    builtin_test_app_labels = [
        'evolution_deps_app',
        'evolutions_app',
        'evolutions_app2',
        'migrations_app',
        'migrations_app2',
        'tests',
    ]

    # Override some internal functions on Django's TestCase to avoid setting
    # up transactions. We won't bother documenting these, since we're just
    # trying to turn off logic.
    if django.VERSION[:2] >= (1, 8):
        # Django >= 1.8
        @classmethod
        def _enter_atomics(cls):
            return

        @classmethod
        def _rollback_atomics(cls, atomics):
            return
    else:
        # Django <= 1.7
        def _fixture_setup(self):
            return

        def _fixture_teardown(self):
            return

    def setUp(self):
        super(TestCase, self).setUp()

        # If these are in the database, we need to delete them so we don't
        # carry signatures between tests.
        try:
            Evolution.objects.all().delete()
            Version.objects.all().delete()
        except Exception:
            pass

        # If the test case indicates it needs the Evolution and Version
        # models pre-populated, then do that now.
        if self.needs_evolution_models:
            self.ensure_evolution_models()

    def tearDown(self):
        super(TestCase, self).tearDown()

        self.ensure_deleted_apps()
        unregister_test_models()

    def shortDescription(self):
        """Returns the description of the current test.

        This changes the default behavior to replace all newlines with spaces,
        allowing a test description to span lines. It should still be kept
        short, though.
        """
        doc = self._testMethodDoc

        if doc is not None:
            doc = doc.split('\n\n', 1)[0]
            doc = self.ws_re.sub(' ', doc).strip()

        return doc

    def ensure_evolution_models(self):
        """Create the Evolution and Version models if missing."""
        Evolver()
        assert Version.objects.exists()

    def ensure_deleted_apps(self, app_labels=None):
        """Ensure an app's models and evolutions aren't in the database.

        Args:
            app_labels (list of unicode, optional):
                An explicit list of app labels to delete. This defaults
                to the built-in list of test app labels.
        """
        if app_labels is None:
            app_labels = self.builtin_test_app_labels

        # Do this one-by-one, since we might have some but not all of the
        # apps in the database.
        sql = []

        for app_label in reversed(app_labels):
            try:
                app = get_app(app_label)
            except Exception:
                # This wasn't registered, so we can skip it.
                continue

            sql += sql_delete(app)

        if sql:
            execute_test_sql(sql)

        Evolution.objects.filter(app_label__in=app_labels).delete()

    def ensure_evolved_apps(self, apps):
        """Ensure an app's models and evolutions are applied to the database.

        Args:
            apps (list of module):
                The app modules to evolve.
        """
        evolver = Evolver()

        for app in apps:
            evolver.queue_evolve_app(app)

        evolver.evolve()

    def record_evolutions(self, version, evolutions,
                          database=DEFAULT_DB_ALIAS):
        """Record evolutions in the database.

        This is a convenience around creating and saving `Evolution` models.

        Args:
            version (django_evolution.models.Version):
                The version to associate the evolutions with.

            evolutions (list of tuple):
                The list of evolutions to store. Each item is a tuple in
                ``(app_label, label)`` form.

            database (unicode, optional):
                The name of the database to save these on.
        """
        Evolution.objects.using(database).bulk_create([
            Evolution(version=version,
                      app_label=app_label,
                      label=label)
            for app_label, label in evolutions
        ])

    def record_applied_migrations(self, migration_targets,
                                  database=DEFAULT_DB_ALIAS):
        """Record applied migrations in the database.

        This is a convenience around creating a migration list and then
        recording it in the database.

        Args:
            migration_targets (list of tuple):
                The list of migration targets to store. Each item is a tuple
                in ``(app_label, label)`` form.

            database (unicode, optional):
                The name of the database to save these on.
        """
        assert supports_migrations

        migration_list = MigrationList()
        migration_list.add_migration_targets(migration_targets)

        record_applied_migrations(connection=connections[database],
                                  migrations=migration_list)

    def get_sql_mapping(self, name, sql_mappings_key=None, db_name=None):
        """Return the SQL for the given mapping name and database.

        Args:
            name (unicode):
                The registered name in the list of SQL mappings for this test
                suite and database.

            sql_mappings_key (unicode, optional):
                The key identifying the SQL mappings. This defaults to
                :py:attr:`sql_mapping_key`.

            db_name (unicode, optional):
                The name of the database to return SQL mappings for.

        Returns:
            list of unicode:
            The resulting list of SQL statements.

        Raises:
            ValueError:
                The provided name is not valid.
        """
        sql_mappings_key = sql_mappings_key or self.sql_mapping_key
        assert sql_mappings_key

        db_name = db_name or self.default_database_name
        assert db_name

        sql_mappings = get_sql_mappings(mapping_key=sql_mappings_key,
                                        db_name=db_name)

        try:
            sql = sql_mappings[name]
        except KeyError:
            raise ValueError('"%s" is not a valid SQL mapping name.'
                             % name)

        if isinstance(sql, six.text_type):
            sql = sql.splitlines()

        return sql

    def assertEvolutionsEqual(self, evolutions, expected_evolutions):
        """Assert that a list of evolutions models match expectations.

        Args:
            evolutions (list of django_evolution.models.Evolution):
                The list of evolution models.

            expected_evolutions (list of tuple):
                A list of evolution information to compare against the models.
                Each item is a tuple in ``(app_label, label)`` form.

        Raises:
            AssertionError:
                The lists do not match.
        """
        self.assertEqual(
            [
                (evolution.app_label, evolution.label)
                for evolution in evolutions
            ],
            expected_evolutions)

    def assertAppliedEvolutions(self, expected_evolutions, version=None,
                                database=DEFAULT_DB_ALIAS):
        """Assert that applied evolutions match expectations.

        Args:
            expected_evolutions (list of tuple):
                A list of evolution information to compare against the models.
                Each item is a tuple in ``(app_label, label)`` form.

            version (django_evolution.models.Version, optional):
                The stored project version that the evolutions must be
                associated with. If not provided, all evolutions in the
                database will be used.

            database (unicode, optional):
                The name of the database to query evolutions against. This
                is only used if ``version`` is not provided.

        Raises:
            AssertionError:
                The lists do not match.
        """
        if version is None:
            queryset = Evolution.objects.using(database)
        else:
            queryset = version.evolutions.all()

        applied_evolutions = set(queryset.values_list('app_label', 'label'))

        for app_label, name in expected_evolutions:
            self.assertIn((app_label, name), applied_evolutions)

    def assertAppliedMigrations(self, expected_migration_targets,
                                database=DEFAULT_DB_ALIAS):
        """Assert that applied migrations match expectations.

        Args:
            expected_migration_targets (list of tuple):
                A list of migration targets to compare against the applied
                migrations. Each item is a tuple in ``(app_label, name)``
                form.

            database (unicode, optional):
                The name of the database to query appliedm migrations against.

        Raises:
            AssertionError:
                The lists do not match.
        """
        applied_migrations = MigrationList.from_database(connections[database])

        for app_label, name in expected_migration_targets:
            self.assertTrue(applied_migrations.has_migration_info(
                app_label=app_label,
                name=name))

    def assertSQLMappingEqual(self, sql, sql_mapping_name,
                              sql_mappings_key=None, database=None):
        """Assert generated SQL against database-specific mapped test SQL.

        This will output the provided generated SQL and the expectation test
        SQL mapped by the given key and optional database name, for debugging,
        and will then compare the contents of both.

        The expected SQL may contain regexes, which are used for comparing
        against generated SQL that may depend on some dynamic value pulled from
        the database). If found, the pattern in the regex will be applied to
        the corresponding generated SQL to determine if there's a match. Other
        lines will be compared directly.

        If any part of the SQL does not match, a diff will be shown in the
        test output.

        Args:
            sql (list of unicode):
                The list of generated SQL statements.

            sql_mapping_name (unicode):
                The mapping name in the database-specific test data to compare
                against.

            sql_mappings_key (unicode, optional):
                The key identifying the SQL mappings. This defaults to
                :py:attr:`sql_mapping_key`.

            database (unicode, optional):
                An explicit database name to use for resolving
                ``sql_mapping_name``.

        Raises:
            AssertionError:
                The generated and expected SQL did not match.
        """
        if database is None:
            database = DEFAULT_DB_ALIAS

        # Normalize the generated and expected SQL so that we are
        # guaranteed to have a list with one item per line.
        try:
            generated_sql = '\n'.join(sql).splitlines()
        except TypeError:
            # This probably has an entry that isn't normalized to a flat list
            # of strings. Do that now.
            from django_evolution.utils.sql import SQLExecutor

            with SQLExecutor(database) as sql_executor:
                sql = sql_executor.run_sql(sql,
                                           capture=True,
                                           execute=False)

            generated_sql = '\n'.join(sql).splitlines()

        expected_sql = self.get_sql_mapping(name=sql_mapping_name,
                                            sql_mappings_key=sql_mappings_key,
                                            db_name=database)

        # Output the statements one-by-one, to help with diagnosing
        # differences.

        print()
        print("** Comparing SQL against '%s'" % sql_mapping_name)
        print('** Generated:')
        print()

        for line in generated_sql:
            print('    %s' % line)

        print()
        print('** Expected:')
        print()

        has_regex = False

        for line in expected_sql:
            if (not isinstance(line, six.text_type) and
                hasattr(line, 'pattern')):
                line = '/%s/' % line.pattern
                has_regex = True

            print('    %s' % line)

        print()

        if has_regex:
            # We can't compare directly at first, so let's see if things
            # are otherwise a match and then, if we spot anything wrong,
            # we'll just do an assertListEqual to get detailed output.
            match = (len(generated_sql) == len(expected_sql))

            if match:
                for gen_line, expected_line in zip(generated_sql,
                                                   expected_sql):
                    if ((isinstance(expected_line, six.text_type) and
                         gen_line != expected_line) or
                        (hasattr(line, 'pattern') and
                         not line.match(gen_line))):
                        match = False
                        break

            if not match:
                # Now show that detailed output.
                self.assertListEqual(generated_sql, expected_sql)
        else:
            # Compare as lists, so that we can better spot inconsistencies.
            self.assertListEqual(generated_sql, expected_sql) def get_app_pending_mutations(app, evolution_labels=[], mutations=None,
                              old_project_sig=None, project_sig=None,
                              database=DEFAULT_DB_ALIAS):
    """Return an app's pending mutations provided by the given evolution names.

    This is similar to :py:meth:`get_app_mutations`, but filters the list
    of mutations down to remove any that are unnecessary (ones that do not
    operate on changed parts of the project signature).

    Args:
        app (module):
            The app the evolutions belong to.

        evolution_labels (list of unicode, optional):
            The labels of the evolutions to return mutations for.

            If ``None``, this will factor in all evolution labels for the
            app.

        mutations (list of django_evolution.mutations.BaseMutation, optional):
            An explicit list of mutations to use. If provided,
            ``evolution_labels`` will be ignored.

        old_project_sig (django_evolution.signature.ProjectSignature, optional):
            A pre-fetched old project signature. If provided, this will be
            used instead of the latest one in the database.

        project_sig (django_evolution.signature.ProjectSignature, optional):
            A project signature representing the current state of the database.
            If provided, this will be used instead of generating a new one
            from the current database state.

        database (unicode, optional):
            The name of the database the evolutions cover.

    Returns:
        list of django_evolution.mutations.BaseMutation:
        The list of mutations provided by the evolutions.

    Raises:
        django_evolution.errors.EvolutionException:
            One or more evolutions are missing.
    """
    # Avoids a nasty circular import. Util modules should always be
    # importable, so we compensate here.
    from django_evolution.models import Version
    from django_evolution.mutations import RenameModel
    from django_evolution.signature import ProjectSignature

    if mutations is None:
        mutations = get_app_mutations(app=app,
                                      evolution_labels=evolution_labels,
                                      database=database)

    if old_project_sig is None:
        latest_version = Version.objects.current_version(using=database)
        old_project_sig = latest_version.signature

    if project_sig is None:
        project_sig = ProjectSignature.from_database(database)

    app_id = get_app_label(app)
    old_app_sig = old_project_sig.get_app_sig(app_id)
    app_sig = project_sig.get_app_sig(app_id)

    if old_app_sig is not None and app_sig is not None:
        # We want to go through now and make sure we're only applying
        # evolutions for models where the signature is different between
        # what's stored and what's current.
        #
        # The reason for this is that we may have just installed a baseline,
        # which would have the up-to-date signature, and we might be trying
        # to apply evolutions on top of that (which would already be applied).
        # Or we may be working with models that weren't present in the old
        # signature and will soon be added. In either case, these would
        # generate errors. So, try hard to prevent that.
        #
        # First, Find the list of models in the latest signature of this app
        # that aren't in the old signature. If a model signature isn't found
        # in the old app signature, it's a new model, and we don't want to
        # try to apply evolutions to it.
        changed_models = set(
            model_sig.model_name
            for model_sig in app_sig.model_sigs
            if old_app_sig.get_model_sig(model_sig.model_name) not in (
                None, model_sig)
        )

        # Now do the same for models in the old signature, in case the
        # model has been deleted.
        changed_models.update(
            old_model_sig.model_name
            for old_model_sig in old_app_sig.model_sigs
            if app_sig.get_model_sig(old_model_sig.model_name) is None
        )

        # We should now have a full list of which models changed. Filter
        # the list of mutations appropriately.
        #
        # Changes affecting a model that was newly-introduced are removed,
        # unless the mutation is a RenameModel, in which case we'll need it
        # during the optimization step (and will remove it if necessary then).
        mutations = [
            mutation
            for mutation in mutations
            if (not hasattr(mutation, 'model_name') or
                mutation.model_name in changed_models or
                isinstance(mutation, RenameModel))
        ]

    return mutations def get_app_upgrade_info(app, scan_evolutions=True, simulate_applied=False,
                         database=None):
    """Return the upgrade information to use for a given app.

    This will determine if the app should be using Django Evolution or
    Django Migrations for any schema upgrades.

    If an ``evolutions`` module is found, then this will determine the method
    to be :py:attr:`UpgradeMethod.EVOLUTIONS
    <django_evolution.consts.UpgradeMethod.EVOLUTIONS>`, unless the app has
    been moved over to using Migrations.

    If instead there's a ``migrations`` module, then this will determine
    the method to be :py:attr:`UpgradeMethod.MIGRATIONS
    <django_evolution.consts.UpgradeMethod.MIGRATIONS>`.

    Otherwise, this will return ``None``, indicating that no established
    method has been chosen. This allows a determination to be made later,
    based on the Django version or the consumer's choice.

    Note that this may return that migrations are the preferred method for
    an app even on versions of Django that do not support migrations. It's
    up to the caller to handle this however it chooses.

    Args:
        app (module):
            The app module to determine the upgrade method for.

        scan_evolutions (bool, optional):
            Whether to scan evolutions for the app to determine the current
            upgrade method.

        simulate_applied (bool, optional):
            Return the upgrade method based on the state of the app if all
            mutations had been applied. This is useful for generating end
            state signatures.

            This is ignored if passing ``scan_evolutions=False``.

        database (unicode, optional):
            The database to use for accessing stored evolution and migration
            information.

    Returns:
        dict:
        A dictionary of information containing the following keys:

        ``applied_migrations`` (:py:class:`~django_evolution.utils.migrations.MigrationList`):
            A list of migrations that have been applied to this app through
            any mutations. This will only be present if the upgrade method is
            set to use migrations and if running on a version of Django that
            supports migrations.

        ``has_evolutions`` (:py:class:`bool`):
            Whether there are any evolutions for this app. This may come from
            the app, project, or Django Evolution.

        ``has_migrations`` (:py:class:`bool`):
            Whether there are any migrations for this app.

        ``upgrade_method`` (:py:class:`unicode`):
            The upgrade method. This will be a value from
            :py:class:`~django_evolution.consts.UpgradeMethod`, or ``None``
            if a clear determination could not be made.
    """
    # Avoids a nasty circular import. Util modules should always be
    # importable, so we compensate here.
    from django_evolution.mutations import MoveToDjangoMigrations

    upgrade_method = None
    applied_migrations = None
    has_evolutions = has_evolutions_module(app)
    has_migrations = has_migrations_module(app)

    if has_evolutions:
        # This app made use of Django Evolution. See if we're still using
        # that, or if it's handed control over to Django migrations.
        if scan_evolutions:
            if simulate_applied:
                evolutions = None
            else:
                evolutions = get_applied_evolutions(app,
                                                    database=database)

            mutations = get_app_mutations(app=app,
                                          evolution_labels=evolutions)

            for mutation in reversed(mutations):
                if isinstance(mutation, MoveToDjangoMigrations):
                    app_label = get_app_label(app)
                    upgrade_method = UpgradeMethod.MIGRATIONS

                    applied_migrations = MigrationList()

                    for name in mutation.mark_applied:
                        applied_migrations.add_migration_info(
                            app_label=app_label,
                            name=name)

                    break

        if not upgrade_method:
            upgrade_method = UpgradeMethod.EVOLUTIONS

    if has_migrations:
        if not upgrade_method:
            upgrade_method = UpgradeMethod.MIGRATIONS

        if supports_migrations:
            applied_migrations = MigrationList.from_database(
                connection=connections[database or DEFAULT_DB_ALIAS],
                app_label=get_app_label(app))

            if not applied_migrations:
                applied_migrations = None

    return {
        'applied_migrations': applied_migrations,
        'has_evolutions': has_evolutions,
        'has_migrations': has_migrations,
        'upgrade_method': upgrade_method,
    } def get_applied_evolutions(app, database=DEFAULT_DB_ALIAS):
    """Return the list of labels for applied evolutions for a Django app.

    Args:
        app (module):
            The app to return evolutions for.

        database (unicode, optional):
            The name of the database containing the
            :py:class:`~django_evolution.models.Evolution` entries.

    Returns:
        list of unicode:
        The labels of evolutions that have been applied.
    """
    # Avoids a nasty circular import. Util modules should always be
    # importable, so we compensate here.
    from django_evolution.models import Evolution

    return list(
        Evolution.objects
        .using(database)
        .filter(app_label=get_app_label(app))
        .values_list('label', flat=True)
    ) def get_evolution_app_dependencies(app):
    """Return dependencies governing all evolutions for an app.

    These dependencies are defined in an :file:`evolutions/__init__.py` file,
    and will ensure that other evolutions or migrations apply either before
    or after the app's evolutions.

    Dependencies are generally specified as a tuple of ``(app_label, name)``,
    where ``name`` is either a migration name or an evolution label.

    Dependencies on evolutions can also be specified as simply a string
    containing an app label, which will reference the sequence of evolutions
    as a whole for that app.

    Version Added:
        2.1

    Args:
        app (module):
            The app the evolution is for.

    Returns:
        dict:
        A dictionary of dependency information for the app. This has the
        following keys:

        * ``before_migrations``
        * ``after_migrations``
        * ``before_evolutions``
        * ``after_evolutions``

        If the evolutions module was not found, this will return ``None``
        instead.
    """
    module = get_evolutions_module(app)

    if not module:
        return None

    return {
        'after_evolutions': set(getattr(module, 'AFTER_EVOLUTIONS', [])),
        'after_migrations': set(getattr(module, 'AFTER_MIGRATIONS', [])),
        'before_evolutions': set(getattr(module, 'BEFORE_EVOLUTIONS', [])),
        'before_migrations': set(getattr(module, 'BEFORE_MIGRATIONS', [])),
    } def get_evolution_dependencies(app, evolution_label):
    """Return dependencies for an evolution.

    Evolutions can depend on other evolutions or migrations, and can be
    marked as being a dependency of them as well (forcing the evolution to
    apply before another evolution/migration).

    Dependencies are generally specified as a tuple of ``(app_label, name)``,
    where ``name`` is either a migration name or an evolution label.

    Dependencies on evolutions can also be specified as simply a string
    containing an app label, which will reference the sequence of evolutions
    as a whole for that app.

    Version Added:
        2.1

    Args:
        app (module):
            The app the evolution is for.

        evolution_label (unicode):
            The label identifying the evolution for the app.

    Returns:
        dict:
        A dictionary of dependency information for the evolution. This has
        the following keys:

        * ``before_migrations``
        * ``after_migrations``
        * ``before_evolutions``
        * ``after_evolutions``

        If the evolution module was not found, this will return ``None``
        instead.
    """
    module = get_evolution_module(app=app,
                                  evolution_label=evolution_label)

    if not module:
        return None

    deps = {
        'after_evolutions': set(getattr(module, 'AFTER_EVOLUTIONS', [])),
        'after_migrations': set(getattr(module, 'AFTER_MIGRATIONS', [])),
        'before_evolutions': set(getattr(module, 'BEFORE_EVOLUTIONS', [])),
        'before_migrations': set(getattr(module, 'BEFORE_MIGRATIONS', [])),
    }

    app_label = get_app_label(app)

    # Check if any mutations have dependencies to inject.
    for mutation in getattr(module, 'MUTATIONS', []):
        mutation_deps = mutation.generate_dependencies(app_label=app_label)

        if mutation_deps:
            for key, value in six.iteritems(mutation_deps):
                assert key in deps, (
                    '"%s" is not a valid dependency key from mutation %r'
                    % (key, mutation))

                deps[key].update(value)

    return deps def get_evolution_module(app, evolution_label):
    """Return the module for a given evolution for an app.

    Version Added:
        2.1

    Args:
        app (module):
            The app.

        evolution_label (unicode):
            The label of the evolution.

    Returns:
        module:
        The evolution module, or ``None`` if it could not be found.
    """
    try:
        return import_module('%s.%s' % (get_evolutions_module_name(app),
                                        evolution_label))
    except ImportError:
        return None def get_evolution_sequence(app):
    """Return the list of evolution labels for a Django app.

    Args:
        app (module):
            The app to return evolutions for.

    Returns:
        list of unicode:
        The list of evolution labels.
    """
    app_name = get_app_name(app)

    if app_name in BUILTIN_SEQUENCES:
        return BUILTIN_SEQUENCES[app_name]

    module = get_evolutions_module(app)

    if module is not None:
        return module.SEQUENCE

    return [] def get_evolutions_module(app):
    """Return the evolutions module for an app.

    Args:
        app (module):
            The app.

    Returns:
        module:
        The evolutions module for the app, or ``None`` if it could not be
        found.
    """
    try:
        return import_module(get_evolutions_module_name(app))
    except ImportError:
        return None def get_evolutions_module_name(app):
    """Return the name of the evolutions module for an app.

    Version Added:
        2.1

    Args:
        app (module):
            The app.

    Returns:
        unicode:
        The name of the evolutions module for the app. This is not guaranteed
        to be importable.
    """
    app_name = get_app_name(app)

    if app_name in BUILTIN_SEQUENCES:
        module_name = 'django_evolution.builtin_evolutions'
    elif app_name in getattr(settings, 'CUSTOM_EVOLUTIONS', {}):
        module_name = settings.CUSTOM_EVOLUTIONS[app_name]
    else:
        module_name = '%s.evolutions' % app_name

    return module_name def get_evolutions_path(app):
    """Return the evolutions path for an app.

    Args:
        app (module):
            The app.

    Returns:
        str:
        The path to the evolutions module for the app, or ``None`` if it
        could not be found.
    """
    module = get_evolutions_module(app)

    if module:
        return os.path.dirname(module.__file__)

    return None def get_evolutions_source(app):
    """Return the source for evolutions.

    This is used to determine where evolutions are coming from. They can be
    provided by the app, project, or built into Django Evolution.

    Args:
        app (module):
            The app module.

    Returns:
        unicode:
        The evolution source. This is an entry from
        :py:class:`~django_evolution.consts.EvolutionsSource`.
    """
    app_name = get_app_name(app)

    if app_name in BUILTIN_SEQUENCES:
        return EvolutionsSource.BUILTIN
    elif app_name in getattr(settings, 'CUSTOM_EVOLUTIONS', {}):
        return EvolutionsSource.PROJECT
    else:
        return EvolutionsSource.APP
import os
import django_evolution
from django.db import models
from django_evolution.compat.apps import get_app
from django_evolution.consts import EvolutionsSource, UpgradeMethod
from django_evolution.models import Version
from django_evolution.mutations import AddField, ChangeField, RenameModel
from django_evolution.support import supports_migrations
from django_evolution.tests.base_test_case import (MigrationsTestsMixin,
                                                   TestCase)
from django_evolution.utils.evolutions import (get_app_pending_mutations,
                                               get_app_upgrade_info,
                                               get_applied_evolutions,
                                               get_evolution_app_dependencies,
                                               get_evolution_dependencies,
                                               get_evolution_module,
                                               get_evolution_sequence,
                                               get_evolutions_module,
                                               get_evolutions_module_name,
                                               get_evolutions_path,
                                               get_evolutions_source)
        from django_evolution import evolutions
        from django_evolution import builtin_evolutions
        from django_evolution.tests.evolutions_app import evolutions
        from django_evolution.tests.evolutions_app.evolutions import \
            first_evolution
        from django_evolution.builtin_evolutions import auth_delete_message
        from django_evolution.tests.evolutions_app.evolutions import \
            first_evolution
                'after_migrations': {
                    ('migrations_app', '0001_initial'),
                },
                'before_evolutions': {
                    'evolutions_app2',
                    ('evolutions_app2', 'second_evolution'),
                },
                'before_migrations': {
                    ('migrations_app2', '0002_add_field'),
                },
            })

    def test_without_dependencies(self):
        """Testing get_evolution_app_dependencies without dependencies"""
        self.assertEqual(
            get_evolution_app_dependencies(get_app('evolution_deps_app')),
            {
                'after_evolutions': set(),
                'after_migrations': set(),
                'before_evolutions': set(),
                'before_migrations': set(),
            })

    def test_with_invalid_app(self):
        """Testing get_evolution_app_dependencies with non-evolution app"""
        self.assertIsNone(
            get_evolution_app_dependencies(get_app('migrations_app')))


class GetEvolutionDependenciesTests(TestCase):
    """Unit tests for get_evolution_dependencies."""

    def test_with_dependencies(self):
        """Testing get_evolution_dependencies with dependencies"""
        self.assertEqual(
            get_evolution_dependencies(get_app('evolution_deps_app'),
                                       'test_evolution'),
            {
                'after_evolutions': {
                    'evolutions_app',
                    ('evolutions_app', 'first_evolution'),
                },
                'after_migrations': {
                    ('migrations_app', '0001_initial'),
                },
                'before_evolutions': {
                    'evolutions_app2',
                    ('evolutions_app2', 'second_evolution'),
                },
                'before_migrations': {
                    ('migrations_app2', '0002_add_field'),
                },
            })

    def test_without_dependencies(self):
        """Testing get_evolution_dependencies without dependencies"""
        self.assertEqual(
            get_evolution_dependencies(get_app('app_deps_app'),
                                       'test_evolution'),
            {
                'after_evolutions': set(),
                'after_migrations': set(),
                'before_evolutions': set(),
                'before_migrations': set(),
            })

    def test_with_move_to_django_migrations(self):
        """Testing get_evolution_dependencies with MoveToDjangoMigrations
        mutation
        """
        self.assertEqual(
            get_evolution_dependencies(get_app('admin'),
                                       'admin_move_to_migrations'),
            {
                'after_evolutions': set(),
                'after_migrations': {
                    ('admin', '0001_initial'),
                },
                'before_evolutions': set(),
                'before_migrations': set(),
            })

    def test_with_invalid_app(self):
        """Testing get_evolution_dependencies with non-evolution app"""
        self.assertIsNone(
            get_evolution_dependencies(app=get_app('migrations_app'),
                                       evolution_label='invalid_evolution'))

    def test_with_invalid_evolution(self):
        """Testing get_evolution_dependencies with invalid evolution name"""
        self.assertIsNone(
            get_evolution_dependencies(app=get_app('django_evolution'),
                                       evolution_label='invalid_evolution'))


class GetEvolutionsSequenceTests(TestCase):
    """Unit tests for get_evolution_sequence."""

    def test_with_app(self):
        """Testing get_evolution_sequence with app-provided evolutions"""
        self.assertEqual(get_evolution_sequence(get_app('evolutions_app')),
                         ['first_evolution', 'second_evolution'])

    def test_with_builtin(self):
        """Testing get_evolution_sequence with built-in evolutions"""
        self.assertEqual(
            get_evolution_sequence(get_app('contenttypes')),
            [
                'contenttypes_unique_together_baseline',
                'contenttypes_move_to_migrations',
            ])

    def test_with_project(self):
        """Testing get_evolution_sequence with project-provided evolutions"""
        custom_evolutions = {
            'django_evolution.tests.migrations_app':
                'django_evolution.tests.evolutions_app.evolutions',
        }

        with self.settings(CUSTOM_EVOLUTIONS=custom_evolutions):

            self.assertEqual(get_evolution_sequence(get_app('migrations_app')),