-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from charettes/stage-deploy
Change stage verbiage and add full test coverage.
- Loading branch information
Showing
17 changed files
with
286 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,4 @@ dist/ | |
.coverage | ||
.tox | ||
.mypy_cache/ | ||
htmlcov/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,6 @@ | ||
from .constants import Stage | ||
|
||
__all__ = ("Stage",) | ||
|
||
|
||
default_app_config = "syzygy.apps.SyzygyConfig" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from enum import Enum | ||
|
||
|
||
class Stage(Enum): | ||
PRE_DEPLOY = "pre_deploy" | ||
POST_DEPLOY = "post_deployment" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,29 @@ | ||
from django.apps import apps | ||
from django.core.management import CommandError | ||
from django.core.management.commands.migrate import ( # type: ignore | ||
Command as MigrateCommand, | ||
) | ||
from django.db.models.signals import pre_migrate | ||
|
||
from syzygy.plan import get_prerequisite_plan | ||
from syzygy.plan import get_pre_deploy_plan | ||
|
||
|
||
class Command(MigrateCommand): | ||
def add_arguments(self, parser): | ||
super().add_arguments(parser) | ||
parser.add_argument("--prerequisite", action="store_true") | ||
parser.add_argument( | ||
"--pre-deploy", | ||
action="store_true", | ||
help="Only run migrations staged for pre-deployment.", | ||
) | ||
|
||
def migrate_prerequisite(self, plan, **kwargs): | ||
def migrate_pre_deploy(self, plan, **kwargs): | ||
try: | ||
plan[:] = get_prerequisite_plan(plan) | ||
plan[:] = get_pre_deploy_plan(plan) | ||
except ValueError as exc: | ||
raise CommandError(str(exc)) from exc | ||
|
||
def handle(self, *args, prerequisite, **options): | ||
if prerequisite: | ||
pre_migrate.connect(self.migrate_prerequisite) | ||
def handle(self, *args, pre_deploy, **options): | ||
if pre_deploy: | ||
pre_migrate.connect(self.migrate_pre_deploy, sender=apps.get_app_config('syzygy')) | ||
super().handle(*args, **options) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Empty models file in order to allow the pre_migrate signal to be sent. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,78 +1,94 @@ | ||
from typing import List, Optional, Tuple | ||
from typing import Dict, List, Optional, Tuple | ||
|
||
from django.conf import settings | ||
from django.db.migrations import DeleteModel, Migration, RemoveField | ||
from django.db.migrations.operations.base import Operation | ||
|
||
from .constants import Stage | ||
|
||
MigrationStagesSetting = Dict[str, Stage] | ||
Plan = List[Tuple[Migration, bool]] | ||
|
||
|
||
def must_postpone_operation( | ||
operation: Operation, backward: bool = False | ||
) -> Optional[bool]: | ||
"""Return whether not operation must be postponed.""" | ||
def get_operation_stage(operation: Operation) -> Stage: | ||
"""Return the heuristically determined `Stage` of the operation.""" | ||
if isinstance(operation, (DeleteModel, RemoveField)): | ||
return not backward | ||
# All other operations are assumed to be prerequisite. | ||
return backward | ||
return Stage.POST_DEPLOY | ||
return Stage.PRE_DEPLOY | ||
|
||
|
||
def _get_configured_migration_stage(migration: Migration) -> Optional[Stage]: | ||
"""Return the `Stage` configured through setting:`MIGRATION_STAGES` of the migration.""" | ||
setting: MigrationStagesSetting = getattr(settings, "MIGRATION_STAGES", None) | ||
if setting is None: | ||
return | ||
return setting.get(f"{migration.app_label}.{migration.name}") | ||
|
||
|
||
def must_postpone_migration( | ||
def get_migration_stage(migration: Migration) -> Optional[Stage]: | ||
""" | ||
Return the `Stage` of the migration. | ||
If not specified through setting:`MIGRATION_STAGES` or a `stage` | ||
:class:`django.db.migrations.Migration` class attribute it will be | ||
tentatively deduced from its list of | ||
attr:`django.db.migrations.Migration.operations`. | ||
If the migration doesn't have any `operations` then `None` will be returned | ||
and a `ValueError` will be raised if its contains operations of different | ||
stages. | ||
""" | ||
stage = getattr(migration, "stage", None) or _get_configured_migration_stage( | ||
migration | ||
) | ||
if stage is not None: | ||
return stage | ||
for operation in migration.operations: | ||
operation_stage = get_operation_stage(operation) | ||
if stage is None: | ||
stage = operation_stage | ||
elif operation_stage != stage: | ||
raise ValueError(f"Cannot automatically determine stage of {migration}.") | ||
return stage | ||
|
||
|
||
def must_post_deploy_migration( | ||
migration: Migration, backward: bool = False | ||
) -> Optional[bool]: | ||
""" | ||
Return whether or not migration must be postponed. | ||
Return whether or not migration must be run after deployment. | ||
If not specified through a `postpone` :class:`django.db.migrations.Migration` | ||
If not specified through a `stage` :class:`django.db.migrations.Migration` | ||
class attribute it will be tentatively deduced from its list of | ||
attr:`django.db.migrations.Migration.operations`. | ||
In cases of ambiguity a `ValueError` will be raised. | ||
""" | ||
# Postponed migrations are considered prerequisite when they are reverted. | ||
try: | ||
setting = settings.SYZYGY_POSTPONE | ||
except AttributeError: | ||
global_postpone = None | ||
else: | ||
key = (migration.app_label, migration.name) | ||
global_postpone = setting.get(key) | ||
postpone = getattr(migration, "postpone", global_postpone) | ||
if postpone is True: | ||
return not backward | ||
elif postpone is False: | ||
migration_stage = get_migration_stage(migration) | ||
if migration_stage is None: | ||
return None | ||
if migration_stage is Stage.PRE_DEPLOY: | ||
return backward | ||
# Migrations without operations such as merges are never postponed. | ||
if not migration.operations: | ||
return False | ||
for operation in migration.operations: | ||
postpone_operation = must_postpone_operation(operation, backward) | ||
if postpone is None: | ||
postpone = postpone_operation | ||
elif postpone_operation != postpone: | ||
raise ValueError( | ||
f"Cannot determine whether or not {migration} should be postponed." | ||
) | ||
return postpone | ||
return not backward | ||
|
||
|
||
def get_prerequisite_plan(plan: Plan) -> Plan: | ||
def get_pre_deploy_plan(plan: Plan) -> Plan: | ||
""" | ||
Trim provided plan to its leading contiguous prerequisite sequence. | ||
Trim provided plan to its leading contiguous pre-deployment sequence. | ||
If the plan contains non-contiguous sequence of prerequisite migrations | ||
or migrations with ambiguous prerequisite nature a `ValueError` is raised. | ||
If the plan contains non-contiguous sequence of pre-deployment migrations | ||
or migrations with ambiguous deploy stage a `ValueError` is raised. | ||
""" | ||
prerequisite_plan: Plan = [] | ||
postpone = False | ||
pre_deploy_plan: Plan = [] | ||
post_deploy = False | ||
for migration, backward in plan: | ||
if must_postpone_migration(migration, backward): | ||
postpone = True | ||
if must_post_deploy_migration(migration, backward): | ||
post_deploy = True | ||
else: | ||
if postpone: | ||
if post_deploy: | ||
raise ValueError( | ||
"Plan contains a non-contiguous sequence of prerequisite " | ||
"Plan contains a non-contiguous sequence of pre-deployment " | ||
"migrations." | ||
) | ||
prerequisite_plan.append((migration, backward)) | ||
return prerequisite_plan | ||
pre_deploy_plan.append((migration, backward)) | ||
return pre_deploy_plan |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from django.apps import apps | ||
from django.core.checks import run_checks | ||
from django.core.checks.messages import Error | ||
from django.test import SimpleTestCase | ||
|
||
|
||
class ChecksTests(SimpleTestCase): | ||
hint = ( | ||
"Assign an explicit stage to it or break its operation into multiple " | ||
"migrations if it's not already applied." | ||
) | ||
|
||
def test_ambiguous_stage(self): | ||
with self.settings( | ||
MIGRATION_MODULES={"tests": "tests.test_migrations.ambiguous"} | ||
): | ||
checks = run_checks( | ||
app_configs=[apps.get_app_config("tests")], tags={"migrations"} | ||
) | ||
self.assertEqual(len(checks), 1) | ||
self.assertEqual( | ||
checks[0], | ||
Error( | ||
msg="Cannot automatically determine stage of tests.0001_initial.", | ||
hint=self.hint, | ||
obj=("tests", "0001_initial"), | ||
id="migrations.0001", | ||
), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from django.core.management import CommandError, call_command | ||
from django.db import connection | ||
from django.db.migrations.recorder import MigrationRecorder | ||
from django.test import TransactionTestCase | ||
|
||
|
||
class MigrateTests(TransactionTestCase): | ||
def get_applied_migrations(self): | ||
return { | ||
name | ||
for (app_label, name) in MigrationRecorder(connection).applied_migrations() | ||
if app_label == "tests" | ||
} | ||
|
||
def assert_not_applied(self, name): | ||
self.assertIn(("tests", name), self.recorder.applied_migrations()) | ||
|
||
def test_pre_deploy_forward(self): | ||
with self.settings( | ||
MIGRATION_MODULES={"tests": "tests.test_migrations.functional"} | ||
): | ||
call_command("migrate", "tests", pre_deploy=True, verbosity=0) | ||
self.assertEqual(self.get_applied_migrations(), {"0001_pre_deploy"}) | ||
|
||
def test_pre_deploy_backward(self): | ||
with self.settings( | ||
MIGRATION_MODULES={"tests": "tests.test_migrations.functional"} | ||
): | ||
call_command("migrate", "tests", verbosity=0) | ||
self.assertEqual( | ||
self.get_applied_migrations(), {"0001_pre_deploy", "0002_post_deploy"} | ||
) | ||
call_command("migrate", "tests", "zero", pre_deploy=True, verbosity=0) | ||
self.assertEqual(self.get_applied_migrations(), {"0001_pre_deploy"}) | ||
|
||
def test_ambiguous(self): | ||
with self.settings( | ||
MIGRATION_MODULES={"tests": "tests.test_migrations.ambiguous"} | ||
): | ||
with self.assertRaisesMessage(CommandError, 'Cannot automatically determine stage of tests.0001_initial.'): | ||
call_command("migrate", "tests", pre_deploy=True, verbosity=0) |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from django.db import migrations | ||
|
||
|
||
class Migration(migrations.Migration): | ||
operations = [migrations.CreateModel("Foo", []), migrations.DeleteModel("Bar")] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from django.db import migrations | ||
|
||
from syzygy import Stage | ||
|
||
|
||
class Migration(migrations.Migration): | ||
initial = True | ||
stage = Stage.PRE_DEPLOY | ||
atomic = False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from django.db import migrations | ||
|
||
from syzygy import Stage | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [("tests", "0001_pre_deploy")] | ||
stage = Stage.POST_DEPLOY | ||
atomic = False |
Empty file.
Oops, something went wrong.