1
0
mirror of https://github.com/django/django.git synced 2025-10-24 06:06:09 +00:00

Implement swappable model support for migrations

This commit is contained in:
Andrew Godwin
2014-01-15 14:20:47 +00:00
parent 5c7ac7494a
commit c9de1b4a55
10 changed files with 216 additions and 13 deletions

View File

@@ -1,2 +1,2 @@
from .migration import Migration # NOQA
from .migration import Migration, swappable_dependency # NOQA
from .operations import * # NOQA

View File

@@ -105,8 +105,12 @@ class MigrationAutodetector(object):
)
)
for field_name, other_app_label, other_model_name in related_fields:
if app_label != other_app_label:
self.add_dependency(app_label, other_app_label)
# If it depends on a swappable something, add a dynamic depend'cy
swappable_setting = new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0].swappable_setting
if swappable_setting is not None:
self.add_swappable_dependency(app_label, swappable_setting)
elif app_label != other_app_label:
self.add_dependency(app_label, other_app_label)
del pending_add[app_label, model_name]
# Ah well, we'll need to split one. Pick deterministically.
else:
@@ -140,7 +144,11 @@ class MigrationAutodetector(object):
),
new=True,
)
if app_label != other_app_label:
# If it depends on a swappable something, add a dynamic depend'cy
swappable_setting = new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0].swappable_setting
if swappable_setting is not None:
self.add_swappable_dependency(app_label, swappable_setting)
elif app_label != other_app_label:
self.add_dependency(app_label, other_app_label)
# Removing models
removed_models = set(old_model_keys) - set(new_model_keys)
@@ -276,6 +284,13 @@ class MigrationAutodetector(object):
dependency = (other_app_label, "__first__")
self.migrations[app_label][-1].dependencies.append(dependency)
def add_swappable_dependency(self, app_label, setting_name):
"""
Adds a dependency to the value of a swappable model setting.
"""
dependency = ("__setting__", setting_name)
self.migrations[app_label][-1].dependencies.append(dependency)
def _arrange_for_graph(self, changes, graph):
"""
Takes in a result from changes() and a MigrationGraph,

View File

@@ -223,7 +223,7 @@ class MigrationLoader(object):
self.graph.add_node(parent, new_migration)
self.applied_migrations.add(parent)
elif parent[0] in self.migrated_apps:
parent = (parent[0], list(self.graph.root_nodes(parent[0]))[0])
parent = list(self.graph.root_nodes(parent[0]))[0]
else:
raise ValueError("Dependency on unknown app %s" % parent[0])
self.graph.add_dependency(key, parent)

View File

@@ -127,3 +127,10 @@ class Migration(object):
to_run.reverse()
for operation, to_state, from_state in to_run:
operation.database_backwards(self.app_label, schema_editor, from_state, to_state)
def swappable_dependency(value):
"""
Turns a setting value into a dependency.
"""
return (value.split(".", 1)[0], "__first__")

View File

@@ -13,6 +13,20 @@ from django.utils.functional import Promise
from django.utils import six
class SettingsReference(str):
"""
Special subclass of string which actually references a current settings
value. It's treated as the value in memory, but serializes out to a
settings.NAME attribute reference.
"""
def __new__(self, value, setting_name):
return str.__new__(self, value)
def __init__(self, value, setting_name):
self.setting_name = setting_name
class MigrationWriter(object):
"""
Takes a Migration instance and is able to produce the contents
@@ -27,7 +41,6 @@ class MigrationWriter(object):
Returns a string of the file contents.
"""
items = {
"dependencies": repr(self.migration.dependencies),
"replaces_str": "",
}
imports = set()
@@ -46,6 +59,15 @@ class MigrationWriter(object):
arg_strings.append("%s = %s" % (kw, arg_string))
operation_strings.append("migrations.%s(%s\n )" % (name, "".join("\n %s," % arg for arg in arg_strings)))
items["operations"] = "[%s\n ]" % "".join("\n %s," % s for s in operation_strings)
# Format dependencies and write out swappable dependencies right
items["dependencies"] = "["
for dependency in self.migration.dependencies:
if dependency[0] == "__setting__":
items["dependencies"] += "\n migrations.swappable_dependency(settings.%s)," % dependency[1]
imports.add("from django.conf import settings")
else:
items["dependencies"] += "\n %s," % repr(dependency)
items["dependencies"] += "\n ]"
# Format imports nicely
imports.discard("from django.db import models")
if not imports:
@@ -136,6 +158,9 @@ class MigrationWriter(object):
# Datetimes
elif isinstance(value, (datetime.datetime, datetime.date)):
return repr(value), set(["import datetime"])
# Settings references
elif isinstance(value, SettingsReference):
return "settings.%s" % value.setting_name, set(["from django.conf import settings"])
# Simple types
elif isinstance(value, six.integer_types + (float, six.binary_type, six.text_type, bool, type(None))):
return repr(value), set()

View File

@@ -16,6 +16,7 @@ from django.utils.deprecation import RenameMethodsBase
from django.utils.translation import ugettext_lazy as _
from django.utils.functional import curry, cached_property
from django.core import exceptions
from django.apps import apps
from django import forms
RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
@@ -121,6 +122,30 @@ class RelatedField(Field):
else:
self.do_related_class(other, cls)
@property
def swappable_setting(self):
"""
Gets the setting that this is powered from for swapping, or None
if it's not swapped in / marked with swappable=False.
"""
if self.swappable:
# Work out string form of "to"
if isinstance(self.rel.to, six.string_types):
to_string = self.rel.to
else:
to_string = "%s.%s" % (
self.rel.to._meta.app_label,
self.rel.to._meta.object_name,
)
# See if anything swapped/swappable matches
for model in apps.get_models(include_swapped=True):
if model._meta.swapped:
if model._meta.swapped == to_string:
return model._meta.swappable
if ("%s.%s" % (model._meta.app_label, model._meta.object_name)) == to_string and model._meta.swappable:
return model._meta.swappable
return None
def set_attributes_from_rel(self):
self.name = self.name or (self.rel.to._meta.model_name + '_' + self.rel.to._meta.pk.name)
if self.verbose_name is None:
@@ -1061,9 +1086,10 @@ class ForeignObject(RelatedField):
generate_reverse_relation = True
related_accessor_class = ForeignRelatedObjectsDescriptor
def __init__(self, to, from_fields, to_fields, **kwargs):
def __init__(self, to, from_fields, to_fields, swappable=True, **kwargs):
self.from_fields = from_fields
self.to_fields = to_fields
self.swappable = swappable
if 'rel' not in kwargs:
kwargs['rel'] = ForeignObjectRel(
@@ -1082,10 +1108,25 @@ class ForeignObject(RelatedField):
name, path, args, kwargs = super(ForeignObject, self).deconstruct()
kwargs['from_fields'] = self.from_fields
kwargs['to_fields'] = self.to_fields
# Work out string form of "to"
if isinstance(self.rel.to, six.string_types):
kwargs['to'] = self.rel.to
else:
kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name)
# If swappable is True, then see if we're actually pointing to the target
# of a swap.
swappable_setting = self.swappable_setting
if swappable_setting is not None:
# If it's already a settings reference, error
if hasattr(kwargs['to'], "setting_name"):
if kwargs['to'].setting_name != swappable_setting:
raise ValueError("Cannot deconstruct a ForeignKey pointing to a model that is swapped in place of more than one model (%s and %s)" % (kwargs['to'].setting_name, swappable_setting))
# Set it
from django.db.migrations.writer import SettingsReference
kwargs['to'] = SettingsReference(
kwargs['to'],
swappable_setting,
)
return name, path, args, kwargs
def resolve_related_fields(self):
@@ -1516,7 +1557,7 @@ def create_many_to_many_intermediary_model(field, klass):
class ManyToManyField(RelatedField):
description = _("Many-to-many relationship")
def __init__(self, to, db_constraint=True, **kwargs):
def __init__(self, to, db_constraint=True, swappable=True, **kwargs):
try:
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
@@ -1534,6 +1575,7 @@ class ManyToManyField(RelatedField):
db_constraint=db_constraint,
)
self.swappable = swappable
self.db_table = kwargs.pop('db_table', None)
if kwargs['rel'].through is not None:
assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
@@ -1552,6 +1594,20 @@ class ManyToManyField(RelatedField):
kwargs['to'] = self.rel.to
else:
kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name)
# If swappable is True, then see if we're actually pointing to the target
# of a swap.
swappable_setting = self.swappable_setting
if swappable_setting is not None:
# If it's already a settings reference, error
if hasattr(kwargs['to'], "setting_name"):
if kwargs['to'].setting_name != swappable_setting:
raise ValueError("Cannot deconstruct a ManyToManyField pointing to a model that is swapped in place of more than one model (%s and %s)" % (kwargs['to'].setting_name, swappable_setting))
# Set it
from django.db.migrations.writer import SettingsReference
kwargs['to'] = SettingsReference(
kwargs['to'],
swappable_setting,
)
return name, path, args, kwargs
def _get_path_info(self, direct=False):