Skip to content

Commit

Permalink
Moved the rendered state logic to the mutable model class.
Browse files Browse the repository at this point in the history
  • Loading branch information
charettes committed Dec 21, 2015
1 parent ff1dcda commit b3d75fb
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 13 deletions.
56 changes: 54 additions & 2 deletions mutant/compat.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals

from contextlib import contextmanager
from operator import attrgetter

import django
Expand All @@ -23,6 +24,8 @@ def clear_opts_related_cache(model_class):
opts._expire_cache()
for child in children:
clear_opts_related_cache(child)

from django.db.migrations.state import StateApps
else:
def get_remote_field_accessor_name(field):
return field.related.get_accessor_name()
Expand Down Expand Up @@ -55,12 +58,61 @@ def clear_opts_related_cache(model_class):
for child in children:
clear_opts_related_cache(child)

from django.apps.registry import Apps
from django.db.migrations.state import InvalidBasesError

class StateApps(Apps):
def __init__(self, *args, **kwargs):
super(StateApps, self).__init__([])

@contextmanager
def bulk_update(self):
# Avoid clearing each model's cache for each change. Instead, clear
# all caches when we're finished updating the model instances.
ready = self.ready
self.ready = False
try:
yield
finally:
self.ready = ready
self.clear_cache()

def render_multiple(self, model_states):
# We keep trying to render the models in a loop, ignoring invalid
# base errors, until the size of the unrendered models doesn't
# decrease by at least one, meaning there's a base dependency loop/
# missing base.
if not model_states:
return
# Prevent that all model caches are expired for each render.
with self.bulk_update():
unrendered_models = model_states
while unrendered_models:
new_unrendered_models = []
for model in unrendered_models:
try:
model.render(self)
except InvalidBasesError:
new_unrendered_models.append(model)
if len(new_unrendered_models) == len(unrendered_models):
raise InvalidBasesError(
"Cannot resolve bases for %r\nThis can happen if you are inheriting models from an "
"app with migrations (e.g. contrib.auth)\n in an app with no migrations; see "
"https://docs.djangoproject.com/en/%s/topics/migrations/#dependencies "
"for more" % (new_unrendered_models, '1.7')
)
unrendered_models = new_unrendered_models

get_remote_field = attrgetter('remote_field' if django.VERSION >= (1, 9) else 'rel')


if django.VERSION >= (1, 9):
def get_remote_field_model(field):
return field.remote_field.model
model = getattr(field, 'model', None)
if model:
return field.remote_field.model
else:
return field.related_model
else:
def get_remote_field_model(field):
return field.rel.to
return getattr(getattr(field, 'rel', None), 'to', None)
28 changes: 28 additions & 0 deletions mutant/db/models.py
Expand Up @@ -2,9 +2,12 @@

from django.core.exceptions import ValidationError
from django.db import models
from django.db.migrations.state import ModelState
from django.utils.six import string_types
from django.utils.translation import ugettext_lazy as _

from .. import logger
from ..compat import get_remote_field_model, StateApps
from ..state import handler as state_handler


Expand All @@ -31,6 +34,31 @@ def is_obsolete(cls):
cls._checksum != state_handler.get_checksum(cls._definition[1])
)

@classmethod
def get_model_state(cls):
return ModelState.from_model(cls)

@classmethod
def render_state(cls):
apps = StateApps([], {})
state = cls.get_model_state()
model_states = {(state.app_label, state.name): state}
for _name, field in state.fields:
related_model = get_remote_field_model(field)
if related_model:
related_model_state = ModelState.from_model(
cls._meta.apps.get_model(related_model), exclude_rels=True
)
model_states[related_model_state.app_label, related_model_state.name] = related_model_state
for base in related_model_state.bases:
if isinstance(base, string_types):
base_model_state = ModelState.from_model(
cls._meta.apps.get_model(base), exclude_rels=True
)
model_states[base_model_state.app_label, base_model_state.name] = base_model_state
apps.render_multiple(model_states.values())
return apps.all_models[state.app_label][state.name.lower()]

@classmethod
def mark_as_obsolete(cls, origin=None):
cls._is_obsolete = True
Expand Down
16 changes: 5 additions & 11 deletions mutant/management/__init__.py
Expand Up @@ -2,10 +2,8 @@

from functools import wraps

from django.apps.registry import Apps
from django.contrib.contenttypes.models import ContentType
from django.db import connections, models, transaction
from django.db.migrations.state import ModelState
from django.db.models.fields import FieldDoesNotExist

from ..compat import get_remote_field
Expand Down Expand Up @@ -106,10 +104,7 @@ def model_definition_post_delete(sender, instance, **kwargs):
def base_definition_post_save(sender, instance, created, raw, **kwargs):
declared_fields = instance.get_declared_fields()
if declared_fields:
# Make sure to flatten abstract bases since Django
# migrations can't deal with them.
state = ModelState.from_model(instance.model_def.model_class())
model_class = state.render(Apps())
model_class = instance.model_def.model_class().render_state()
opts = model_class._meta
if created:
add_columns = popattr(instance._state, '_add_columns', True)
Expand Down Expand Up @@ -147,7 +142,7 @@ def base_definition_pre_delete(sender, instance, **kwargs):
return
if (instance.base and issubclass(instance.base, models.Model) and
instance.base._meta.abstract):
instance._state._deletion = instance.model_def.model_class()
instance._state._deletion = instance.model_def.model_class().render_state()


def base_definition_post_delete(sender, instance, **kwargs):
Expand All @@ -157,8 +152,7 @@ def base_definition_post_delete(sender, instance, **kwargs):
if hasattr(instance._state, '_deletion'):
# Make sure to flatten abstract bases since Django
# migrations can't deal with them.
state = ModelState.from_model(popattr(instance._state, '_deletion'))
model = state.render(Apps())
model = popattr(instance._state, '_deletion')
for field in instance.base._meta.fields:
perform_ddl('remove_field', model, field)

Expand Down Expand Up @@ -197,7 +191,7 @@ def field_definition_post_save(sender, instance, created, raw, **kwargs):
This signal is connected by all FieldDefinition subclasses
see comment in FieldDefinitionBase for more details
"""
model_class = instance.model_def.model_class()
model_class = instance.model_def.model_class().render_state()
field = instance.construct_for_migrate()
field.model = model_class
if created:
Expand All @@ -210,7 +204,7 @@ def field_definition_post_save(sender, instance, created, raw, **kwargs):
# If the field definition is raw we must re-create the model class
# since ModelDefinitionAttribute.save won't be called
if raw:
model_class.mark_as_obsolete()
instance.model_def.model_class().mark_as_obsolete()
else:
old_field = instance._state._pre_save_field
delattr(instance._state, '_pre_save_field')
Expand Down

0 comments on commit b3d75fb

Please sign in to comment.