From b3d75fb44e30239804059d525dc882159309c3b9 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Mon, 21 Dec 2015 02:07:20 -0500 Subject: [PATCH] Moved the rendered state logic to the mutable model class. --- mutant/compat.py | 56 +++++++++++++++++++++++++++++++++-- mutant/db/models.py | 28 ++++++++++++++++++ mutant/management/__init__.py | 16 ++++------ 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/mutant/compat.py b/mutant/compat.py index c0ac683..7f86976 100644 --- a/mutant/compat.py +++ b/mutant/compat.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from contextlib import contextmanager from operator import attrgetter import django @@ -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() @@ -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) diff --git a/mutant/db/models.py b/mutant/db/models.py index c534412..2aff3ab 100644 --- a/mutant/db/models.py +++ b/mutant/db/models.py @@ -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 @@ -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 diff --git a/mutant/management/__init__.py b/mutant/management/__init__.py index 553b14e..5dae3b5 100644 --- a/mutant/management/__init__.py +++ b/mutant/management/__init__.py @@ -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 @@ -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) @@ -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): @@ -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) @@ -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: @@ -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')