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.
This commit is contained in:
Markus Holtermann 2015-08-22 23:40:34 +10:00
parent 91f701f4fc
commit e1427cc609
5 changed files with 56 additions and 29 deletions

View File

@ -260,6 +260,28 @@ class Apps(object):
"Model '%s.%s' not registered." % (app_label, model_name)) "Model '%s.%s' not registered." % (app_label, model_name))
return model 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): def set_available_apps(self, available):
""" """
Restricts the set of installed apps used by get_app_config[s]. Restricts the set of installed apps used by get_app_config[s].

View File

@ -314,17 +314,8 @@ class RelatedField(Field):
if isinstance(self.remote_field.model, six.string_types): if isinstance(self.remote_field.model, six.string_types):
to_string = self.remote_field.model to_string = self.remote_field.model
else: else:
to_string = "%s.%s" % ( to_string = self.remote_field.model._meta.label
self.remote_field.model._meta.app_label, return apps.get_swappable_settings_name(to_string)
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
return None return None
def set_attributes_from_rel(self): def set_attributes_from_rel(self):

View File

@ -396,7 +396,7 @@ class Options(object):
# or as part of validation. # or as part of validation.
return swapped_for 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 swapped_for
return None return None

View File

@ -29,7 +29,7 @@ except ImportError:
__all__ = ( __all__ = (
'Approximate', 'ContextList', 'get_runner', 'Approximate', 'ContextList', 'isolate_lru_cache', 'get_runner',
'modify_settings', 'override_settings', 'modify_settings', 'override_settings',
'requires_tz_support', 'requires_tz_support',
'setup_test_environment', 'teardown_test_environment', 'setup_test_environment', 'teardown_test_environment',
@ -503,6 +503,16 @@ def extend_sys_path(*paths):
sys.path = _orig_sys_path 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 @contextmanager
def captured_output(stream_name): def captured_output(stream_name):
"""Return a context manager used by captured_stdout/stdin/stderr """Return a context manager used by captured_stdout/stdin/stderr

View File

@ -1,7 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.apps import apps
from django.db import models from django.db import models
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, override_settings
from django.test.utils import isolate_lru_cache
from django.utils import six from django.utils import six
@ -219,6 +221,7 @@ class FieldDeconstructionTests(SimpleTestCase):
@override_settings(AUTH_USER_MODEL="auth.Permission") @override_settings(AUTH_USER_MODEL="auth.Permission")
def test_foreign_key_swapped(self): 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; # It doesn't matter that we swapped out user for permission;
# there's no validation. We just want to check the setting stuff works. # there's no validation. We just want to check the setting stuff works.
field = models.ForeignKey("auth.Permission", models.CASCADE) field = models.ForeignKey("auth.Permission", models.CASCADE)
@ -297,6 +300,7 @@ class FieldDeconstructionTests(SimpleTestCase):
@override_settings(AUTH_USER_MODEL="auth.Permission") @override_settings(AUTH_USER_MODEL="auth.Permission")
def test_many_to_many_field_swapped(self): 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; # It doesn't matter that we swapped out user for permission;
# there's no validation. We just want to check the setting stuff works. # there's no validation. We just want to check the setting stuff works.
field = models.ManyToManyField("auth.Permission") field = models.ManyToManyField("auth.Permission")