Refactored INSTALLED_APPS overrides.
* Introduced [un]set_installed_apps to handle changes to the INSTALLED_APPS setting. * Refactored [un]set_available_apps to share its implementation with [un]set_installed_apps. * Implemented a receiver to clear some app-related caches. * Removed test_missing_app as it is basically impossible to reproduce this situation with public methods of the new app cache.
This commit is contained in:
parent
8cff95e937
commit
5891990b6e
|
@ -1,2 +1,2 @@
|
|||
from .base import AppConfig # NOQA
|
||||
from .cache import app_cache, UnavailableApp # NOQA
|
||||
from .cache import app_cache # NOQA
|
||||
|
|
|
@ -14,10 +14,6 @@ from django.utils._os import upath
|
|||
from .base import AppConfig
|
||||
|
||||
|
||||
class UnavailableApp(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AppCache(object):
|
||||
"""
|
||||
A cache that stores installed applications and their models. Used to
|
||||
|
@ -43,9 +39,9 @@ class AppCache(object):
|
|||
# Mapping of labels to AppConfig instances for installed apps.
|
||||
self.app_configs = OrderedDict()
|
||||
|
||||
# Set of app names. Allows restricting the set of installed apps.
|
||||
# Used by TransactionTestCase.available_apps for performance reasons.
|
||||
self.available_apps = None
|
||||
# Stack of app_configs. Used to store the current state in
|
||||
# set_available_apps and set_installed_apps.
|
||||
self.stored_app_configs = []
|
||||
|
||||
# Internal flags used when populating the master cache.
|
||||
self._apps_loaded = not self.master
|
||||
|
@ -157,8 +153,6 @@ class AppCache(object):
|
|||
for app_config in self.app_configs.values():
|
||||
if only_with_models_module and app_config.models_module is None:
|
||||
continue
|
||||
if self.available_apps is not None and app_config.name not in self.available_apps:
|
||||
continue
|
||||
yield app_config
|
||||
|
||||
def get_app_config(self, app_label, only_with_models_module=False):
|
||||
|
@ -167,9 +161,6 @@ class AppCache(object):
|
|||
|
||||
Raises LookupError if no application exists with this label.
|
||||
|
||||
Raises UnavailableApp when set_available_apps() disables the
|
||||
application with this label.
|
||||
|
||||
If only_with_models_module in True (non-default), imports models and
|
||||
considers only applications containing a models module.
|
||||
"""
|
||||
|
@ -183,8 +174,6 @@ class AppCache(object):
|
|||
raise LookupError("No installed app with label %r." % app_label)
|
||||
if only_with_models_module and app_config.models_module is None:
|
||||
raise LookupError("App with label %r doesn't have a models module." % app_label)
|
||||
if self.available_apps is not None and app_config.name not in self.available_apps:
|
||||
raise UnavailableApp("App with label %r isn't available." % app_label)
|
||||
return app_config
|
||||
|
||||
def get_models(self, app_mod=None,
|
||||
|
@ -216,13 +205,7 @@ class AppCache(object):
|
|||
cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped)
|
||||
model_list = None
|
||||
try:
|
||||
model_list = self._get_models_cache[cache_key]
|
||||
if self.available_apps is not None and only_installed:
|
||||
model_list = [
|
||||
m for m in model_list
|
||||
if self.app_configs[m._meta.app_label].name in self.available_apps
|
||||
]
|
||||
return model_list
|
||||
return self._get_models_cache[cache_key]
|
||||
except KeyError:
|
||||
pass
|
||||
self.populate_models()
|
||||
|
@ -249,11 +232,6 @@ class AppCache(object):
|
|||
(not model._meta.swapped or include_swapped))
|
||||
)
|
||||
self._get_models_cache[cache_key] = model_list
|
||||
if self.available_apps is not None and only_installed:
|
||||
model_list = [
|
||||
m for m in model_list
|
||||
if self.app_configs[m._meta.app_label].name in self.available_apps
|
||||
]
|
||||
return model_list
|
||||
|
||||
def get_model(self, app_label, model_name, only_installed=True):
|
||||
|
@ -262,9 +240,6 @@ class AppCache(object):
|
|||
model_name.
|
||||
|
||||
Returns None if no model is found.
|
||||
|
||||
Raises UnavailableApp when set_available_apps() in in effect and
|
||||
doesn't include app_label.
|
||||
"""
|
||||
if not self.master:
|
||||
only_installed = False
|
||||
|
@ -273,9 +248,6 @@ class AppCache(object):
|
|||
app_config = self.app_configs.get(app_label)
|
||||
if app_config is None:
|
||||
return None
|
||||
if (self.available_apps is not None
|
||||
and app_config.name not in self.available_apps):
|
||||
raise UnavailableApp("App with label %s isn't available." % app_label)
|
||||
return self.all_models[app_label].get(model_name.lower())
|
||||
|
||||
def register_model(self, app_label, model):
|
||||
|
@ -326,22 +298,57 @@ class AppCache(object):
|
|||
available must be an iterable of application names.
|
||||
|
||||
Primarily used for performance optimization in TransactionTestCase.
|
||||
|
||||
This method is safe is the sense that it doesn't trigger any imports.
|
||||
"""
|
||||
if self.available_apps is not None:
|
||||
raise RuntimeError("set_available_apps() may be called only once "
|
||||
"in a row; make sure it's paired with unset_available_apps()")
|
||||
available = set(available)
|
||||
installed = set(app_config.name for app_config in self.get_app_configs())
|
||||
if not available.issubset(installed):
|
||||
raise ValueError("Available apps isn't a subset of installed "
|
||||
"apps, extra apps: %s" % ", ".join(available - installed))
|
||||
self.available_apps = available
|
||||
|
||||
self.stored_app_configs.append(self.app_configs)
|
||||
self.app_configs = OrderedDict(
|
||||
(label, app_config)
|
||||
for label, app_config in self.app_configs.items()
|
||||
if app_config.name in available)
|
||||
|
||||
def unset_available_apps(self):
|
||||
"""
|
||||
Cancels a previous call to set_available_apps().
|
||||
"""
|
||||
self.available_apps = None
|
||||
self.app_configs = self.stored_app_configs.pop()
|
||||
|
||||
def set_installed_apps(self, installed):
|
||||
"""
|
||||
Enables a different set of installed_apps for get_app_config[s].
|
||||
|
||||
installed must be an iterable in the same format as INSTALLED_APPS.
|
||||
|
||||
Primarily used as a receiver of the setting_changed signal in tests.
|
||||
|
||||
This method may trigger new imports, which may add new models to the
|
||||
registry of all imported models. They will stay in the registry even
|
||||
after unset_installed_apps(). Since it isn't possible to replay
|
||||
imports safely (eg. that could lead to registering listeners twice),
|
||||
models are registered when they're imported and never removed.
|
||||
"""
|
||||
self.stored_app_configs.append(self.app_configs)
|
||||
self.app_configs = OrderedDict()
|
||||
try:
|
||||
self._apps_loaded = False
|
||||
self.populate_apps()
|
||||
self._models_loaded = False
|
||||
self.populate_models()
|
||||
except Exception:
|
||||
self.unset_installed_apps()
|
||||
raise
|
||||
|
||||
def unset_installed_apps(self):
|
||||
"""
|
||||
Cancels a previous call to set_installed_apps().
|
||||
"""
|
||||
self.app_configs = self.stored_app_configs.pop()
|
||||
|
||||
### DANGEROUS METHODS ### (only used to preserve existing tests)
|
||||
|
||||
|
@ -353,15 +360,11 @@ class AppCache(object):
|
|||
else:
|
||||
app_config.import_models(self.all_models[app_config.label])
|
||||
self.app_configs[app_config.label] = app_config
|
||||
if self.available_apps is not None:
|
||||
self.available_apps.add(app_config.name)
|
||||
return app_config
|
||||
|
||||
def _end_with_app(self, app_config):
|
||||
if app_config is not None:
|
||||
del self.app_configs[app_config.label]
|
||||
if self.available_apps is not None:
|
||||
self.available_apps.discard(app_config.name)
|
||||
|
||||
@contextmanager
|
||||
def _with_app(self, app_name):
|
||||
|
@ -420,9 +423,6 @@ class AppCache(object):
|
|||
def get_app(self, app_label):
|
||||
"""
|
||||
Returns the module containing the models for the given app_label.
|
||||
|
||||
Raises UnavailableApp when set_available_apps() in in effect and
|
||||
doesn't include app_label.
|
||||
"""
|
||||
warnings.warn(
|
||||
"get_app_config(app_label).models_module supersedes get_app(app_label).",
|
||||
|
|
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||
import getpass
|
||||
import unicodedata
|
||||
|
||||
from django.apps import app_cache, UnavailableApp
|
||||
from django.apps import app_cache
|
||||
from django.contrib.auth import (models as auth_app, get_permission_codename,
|
||||
get_user_model)
|
||||
from django.core import exceptions
|
||||
|
@ -61,9 +61,7 @@ def _check_permission_clashing(custom, builtin, ctype):
|
|||
|
||||
|
||||
def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs):
|
||||
try:
|
||||
app_cache.get_model('auth', 'Permission')
|
||||
except UnavailableApp:
|
||||
if app_cache.get_model('auth', 'Permission') is None:
|
||||
return
|
||||
|
||||
if not router.allow_migrate(db, auth_app.Permission):
|
||||
|
@ -119,12 +117,11 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw
|
|||
|
||||
|
||||
def create_superuser(app, created_models, verbosity, db, **kwargs):
|
||||
try:
|
||||
app_cache.get_model('auth', 'Permission')
|
||||
UserModel = get_user_model()
|
||||
except UnavailableApp:
|
||||
if app_cache.get_model('auth', 'Permission') is None:
|
||||
return
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
if UserModel in created_models and kwargs.get('interactive', True):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.apps import app_cache, UnavailableApp
|
||||
from django.apps import app_cache
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import DEFAULT_DB_ALIAS, router
|
||||
from django.db.models import signals
|
||||
|
@ -12,9 +12,7 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, *
|
|||
Creates content types for models in the given app, removing any model
|
||||
entries that no longer have a matching model class.
|
||||
"""
|
||||
try:
|
||||
app_cache.get_model('contenttypes', 'ContentType')
|
||||
except UnavailableApp:
|
||||
if app_cache.get_model('contenttypes', 'ContentType') is None:
|
||||
return
|
||||
|
||||
if not router.allow_migrate(db, ContentType):
|
||||
|
|
|
@ -17,7 +17,7 @@ setting_changed = Signal(providing_args=["setting", "value", "enter"])
|
|||
# except for cases where the receiver is related to a contrib app.
|
||||
|
||||
# Settings that may not work well when using 'override_settings' (#19031)
|
||||
COMPLEX_OVERRIDE_SETTINGS = set(['DATABASES', 'INSTALLED_APPS'])
|
||||
COMPLEX_OVERRIDE_SETTINGS = set(['DATABASES'])
|
||||
|
||||
|
||||
@receiver(setting_changed)
|
||||
|
@ -27,6 +27,14 @@ def clear_cache_handlers(**kwargs):
|
|||
caches._caches = threading.local()
|
||||
|
||||
|
||||
@receiver(setting_changed)
|
||||
def update_installed_apps(**kwargs):
|
||||
if kwargs['setting'] == 'INSTALLED_APPS':
|
||||
# Rebuild any AppDirectoriesFinder instance.
|
||||
from django.contrib.staticfiles.finders import get_finder
|
||||
get_finder.cache_clear()
|
||||
|
||||
|
||||
@receiver(setting_changed)
|
||||
def update_connections_time_zone(**kwargs):
|
||||
if kwargs['setting'] == 'TIME_ZONE':
|
||||
|
|
|
@ -30,7 +30,7 @@ from django.forms.fields import CharField
|
|||
from django.http import QueryDict
|
||||
from django.test.client import Client
|
||||
from django.test.html import HTMLParseError, parse_html
|
||||
from django.test.signals import template_rendered
|
||||
from django.test.signals import setting_changed, template_rendered
|
||||
from django.test.utils import (CaptureQueriesContext, ContextList,
|
||||
override_settings, compare_xml)
|
||||
from django.utils.encoding import force_text
|
||||
|
@ -726,6 +726,10 @@ class TransactionTestCase(SimpleTestCase):
|
|||
super(TransactionTestCase, self)._pre_setup()
|
||||
if self.available_apps is not None:
|
||||
app_cache.set_available_apps(self.available_apps)
|
||||
setting_changed.send(sender=settings._wrapped.__class__,
|
||||
setting='INSTALLED_APPS',
|
||||
value=self.available_apps,
|
||||
enter=True)
|
||||
for db_name in self._databases_names(include_mirrors=False):
|
||||
flush.Command.emit_post_migrate(verbosity=0, interactive=False, database=db_name)
|
||||
try:
|
||||
|
@ -733,6 +737,11 @@ class TransactionTestCase(SimpleTestCase):
|
|||
except Exception:
|
||||
if self.available_apps is not None:
|
||||
app_cache.unset_available_apps()
|
||||
setting_changed.send(sender=settings._wrapped.__class__,
|
||||
setting='INSTALLED_APPS',
|
||||
value=settings.INSTALLED_APPS,
|
||||
enter=False)
|
||||
|
||||
raise
|
||||
|
||||
def _databases_names(self, include_mirrors=True):
|
||||
|
@ -786,7 +795,12 @@ class TransactionTestCase(SimpleTestCase):
|
|||
for conn in connections.all():
|
||||
conn.close()
|
||||
finally:
|
||||
if self.available_apps is not None:
|
||||
app_cache.unset_available_apps()
|
||||
setting_changed.send(sender=settings._wrapped.__class__,
|
||||
setting='INSTALLED_APPS',
|
||||
value=settings.INSTALLED_APPS,
|
||||
enter=False)
|
||||
|
||||
def _fixture_teardown(self):
|
||||
# Allow TRUNCATE ... CASCADE and don't emit the post_migrate signal
|
||||
|
|
|
@ -9,6 +9,7 @@ import warnings
|
|||
from functools import wraps
|
||||
from xml.dom.minidom import parseString, Node
|
||||
|
||||
from django.apps import app_cache
|
||||
from django.conf import settings, UserSettingsHolder
|
||||
from django.core import mail
|
||||
from django.core.signals import request_started
|
||||
|
@ -190,6 +191,8 @@ class override_settings(object):
|
|||
"""
|
||||
def __init__(self, **kwargs):
|
||||
self.options = kwargs
|
||||
# Special case that requires updating the app cache, a core feature.
|
||||
self.installed_apps = self.options.get('INSTALLED_APPS')
|
||||
|
||||
def __enter__(self):
|
||||
self.enable()
|
||||
|
@ -223,6 +226,8 @@ class override_settings(object):
|
|||
setattr(override, key, new_value)
|
||||
self.wrapped = settings._wrapped
|
||||
settings._wrapped = override
|
||||
if self.installed_apps is not None:
|
||||
app_cache.set_installed_apps(self.installed_apps)
|
||||
for key, new_value in self.options.items():
|
||||
setting_changed.send(sender=settings._wrapped.__class__,
|
||||
setting=key, value=new_value, enter=True)
|
||||
|
@ -230,6 +235,8 @@ class override_settings(object):
|
|||
def disable(self):
|
||||
settings._wrapped = self.wrapped
|
||||
del self.wrapped
|
||||
if self.installed_apps is not None:
|
||||
app_cache.unset_installed_apps()
|
||||
for key in self.options:
|
||||
new_value = getattr(settings, key, None)
|
||||
setting_changed.send(sender=settings._wrapped.__class__,
|
||||
|
|
|
@ -3,11 +3,8 @@ from __future__ import unicode_literals
|
|||
import os
|
||||
import sys
|
||||
from unittest import TestCase
|
||||
import warnings
|
||||
|
||||
from django.apps import app_cache
|
||||
from django.apps.cache import AppCache
|
||||
from django.test.utils import override_settings
|
||||
from django.utils._os import upath
|
||||
from django.utils import six
|
||||
|
||||
|
@ -69,23 +66,6 @@ class EggLoadingTest(TestCase):
|
|||
with app_cache._with_app('broken_app'):
|
||||
app_cache.get_app_config('omelet.app_no_models').models_module
|
||||
|
||||
def test_missing_app(self):
|
||||
"""
|
||||
Test that repeated app loading doesn't succeed in case there is an
|
||||
error. Refs #17667.
|
||||
"""
|
||||
app_cache = AppCache()
|
||||
# Pretend we're the master app cache to test the population process.
|
||||
app_cache._apps_loaded = False
|
||||
app_cache._models_loaded = False
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", "Overriding setting INSTALLED_APPS")
|
||||
with override_settings(INSTALLED_APPS=['notexists']):
|
||||
with self.assertRaises(ImportError):
|
||||
app_cache.get_model('notexists', 'nomodel')
|
||||
with self.assertRaises(ImportError):
|
||||
app_cache.get_model('notexists', 'nomodel')
|
||||
|
||||
|
||||
class GetModelsTest(TestCase):
|
||||
def setUp(self):
|
||||
|
|
Loading…
Reference in New Issue