Refs #27236 -- Added generic mechanism to handle the deprecation of migration operations.
This commit is contained in:
parent
57793b4765
commit
41019e48bb
|
@ -19,6 +19,7 @@ import django.core.checks.caches # NOQA isort:skip
|
||||||
import django.core.checks.compatibility.django_4_0 # NOQA isort:skip
|
import django.core.checks.compatibility.django_4_0 # NOQA isort:skip
|
||||||
import django.core.checks.database # NOQA isort:skip
|
import django.core.checks.database # NOQA isort:skip
|
||||||
import django.core.checks.files # NOQA isort:skip
|
import django.core.checks.files # NOQA isort:skip
|
||||||
|
import django.core.checks.migrations # NOQA isort:skip
|
||||||
import django.core.checks.model_checks # NOQA isort:skip
|
import django.core.checks.model_checks # NOQA isort:skip
|
||||||
import django.core.checks.security.base # NOQA isort:skip
|
import django.core.checks.security.base # NOQA isort:skip
|
||||||
import django.core.checks.security.csrf # NOQA isort:skip
|
import django.core.checks.security.csrf # NOQA isort:skip
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
from django.core.checks import Tags, register
|
||||||
|
|
||||||
|
|
||||||
|
@register(Tags.migrations)
|
||||||
|
def check_migration_operations(**kwargs):
|
||||||
|
from django.db.migrations.loader import MigrationLoader
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
loader = MigrationLoader(None, ignore_no_migrations=True)
|
||||||
|
for migration in loader.disk_migrations.values():
|
||||||
|
errors.extend(migration.check())
|
||||||
|
return errors
|
|
@ -15,6 +15,7 @@ class Tags:
|
||||||
compatibility = "compatibility"
|
compatibility = "compatibility"
|
||||||
database = "database"
|
database = "database"
|
||||||
files = "files"
|
files = "files"
|
||||||
|
migrations = "migrations"
|
||||||
models = "models"
|
models = "models"
|
||||||
security = "security"
|
security = "security"
|
||||||
signals = "signals"
|
signals = "signals"
|
||||||
|
|
|
@ -219,6 +219,12 @@ class Migration:
|
||||||
name = new_name
|
name = new_name
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
errors = []
|
||||||
|
for operation in self.operations:
|
||||||
|
errors.extend(operation.check_deprecation_details())
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
class SwappableTuple(tuple):
|
class SwappableTuple(tuple):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from django.db import router
|
from django.db import router
|
||||||
|
from django.utils.deprecation import DeprecationForHistoricalMigrationMixin
|
||||||
|
|
||||||
|
|
||||||
class Operation:
|
class Operation(DeprecationForHistoricalMigrationMixin):
|
||||||
"""
|
"""
|
||||||
Base class for migration operations.
|
Base class for migration operations.
|
||||||
|
|
||||||
|
@ -33,6 +34,8 @@ class Operation:
|
||||||
|
|
||||||
serialization_expand_args = []
|
serialization_expand_args = []
|
||||||
|
|
||||||
|
check_type = "migrations"
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
# We capture the arguments to make returning them trivial
|
# We capture the arguments to make returning them trivial
|
||||||
self = object.__new__(cls)
|
self = object.__new__(cls)
|
||||||
|
|
|
@ -83,6 +83,7 @@ Django's system checks are organized using the following tags:
|
||||||
you specify configured database aliases using the ``--database`` option when
|
you specify configured database aliases using the ``--database`` option when
|
||||||
calling the :djadmin:`check` command.
|
calling the :djadmin:`check` command.
|
||||||
* ``files``: Checks files related configuration.
|
* ``files``: Checks files related configuration.
|
||||||
|
* ``migrations``: Checks of migration operations.
|
||||||
* ``models``: Checks of model, field, and manager definitions.
|
* ``models``: Checks of model, field, and manager definitions.
|
||||||
* ``security``: Checks security related configuration.
|
* ``security``: Checks security related configuration.
|
||||||
* ``signals``: Checks on signal declarations and handler registrations.
|
* ``signals``: Checks on signal declarations and handler registrations.
|
||||||
|
@ -94,6 +95,10 @@ Django's system checks are organized using the following tags:
|
||||||
|
|
||||||
Some checks may be registered with multiple tags.
|
Some checks may be registered with multiple tags.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.2
|
||||||
|
|
||||||
|
The ``migrations`` tag was added.
|
||||||
|
|
||||||
Core system checks
|
Core system checks
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,8 @@ Management Commands
|
||||||
Migrations
|
Migrations
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* The new :ref:`generic mechanism <migrations-removing-operation>` allows
|
||||||
|
handling the deprecation of migration operations.
|
||||||
|
|
||||||
Models
|
Models
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
|
@ -128,18 +128,18 @@ The code below is equivalent to the code above::
|
||||||
|
|
||||||
.. _field-checking:
|
.. _field-checking:
|
||||||
|
|
||||||
Field, model, manager, and database checks
|
Field, model, manager, migration, and database checks
|
||||||
------------------------------------------
|
-----------------------------------------------------
|
||||||
|
|
||||||
In some cases, you won't need to register your check function -- you can
|
In some cases, you won't need to register your check function -- you can
|
||||||
piggyback on an existing registration.
|
piggyback on an existing registration.
|
||||||
|
|
||||||
Fields, models, model managers, and database backends all implement a
|
Fields, models, model managers, migrations, and database backends all implement
|
||||||
``check()`` method that is already registered with the check framework. If you
|
a ``check()`` method that is already registered with the check framework. If
|
||||||
want to add extra checks, you can extend the implementation on the base class,
|
you want to add extra checks, you can extend the implementation on the base
|
||||||
perform any extra checks you need, and append any messages to those generated
|
class, perform any extra checks you need, and append any messages to those
|
||||||
by the base class. It's recommended that you delegate each check to separate
|
generated by the base class. It's recommended that you delegate each check to
|
||||||
methods.
|
separate methods.
|
||||||
|
|
||||||
Consider an example where you are implementing a custom field named
|
Consider an example where you are implementing a custom field named
|
||||||
``RangedIntegerField``. This field adds ``min`` and ``max`` arguments to the
|
``RangedIntegerField``. This field adds ``min`` and ``max`` arguments to the
|
||||||
|
@ -194,6 +194,10 @@ the only difference is that the check is a classmethod, not an instance method::
|
||||||
# ... your own checks ...
|
# ... your own checks ...
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
.. versionchanged:: 4.2
|
||||||
|
|
||||||
|
Migration checks were added.
|
||||||
|
|
||||||
Writing tests
|
Writing tests
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
|
@ -503,6 +503,56 @@ database migrations such as ``__init__()``, ``deconstruct()``, and
|
||||||
which reference the field exist. For example, after squashing migrations and
|
which reference the field exist. For example, after squashing migrations and
|
||||||
removing the old ones, you should be able to remove the field completely.
|
removing the old ones, you should be able to remove the field completely.
|
||||||
|
|
||||||
|
.. _migrations-removing-operation:
|
||||||
|
|
||||||
|
Considerations when removing migration operations
|
||||||
|
=================================================
|
||||||
|
|
||||||
|
.. versionadded:: 4.2
|
||||||
|
|
||||||
|
Removing custom operations from your project or third-party app will cause a
|
||||||
|
problem if they are referenced in old migrations.
|
||||||
|
|
||||||
|
To help with this situation, Django provides some operation attributes to
|
||||||
|
assist with operation deprecation using the :doc:`system checks framework
|
||||||
|
</topics/checks>`.
|
||||||
|
|
||||||
|
Add the ``system_check_deprecated_details`` attribute to your operation similar
|
||||||
|
to the following::
|
||||||
|
|
||||||
|
class MyCustomOperation(Operation):
|
||||||
|
system_check_deprecated_details = {
|
||||||
|
"msg": (
|
||||||
|
"MyCustomOperation has been deprecated. Support for it "
|
||||||
|
"(except in historical migrations) will be removed in "
|
||||||
|
"Django 5.1."
|
||||||
|
),
|
||||||
|
"hint": "Use DifferentOperation instead.", # optional
|
||||||
|
"id": "migrations.W900", # pick a unique ID for your operation.
|
||||||
|
}
|
||||||
|
|
||||||
|
After a deprecation period of your choosing (two or three feature releases for
|
||||||
|
operations in Django itself), change the ``system_check_deprecated_details``
|
||||||
|
attribute to ``system_check_removed_details`` and update the dictionary similar
|
||||||
|
to::
|
||||||
|
|
||||||
|
class MyCustomOperation(Operation):
|
||||||
|
system_check_removed_details = {
|
||||||
|
"msg": (
|
||||||
|
"MyCustomOperation has been removed except for support in "
|
||||||
|
"historical migrations."
|
||||||
|
),
|
||||||
|
"hint': "Use DifferentOperation instead.",
|
||||||
|
"id": "migrations.E900", # pick a unique ID for your operation.
|
||||||
|
}
|
||||||
|
|
||||||
|
You should keep the operation's methods that are required for it to operate in
|
||||||
|
database migrations such as ``__init__()``, ``state_forwards()``,
|
||||||
|
``database_forwards()``, and ``database_backwards()``. Keep this stub operation
|
||||||
|
for as long as any migrations which reference the operation exist. For example,
|
||||||
|
after squashing migrations and removing the old ones, you should be able to
|
||||||
|
remove the operation completely.
|
||||||
|
|
||||||
.. _data-migrations:
|
.. _data-migrations:
|
||||||
|
|
||||||
Data Migrations
|
Data Migrations
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
from django.core import checks
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.migrations.operations.base import Operation
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedMigrationOperationTests(TestCase):
|
||||||
|
def test_default_operation(self):
|
||||||
|
class MyOperation(Operation):
|
||||||
|
system_check_deprecated_details = {}
|
||||||
|
|
||||||
|
my_operation = MyOperation()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
operations = [my_operation]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
Migration("name", "app_label").check(),
|
||||||
|
[
|
||||||
|
checks.Warning(
|
||||||
|
msg="MyOperation has been deprecated.",
|
||||||
|
obj=my_operation,
|
||||||
|
id="migrations.WXXX",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_user_specified_details(self):
|
||||||
|
class MyOperation(Operation):
|
||||||
|
system_check_deprecated_details = {
|
||||||
|
"msg": "This operation is deprecated and will be removed soon.",
|
||||||
|
"hint": "Use something else.",
|
||||||
|
"id": "migrations.W999",
|
||||||
|
}
|
||||||
|
|
||||||
|
my_operation = MyOperation()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
operations = [my_operation]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
Migration("name", "app_label").check(),
|
||||||
|
[
|
||||||
|
checks.Warning(
|
||||||
|
msg="This operation is deprecated and will be removed soon.",
|
||||||
|
obj=my_operation,
|
||||||
|
hint="Use something else.",
|
||||||
|
id="migrations.W999",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RemovedMigrationOperationTests(TestCase):
|
||||||
|
def test_default_operation(self):
|
||||||
|
class MyOperation(Operation):
|
||||||
|
system_check_removed_details = {}
|
||||||
|
|
||||||
|
my_operation = MyOperation()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
operations = [my_operation]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
Migration("name", "app_label").check(),
|
||||||
|
[
|
||||||
|
checks.Error(
|
||||||
|
msg=(
|
||||||
|
"MyOperation has been removed except for support in historical "
|
||||||
|
"migrations."
|
||||||
|
),
|
||||||
|
obj=my_operation,
|
||||||
|
id="migrations.EXXX",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_user_specified_details(self):
|
||||||
|
class MyOperation(Operation):
|
||||||
|
system_check_removed_details = {
|
||||||
|
"msg": "Support for this operation is gone.",
|
||||||
|
"hint": "Use something else.",
|
||||||
|
"id": "migrations.E999",
|
||||||
|
}
|
||||||
|
|
||||||
|
my_operation = MyOperation()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
operations = [my_operation]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
Migration("name", "app_label").check(),
|
||||||
|
[
|
||||||
|
checks.Error(
|
||||||
|
msg="Support for this operation is gone.",
|
||||||
|
obj=my_operation,
|
||||||
|
hint="Use something else.",
|
||||||
|
id="migrations.E999",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
|
@ -1,11 +1,22 @@
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
from django.db.migrations.operations.base import Operation
|
||||||
|
|
||||||
|
|
||||||
|
class DummyOperation(Operation):
|
||||||
|
def state_forwards(self, app_label, state):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def database_forwards(self, app_label, schema_editor, from_state, to_state):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def database_backwards(self, app_label, schema_editor, from_state, to_state):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django.contrib.postgres.operations import CryptoExtension
|
from django.contrib.postgres.operations import CryptoExtension
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CryptoExtension = mock.Mock()
|
CryptoExtension = DummyOperation
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from django.db import connection, migrations
|
from django.db import connection, migrations
|
||||||
|
from django.db.migrations.operations.base import Operation
|
||||||
|
|
||||||
|
|
||||||
|
class DummyOperation(Operation):
|
||||||
|
def state_forwards(self, app_label, state):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def database_forwards(self, app_label, schema_editor, from_state, to_state):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def database_backwards(self, app_label, schema_editor, from_state, to_state):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django.contrib.postgres.operations import (
|
from django.contrib.postgres.operations import (
|
||||||
|
@ -15,14 +26,14 @@ try:
|
||||||
UnaccentExtension,
|
UnaccentExtension,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
BloomExtension = mock.Mock()
|
BloomExtension = DummyOperation
|
||||||
BtreeGinExtension = mock.Mock()
|
BtreeGinExtension = DummyOperation
|
||||||
BtreeGistExtension = mock.Mock()
|
BtreeGistExtension = DummyOperation
|
||||||
CITextExtension = mock.Mock()
|
CITextExtension = DummyOperation
|
||||||
CreateExtension = mock.Mock()
|
CreateExtension = DummyOperation
|
||||||
HStoreExtension = mock.Mock()
|
HStoreExtension = DummyOperation
|
||||||
TrigramExtension = mock.Mock()
|
TrigramExtension = DummyOperation
|
||||||
UnaccentExtension = mock.Mock()
|
UnaccentExtension = DummyOperation
|
||||||
needs_crypto_extension = False
|
needs_crypto_extension = False
|
||||||
else:
|
else:
|
||||||
needs_crypto_extension = (
|
needs_crypto_extension = (
|
||||||
|
@ -41,7 +52,7 @@ class Migration(migrations.Migration):
|
||||||
# dash in its name.
|
# dash in its name.
|
||||||
CreateExtension("uuid-ossp"),
|
CreateExtension("uuid-ossp"),
|
||||||
# CryptoExtension is required for RandomUUID() on PostgreSQL < 13.
|
# CryptoExtension is required for RandomUUID() on PostgreSQL < 13.
|
||||||
CryptoExtension() if needs_crypto_extension else mock.Mock(),
|
CryptoExtension() if needs_crypto_extension else DummyOperation(),
|
||||||
HStoreExtension(),
|
HStoreExtension(),
|
||||||
TrigramExtension(),
|
TrigramExtension(),
|
||||||
UnaccentExtension(),
|
UnaccentExtension(),
|
||||||
|
|
Loading…
Reference in New Issue