Fixed #24100 -- Made the migration signals dispatch its plan and apps.
Thanks Markus for your contribution and Tim for your review.
This commit is contained in:
parent
e475e84970
commit
f937c9ec97
|
@ -15,7 +15,7 @@ from django.db import DEFAULT_DB_ALIAS, connections, router, transaction
|
||||||
from django.db.migrations.autodetector import MigrationAutodetector
|
from django.db.migrations.autodetector import MigrationAutodetector
|
||||||
from django.db.migrations.executor import MigrationExecutor
|
from django.db.migrations.executor import MigrationExecutor
|
||||||
from django.db.migrations.loader import AmbiguityError
|
from django.db.migrations.loader import AmbiguityError
|
||||||
from django.db.migrations.state import ProjectState
|
from django.db.migrations.state import ModelState, ProjectState
|
||||||
from django.utils.module_loading import module_has_submodule
|
from django.utils.module_loading import module_has_submodule
|
||||||
|
|
||||||
|
|
||||||
|
@ -160,7 +160,10 @@ class Command(BaseCommand):
|
||||||
% (targets[0][1], targets[0][0])
|
% (targets[0][1], targets[0][0])
|
||||||
)
|
)
|
||||||
|
|
||||||
emit_pre_migrate_signal(self.verbosity, self.interactive, connection.alias)
|
pre_migrate_apps = executor._create_project_state().apps
|
||||||
|
emit_pre_migrate_signal(
|
||||||
|
self.verbosity, self.interactive, connection.alias, apps=pre_migrate_apps, plan=plan,
|
||||||
|
)
|
||||||
|
|
||||||
# Run the syncdb phase.
|
# Run the syncdb phase.
|
||||||
if run_syncdb:
|
if run_syncdb:
|
||||||
|
@ -191,14 +194,33 @@ class Command(BaseCommand):
|
||||||
"migrations, and then re-run 'manage.py migrate' to "
|
"migrations, and then re-run 'manage.py migrate' to "
|
||||||
"apply them."
|
"apply them."
|
||||||
))
|
))
|
||||||
|
post_migrate_apps = pre_migrate_apps
|
||||||
else:
|
else:
|
||||||
fake = options['fake']
|
fake = options['fake']
|
||||||
fake_initial = options['fake_initial']
|
fake_initial = options['fake_initial']
|
||||||
executor.migrate(targets, plan, fake=fake, fake_initial=fake_initial)
|
post_migrate_project_state = executor.migrate(
|
||||||
|
targets, plan, fake=fake, fake_initial=fake_initial
|
||||||
|
)
|
||||||
|
post_migrate_apps = post_migrate_project_state.apps
|
||||||
|
|
||||||
|
# Re-render models of real apps to include relationships now that
|
||||||
|
# we've got a final state. This wouldn't be necessary if real apps
|
||||||
|
# models were rendered with relationships in the first place.
|
||||||
|
with post_migrate_apps.bulk_update():
|
||||||
|
model_keys = []
|
||||||
|
for model_state in post_migrate_apps.real_models:
|
||||||
|
model_key = model_state.app_label, model_state.name_lower
|
||||||
|
model_keys.append(model_key)
|
||||||
|
post_migrate_apps.unregister_model(*model_key)
|
||||||
|
post_migrate_apps.render_multiple([
|
||||||
|
ModelState.from_model(apps.get_model(*model_key)) for model_key in model_keys
|
||||||
|
])
|
||||||
|
|
||||||
# Send the post_migrate signal, so individual apps can do whatever they need
|
# Send the post_migrate signal, so individual apps can do whatever they need
|
||||||
# to do at this point.
|
# to do at this point.
|
||||||
emit_post_migrate_signal(self.verbosity, self.interactive, connection.alias)
|
emit_post_migrate_signal(
|
||||||
|
self.verbosity, self.interactive, connection.alias, apps=post_migrate_apps, plan=plan,
|
||||||
|
)
|
||||||
|
|
||||||
def migration_progress_callback(self, action, migration=None, fake=False):
|
def migration_progress_callback(self, action, migration=None, fake=False):
|
||||||
if self.verbosity >= 1:
|
if self.verbosity >= 1:
|
||||||
|
|
|
@ -20,7 +20,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True, allow_
|
||||||
return statements
|
return statements
|
||||||
|
|
||||||
|
|
||||||
def emit_pre_migrate_signal(verbosity, interactive, db):
|
def emit_pre_migrate_signal(verbosity, interactive, db, **kwargs):
|
||||||
# Emit the pre_migrate signal for every application.
|
# Emit the pre_migrate signal for every application.
|
||||||
for app_config in apps.get_app_configs():
|
for app_config in apps.get_app_configs():
|
||||||
if app_config.models_module is None:
|
if app_config.models_module is None:
|
||||||
|
@ -32,10 +32,12 @@ def emit_pre_migrate_signal(verbosity, interactive, db):
|
||||||
app_config=app_config,
|
app_config=app_config,
|
||||||
verbosity=verbosity,
|
verbosity=verbosity,
|
||||||
interactive=interactive,
|
interactive=interactive,
|
||||||
using=db)
|
using=db,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def emit_post_migrate_signal(verbosity, interactive, db):
|
def emit_post_migrate_signal(verbosity, interactive, db, **kwargs):
|
||||||
# Emit the post_migrate signal for every application.
|
# Emit the post_migrate signal for every application.
|
||||||
for app_config in apps.get_app_configs():
|
for app_config in apps.get_app_configs():
|
||||||
if app_config.models_module is None:
|
if app_config.models_module is None:
|
||||||
|
@ -47,4 +49,6 @@ def emit_post_migrate_signal(verbosity, interactive, db):
|
||||||
app_config=app_config,
|
app_config=app_config,
|
||||||
verbosity=verbosity,
|
verbosity=verbosity,
|
||||||
interactive=interactive,
|
interactive=interactive,
|
||||||
using=db)
|
using=db,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
|
@ -63,6 +63,9 @@ class MigrationExecutor(object):
|
||||||
applied.add(migration)
|
applied.add(migration)
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
|
def _create_project_state(self):
|
||||||
|
return ProjectState(real_apps=list(self.loader.unmigrated_apps))
|
||||||
|
|
||||||
def migrate(self, targets, plan=None, fake=False, fake_initial=False):
|
def migrate(self, targets, plan=None, fake=False, fake_initial=False):
|
||||||
"""
|
"""
|
||||||
Migrates the database up to the given targets.
|
Migrates the database up to the given targets.
|
||||||
|
@ -79,7 +82,9 @@ class MigrationExecutor(object):
|
||||||
all_backwards = all(backwards for mig, backwards in plan)
|
all_backwards = all(backwards for mig, backwards in plan)
|
||||||
|
|
||||||
if not plan:
|
if not plan:
|
||||||
pass # Nothing to do for an empty plan
|
# Nothing to do for an empty plan, except for building the post
|
||||||
|
# migrate project state
|
||||||
|
state = self._create_project_state()
|
||||||
elif all_forwards == all_backwards:
|
elif all_forwards == all_backwards:
|
||||||
# This should only happen if there's a mixed plan
|
# This should only happen if there's a mixed plan
|
||||||
raise InvalidMigrationPlan(
|
raise InvalidMigrationPlan(
|
||||||
|
@ -89,21 +94,27 @@ class MigrationExecutor(object):
|
||||||
plan
|
plan
|
||||||
)
|
)
|
||||||
elif all_forwards:
|
elif all_forwards:
|
||||||
self._migrate_all_forwards(plan, full_plan, fake=fake, fake_initial=fake_initial)
|
state = self._migrate_all_forwards(plan, full_plan, fake=fake, fake_initial=fake_initial)
|
||||||
else:
|
else:
|
||||||
# No need to check for `elif all_backwards` here, as that condition
|
# No need to check for `elif all_backwards` here, as that condition
|
||||||
# would always evaluate to true.
|
# would always evaluate to true.
|
||||||
self._migrate_all_backwards(plan, full_plan, fake=fake)
|
state = self._migrate_all_backwards(plan, full_plan, fake=fake)
|
||||||
|
|
||||||
self.check_replacements()
|
self.check_replacements()
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
def _migrate_all_forwards(self, plan, full_plan, fake, fake_initial):
|
def _migrate_all_forwards(self, plan, full_plan, fake, fake_initial):
|
||||||
"""
|
"""
|
||||||
Take a list of 2-tuples of the form (migration instance, False) and
|
Take a list of 2-tuples of the form (migration instance, False) and
|
||||||
apply them in the order they occur in the full_plan.
|
apply them in the order they occur in the full_plan.
|
||||||
"""
|
"""
|
||||||
migrations_to_run = {m[0] for m in plan}
|
migrations_to_run = {m[0] for m in plan}
|
||||||
state = ProjectState(real_apps=list(self.loader.unmigrated_apps))
|
state = self._create_project_state()
|
||||||
|
applied_migrations = {
|
||||||
|
self.loader.graph.nodes[key] for key in self.loader.applied_migrations
|
||||||
|
if key in self.loader.graph.nodes
|
||||||
|
}
|
||||||
for migration, _ in full_plan:
|
for migration, _ in full_plan:
|
||||||
if not migrations_to_run:
|
if not migrations_to_run:
|
||||||
# We remove every migration that we applied from this set so
|
# We remove every migration that we applied from this set so
|
||||||
|
@ -120,9 +131,14 @@ class MigrationExecutor(object):
|
||||||
self.progress_callback("render_success")
|
self.progress_callback("render_success")
|
||||||
state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
|
state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
|
||||||
migrations_to_run.remove(migration)
|
migrations_to_run.remove(migration)
|
||||||
else:
|
elif migration in applied_migrations:
|
||||||
|
# Only mutate the state if the migration is actually applied
|
||||||
|
# to make sure the resulting state doesn't include changes
|
||||||
|
# from unrelated migrations.
|
||||||
migration.mutate_state(state, preserve=False)
|
migration.mutate_state(state, preserve=False)
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
def _migrate_all_backwards(self, plan, full_plan, fake):
|
def _migrate_all_backwards(self, plan, full_plan, fake):
|
||||||
"""
|
"""
|
||||||
Take a list of 2-tuples of the form (migration instance, True) and
|
Take a list of 2-tuples of the form (migration instance, True) and
|
||||||
|
@ -136,7 +152,11 @@ class MigrationExecutor(object):
|
||||||
migrations_to_run = {m[0] for m in plan}
|
migrations_to_run = {m[0] for m in plan}
|
||||||
# Holds all migration states prior to the migrations being unapplied
|
# Holds all migration states prior to the migrations being unapplied
|
||||||
states = {}
|
states = {}
|
||||||
state = ProjectState(real_apps=list(self.loader.unmigrated_apps))
|
state = self._create_project_state()
|
||||||
|
applied_migrations = {
|
||||||
|
self.loader.graph.nodes[key] for key in self.loader.applied_migrations
|
||||||
|
if key in self.loader.graph.nodes
|
||||||
|
}
|
||||||
if self.progress_callback:
|
if self.progress_callback:
|
||||||
self.progress_callback("render_start")
|
self.progress_callback("render_start")
|
||||||
for migration, _ in full_plan:
|
for migration, _ in full_plan:
|
||||||
|
@ -154,13 +174,31 @@ class MigrationExecutor(object):
|
||||||
# The old state keeps as-is, we continue with the new state
|
# The old state keeps as-is, we continue with the new state
|
||||||
state = migration.mutate_state(state, preserve=True)
|
state = migration.mutate_state(state, preserve=True)
|
||||||
migrations_to_run.remove(migration)
|
migrations_to_run.remove(migration)
|
||||||
else:
|
elif migration in applied_migrations:
|
||||||
|
# Only mutate the state if the migration is actually applied
|
||||||
|
# to make sure the resulting state doesn't include changes
|
||||||
|
# from unrelated migrations.
|
||||||
migration.mutate_state(state, preserve=False)
|
migration.mutate_state(state, preserve=False)
|
||||||
if self.progress_callback:
|
if self.progress_callback:
|
||||||
self.progress_callback("render_success")
|
self.progress_callback("render_success")
|
||||||
|
|
||||||
for migration, _ in plan:
|
for migration, _ in plan:
|
||||||
self.unapply_migration(states[migration], migration, fake=fake)
|
self.unapply_migration(states[migration], migration, fake=fake)
|
||||||
|
applied_migrations.remove(migration)
|
||||||
|
|
||||||
|
# Generate the post migration state by starting from the state before
|
||||||
|
# the last migration is unapplied and mutating it to include all the
|
||||||
|
# remaining applied migrations.
|
||||||
|
last_unapplied_migration = plan[-1][0]
|
||||||
|
state = states[last_unapplied_migration]
|
||||||
|
for index, (migration, _) in enumerate(full_plan):
|
||||||
|
if migration == last_unapplied_migration:
|
||||||
|
for migration, _ in full_plan[index:]:
|
||||||
|
if migration in applied_migrations:
|
||||||
|
migration.mutate_state(state, preserve=False)
|
||||||
|
break
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
def collect_sql(self, plan):
|
def collect_sql(self, plan):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -65,5 +65,5 @@ m2m_changed = ModelSignal(
|
||||||
use_caching=True,
|
use_caching=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
pre_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using"])
|
pre_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using", "apps", "plan"])
|
||||||
post_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using"])
|
post_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using", "apps", "plan"])
|
||||||
|
|
|
@ -406,6 +406,23 @@ Arguments sent with this signal:
|
||||||
``using``
|
``using``
|
||||||
The alias of database on which a command will operate.
|
The alias of database on which a command will operate.
|
||||||
|
|
||||||
|
``plan``
|
||||||
|
.. versionadded:: 1.10
|
||||||
|
|
||||||
|
The migration plan that is going to be used for the migration run. While
|
||||||
|
the plan is not public API, this allows for the rare cases when it is
|
||||||
|
necessary to know the plan. A plan is a list of two-tuples with the first
|
||||||
|
item being the instance of a migration class and the second item showing
|
||||||
|
if the migration was rolled back (``True``) or applied (``False``).
|
||||||
|
|
||||||
|
``apps``
|
||||||
|
.. versionadded:: 1.10
|
||||||
|
|
||||||
|
An instance of :data:`Apps <django.apps>` containing the state of the
|
||||||
|
project before the migration run. It should be used instead of the global
|
||||||
|
:attr:`apps <django.apps.apps>` registry to retrieve the models you
|
||||||
|
want to perform operations on.
|
||||||
|
|
||||||
``post_migrate``
|
``post_migrate``
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
@ -448,6 +465,23 @@ Arguments sent with this signal:
|
||||||
The database alias used for synchronization. Defaults to the ``default``
|
The database alias used for synchronization. Defaults to the ``default``
|
||||||
database.
|
database.
|
||||||
|
|
||||||
|
``plan``
|
||||||
|
.. versionadded:: 1.10
|
||||||
|
|
||||||
|
The migration plan that was used for the migration run. While the plan is
|
||||||
|
not public API, this allows for the rare cases when it is necessary to
|
||||||
|
know the plan. A plan is a list of two-tuples with the first item being
|
||||||
|
the instance of a migration class and the second item showing if the
|
||||||
|
migration was rolled back (``True``) or applied (``False``).
|
||||||
|
|
||||||
|
``apps``
|
||||||
|
.. versionadded:: 1.10
|
||||||
|
|
||||||
|
An instance of :data:`Apps <django.apps.apps>` containing the state of the
|
||||||
|
project after the migration run. It should be used instead of the global
|
||||||
|
:attr:`apps <django.apps.apps>` registry to retrieve the models you
|
||||||
|
want to perform operations on.
|
||||||
|
|
||||||
For example, you could register a callback in an
|
For example, you could register a callback in an
|
||||||
:class:`~django.apps.AppConfig` like this::
|
:class:`~django.apps.AppConfig` like this::
|
||||||
|
|
||||||
|
|
|
@ -368,6 +368,10 @@ Migrations
|
||||||
migration history. If they find some unapplied dependencies of an applied
|
migration history. If they find some unapplied dependencies of an applied
|
||||||
migration, ``InconsistentMigrationHistory`` is raised.
|
migration, ``InconsistentMigrationHistory`` is raised.
|
||||||
|
|
||||||
|
* The :func:`~django.db.models.signals.pre_migrate` and
|
||||||
|
:func:`~django.db.models.signals.post_migrate` signals now dispatch their
|
||||||
|
migration ``plan`` and ``apps``.
|
||||||
|
|
||||||
Models
|
Models
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
"Signal",
|
||||||
|
[
|
||||||
|
("id", models.AutoField(primary_key=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,11 +1,12 @@
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core import management
|
from django.core import management
|
||||||
|
from django.db import migrations
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
APP_CONFIG = apps.get_app_config('migrate_signals')
|
APP_CONFIG = apps.get_app_config('migrate_signals')
|
||||||
SIGNAL_ARGS = ['app_config', 'verbosity', 'interactive', 'using']
|
SIGNAL_ARGS = ['app_config', 'verbosity', 'interactive', 'using', 'plan', 'apps']
|
||||||
MIGRATE_DATABASE = 'default'
|
MIGRATE_DATABASE = 'default'
|
||||||
MIGRATE_VERBOSITY = 1
|
MIGRATE_VERBOSITY = 1
|
||||||
MIGRATE_INTERACTIVE = False
|
MIGRATE_INTERACTIVE = False
|
||||||
|
@ -80,6 +81,8 @@ class MigrateSignalTests(TestCase):
|
||||||
self.assertEqual(args['verbosity'], MIGRATE_VERBOSITY)
|
self.assertEqual(args['verbosity'], MIGRATE_VERBOSITY)
|
||||||
self.assertEqual(args['interactive'], MIGRATE_INTERACTIVE)
|
self.assertEqual(args['interactive'], MIGRATE_INTERACTIVE)
|
||||||
self.assertEqual(args['using'], 'default')
|
self.assertEqual(args['using'], 'default')
|
||||||
|
self.assertEqual(args['plan'], [])
|
||||||
|
self.assertIsInstance(args['apps'], migrations.state.StateApps)
|
||||||
|
|
||||||
@override_settings(MIGRATION_MODULES={'migrate_signals': 'migrate_signals.custom_migrations'})
|
@override_settings(MIGRATION_MODULES={'migrate_signals': 'migrate_signals.custom_migrations'})
|
||||||
def test_migrations_only(self):
|
def test_migrations_only(self):
|
||||||
|
@ -101,3 +104,12 @@ class MigrateSignalTests(TestCase):
|
||||||
self.assertEqual(args['verbosity'], MIGRATE_VERBOSITY)
|
self.assertEqual(args['verbosity'], MIGRATE_VERBOSITY)
|
||||||
self.assertEqual(args['interactive'], MIGRATE_INTERACTIVE)
|
self.assertEqual(args['interactive'], MIGRATE_INTERACTIVE)
|
||||||
self.assertEqual(args['using'], 'default')
|
self.assertEqual(args['using'], 'default')
|
||||||
|
self.assertIsInstance(args['plan'][0][0], migrations.Migration)
|
||||||
|
# The migration isn't applied backward.
|
||||||
|
self.assertFalse(args['plan'][0][1])
|
||||||
|
self.assertIsInstance(args['apps'], migrations.state.StateApps)
|
||||||
|
self.assertEqual(pre_migrate_receiver.call_args['apps'].get_models(), [])
|
||||||
|
self.assertEqual(
|
||||||
|
[model._meta.label for model in post_migrate_receiver.call_args['apps'].get_models()],
|
||||||
|
['migrate_signals.Signal']
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue