Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add disabled mode to avoid safemigrate's protections. #49

Merged
merged 2 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Pending
*******

* Add ``settings.SAFEMIGRATE = "disabled"`` setting to disable ``safemigrate``
protections.

4.2 (2023-12-13)
+++++++++

Expand Down
12 changes: 12 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ that are not blocked by any unsafe migrations.
Any remaining migrations can be run after the fact
using the normal ``migrate`` Django command.

Disabled Mode
=============

To disable the protections of ``safemigrate`` entirely, add the
``SAFEMIGRATE`` setting:

.. code-block:: python

SAFEMIGRATE = "disabled"

In this mode ``safemigrate`` will migrations as if they were
using the normal ``migrate`` Django command.

Contributing
============
Expand Down
27 changes: 26 additions & 1 deletion src/django_safemigrate/management/commands/safemigrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Migration safety is enforced by a pre_migrate signal receiver.
"""
from enum import Enum

from django.conf import settings
from django.core.management.base import CommandError
from django.core.management.commands import migrate
Expand All @@ -10,11 +12,29 @@
from django_safemigrate import Safe


class SafeMigrate(Enum):
strict = "strict"
nonstrict = "nonstrict"
disabled = "disabled"


def safety(migration):
"""Determine the safety status of a migration."""
return getattr(migration, "safe", Safe.after_deploy)


def safemigrate():
state = getattr(settings, "SAFEMIGRATE", None)
if state is None:
return state
try:
return SafeMigrate(state.lower())
except ValueError as e:
raise ValueError(
"Invalid SAFEMIGRATE setting, it must be one of 'strict', 'nonstrict', or 'disabled'."
) from e


class Command(migrate.Command):
"""Run database migrations that are safe to run before deployment."""

Expand All @@ -37,8 +57,13 @@ def pre_migrate_receiver(self, *, plan, **_):
return # Only run once
self.receiver_has_run = True

safemigrate_state = safemigrate()
if safemigrate_state == SafeMigrate.disabled:
# When disabled, run migrate
return

# strict by default
strict = getattr(settings, "SAFEMIGRATE", None) != "nonstrict"
strict = safemigrate_state != SafeMigrate.nonstrict

if any(backward for mig, backward in plan):
raise CommandError("Backward migrations are not supported.")
Expand Down
78 changes: 77 additions & 1 deletion tests/safemigrate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def test_blocked_by_after_nonstrict(self, settings, receiver):
This allows the command to succeed where it would normally
error. This allows for development environments, where
errors are acceptable during transitions, to avoid having
to migrate everything incrementall the way production
to migrate everything incrementally the way production
environments are expected to.
"""
settings.SAFEMIGRATE = "nonstrict"
Expand Down Expand Up @@ -255,6 +255,82 @@ def test_blocked_by_after_nonstrict(self, settings, receiver):
receiver(plan=plan)
assert len(plan) == 1

def test_with_non_safe_migration_nonstrict(self, settings, receiver):
"""
Nonstrict mode allows prevents protected migrations.
In this case, that's migrations without a safe attribute.
"""
settings.SAFEMIGRATE = "nonstrict"
plan = [
(
Migration(
"auth",
"0001_initial",
),
False,
),
(
Migration(
"spam",
"0001_initial",
safe=Safe.before_deploy,
dependencies=[("auth", "0001_initial")],
),
False,
),
]
receiver(plan=plan)
assert len(plan) == 0

def test_with_non_safe_migration_disabled(self, settings, receiver):
"""Disabled mode allows all migrations"""
settings.SAFEMIGRATE = "disabled"
plan = [
(
Migration(
"auth",
"0001_initial",
),
False,
),
(
Migration(
"spam",
"0001_initial",
safe=Safe.before_deploy,
dependencies=[("auth", "0001_initial")],
),
False,
),
(
Migration(
"spam",
"0002_followup",
safe=Safe.after_deploy,
dependencies=[("spam", "0001_initial")],
),
False,
),
(
Migration(
"spam",
"0003_safety",
safe=Safe.before_deploy,
dependencies=[("spam", "0002_followup")],
),
False,
),
]
receiver(plan=plan)
assert len(plan) == 4

def test_safemigrate_invalid_value(self, settings, receiver):
"""Invalid settings of the SAFEMIGRATE setting will raise an error."""
settings.SAFEMIGRATE = "invalid"
plan = []
with pytest.raises(ValueError):
receiver(plan=plan)

def test_string_invalid(self, receiver):
"""Invalid settings of the safe property will raise an error."""
plan = [(Migration("spam", "0001_initial", safe="before_deploy"), False)]
Expand Down
6 changes: 5 additions & 1 deletion tests/testproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
USE_TZ = True

# Application definition
INSTALLED_APPS = ["django_safemigrate.apps.SafeMigrateConfig"]
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django_safemigrate.apps.SafeMigrateConfig",
]
MIDDLEWARE = []
ROOT_URLCONF = "testproject.urls"
Loading