Commit e1427cc6 authored by Markus Holtermann's avatar Markus Holtermann
Browse files

Fixed #24590 -- Cached calls to swappable_setting.

Moved the lookup in Field.swappable_setting to Apps, and added
an lru_cache to cache the results.

Refs #24743

Thanks Marten Kenbeek for the initial work on the patch. Thanks Aymeric
Augustin and Tim Graham for the review.
parent 91f701f4
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
@@ -260,6 +260,28 @@ class Apps(object):
                "Model '%s.%s' not registered." % (app_label, model_name))
        return model

    @lru_cache.lru_cache(maxsize=None)
    def get_swappable_settings_name(self, to_string):
        """
        For a given model string (e.g. "auth.User"), return the name of the
        corresponding settings name if it refers to a swappable model. If the
        referred model is not swappable, return None.

        This method is decorated with lru_cache because it's performance
        critical when it comes to migrations. Since the swappable settings don't
        change after Django has loaded the settings, there is no reason to get
        the respective settings attribute over and over again.
        """
        for model in self.get_models(include_swapped=True):
            swapped = model._meta.swapped
            # Is this model swapped out for the model given by to_string?
            if swapped and swapped == to_string:
                return model._meta.swappable
            # Is this model swappable and the one given by to_string?
            if model._meta.swappable and model._meta.label == to_string:
                return model._meta.swappable
        return None

    def set_available_apps(self, available):
        """
        Restricts the set of installed apps used by get_app_config[s].
+2 −11
Original line number Diff line number Diff line
@@ -314,17 +314,8 @@ class RelatedField(Field):
            if isinstance(self.remote_field.model, six.string_types):
                to_string = self.remote_field.model
            else:
                to_string = "%s.%s" % (
                    self.remote_field.model._meta.app_label,
                    self.remote_field.model._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
                to_string = self.remote_field.model._meta.label
            return apps.get_swappable_settings_name(to_string)
        return None

    def set_attributes_from_rel(self):
+1 −1
Original line number Diff line number Diff line
@@ -396,7 +396,7 @@ class Options(object):
                    # or as part of validation.
                    return swapped_for

                if '%s.%s' % (swapped_label, swapped_object.lower()) not in (None, self.label_lower):
                if '%s.%s' % (swapped_label, swapped_object.lower()) != self.label_lower:
                    return swapped_for
        return None

+11 −1
Original line number Diff line number Diff line
@@ -29,7 +29,7 @@ except ImportError:


__all__ = (
    'Approximate', 'ContextList', 'get_runner',
    'Approximate', 'ContextList', 'isolate_lru_cache', 'get_runner',
    'modify_settings', 'override_settings',
    'requires_tz_support',
    'setup_test_environment', 'teardown_test_environment',
@@ -503,6 +503,16 @@ def extend_sys_path(*paths):
        sys.path = _orig_sys_path


@contextmanager
def isolate_lru_cache(lru_cache_object):
    """Clear the cache of an LRU cache object on entering and exiting."""
    lru_cache_object.cache_clear()
    try:
        yield
    finally:
        lru_cache_object.cache_clear()


@contextmanager
def captured_output(stream_name):
    """Return a context manager used by captured_stdout/stdin/stderr
+20 −16
Original line number Diff line number Diff line
from __future__ import unicode_literals

from django.apps import apps
from django.db import models
from django.test import SimpleTestCase, override_settings
from django.test.utils import isolate_lru_cache
from django.utils import six


@@ -219,6 +221,7 @@ class FieldDeconstructionTests(SimpleTestCase):

    @override_settings(AUTH_USER_MODEL="auth.Permission")
    def test_foreign_key_swapped(self):
        with isolate_lru_cache(apps.get_swappable_settings_name):
            # It doesn't matter that we swapped out user for permission;
            # there's no validation. We just want to check the setting stuff works.
            field = models.ForeignKey("auth.Permission", models.CASCADE)
@@ -297,6 +300,7 @@ class FieldDeconstructionTests(SimpleTestCase):

    @override_settings(AUTH_USER_MODEL="auth.Permission")
    def test_many_to_many_field_swapped(self):
        with isolate_lru_cache(apps.get_swappable_settings_name):
            # It doesn't matter that we swapped out user for permission;
            # there's no validation. We just want to check the setting stuff works.
            field = models.ManyToManyField("auth.Permission")