From 1a9f13df59e83eb6e0c20f96d878826519eca62a Mon Sep 17 00:00:00 2001 From: Anubhav Joshi Date: Thu, 19 Dec 2013 03:01:48 +0530 Subject: [PATCH 001/208] Fixed #21478 -- Corrected docs for when Field.db_type() is called. --- docs/howto/custom-model-fields.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 6054a83f10..b7bcefad53 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -458,11 +458,14 @@ For example:: else: return 'timestamp' -The :meth:`.db_type` method is only called by Django when the framework +The :meth:`.db_type` method is called by Django when the framework constructs the ``CREATE TABLE`` statements for your application -- that is, -when you first create your tables. It's not called at any other time, so it can -afford to execute slightly complex code, such as the -``connection.settings_dict`` check in the above example. +when you first create your tables. It is also called when constructing a +``WHERE`` clause that includes the model field -- that is, when you retrieve data +using QuerySet methods like ``get()``, ``filter()``, and ``exclude()`` and have +the model field as an argument. It's not called at any other time, so it can afford to +execute slightly complex code, such as the ``connection.settings_dict`` check in +the above example. Some database column types accept parameters, such as ``CHAR(25)``, where the parameter ``25`` represents the maximum column length. In cases like these, From 7f2485b4d180956aa556a88ed1d9754eeeeea054 Mon Sep 17 00:00:00 2001 From: Alexey Voronov Date: Sun, 22 Dec 2013 00:03:17 +0200 Subject: [PATCH 002/208] Fixed #21643 -- repeated execution of qs with F() + timedelta Thanks Tim Graham for review. --- django/db/models/sql/expressions.py | 1 + tests/expressions_regress/tests.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index bd661b5adf..9f29e2ace5 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -111,6 +111,7 @@ class SQLEvaluator(object): def evaluate_date_modifier_node(self, node, qn, connection): timedelta = node.children.pop() sql, params = self.evaluate_node(node, qn, connection) + node.children.append(timedelta) if (timedelta.days == timedelta.seconds == timedelta.microseconds == 0): return sql, params diff --git a/tests/expressions_regress/tests.py b/tests/expressions_regress/tests.py index e603c410ac..7533e4ee39 100644 --- a/tests/expressions_regress/tests.py +++ b/tests/expressions_regress/tests.py @@ -274,6 +274,13 @@ class FTimeDeltaTests(TestCase): self.days_long.append(e4.completed - e4.assigned) self.expnames = [e.name for e in Experiment.objects.all()] + def test_multiple_query_compilation(self): + # Ticket #21643 + queryset = Experiment.objects.filter(end__lt=F('start') + datetime.timedelta(hours=1)) + q1 = str(queryset.query) + q2 = str(queryset.query) + self.assertEqual(q1, q2) + def test_delta_add(self): for i in range(len(self.deltas)): delta = self.deltas[i] From 73c9e65b75cd956410ae71f39fb2f6a9b7b45b42 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 18 Dec 2013 10:34:20 +0100 Subject: [PATCH 003/208] Added a context manager to hold the import lock. --- django/core/apps/cache.py | 8 ++------ django/utils/module_loading.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index b076fc1968..d07c3bbe34 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -1,7 +1,6 @@ "Utilities for loading models and the modules that contain them." from collections import OrderedDict -import imp from importlib import import_module import os import sys @@ -9,7 +8,7 @@ import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils.module_loading import module_has_submodule +from django.utils.module_loading import import_lock, module_has_submodule from django.utils._os import upath from django.utils import six @@ -74,8 +73,7 @@ class AppCache(object): # without holding the importer lock and another thread then tries to # import something which also launches the app loading. For details of # this situation see #18251. - imp.acquire_lock() - try: + with import_lock(): if self.loaded: return for app_name in settings.INSTALLED_APPS: @@ -86,8 +84,6 @@ class AppCache(object): for app_name in self.postponed: self.load_app(app_name) self.loaded = True - finally: - imp.release_lock() def load_app(self, app_name, can_postpone=False): """ diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index 8868a2d3ab..0c6bad2a7b 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -1,5 +1,6 @@ from __future__ import absolute_import # Avoid importing `importlib` from this package. +from contextlib import contextmanager import copy import imp from importlib import import_module @@ -35,6 +36,18 @@ def import_by_path(dotted_path, error_prefix=''): return attr +@contextmanager +def import_lock(): + """ + Context manager that aquires the import lock. + """ + imp.acquire_lock() + try: + yield + finally: + imp.release_lock() + + def autodiscover_modules(*args, **kwargs): """ Auto-discover INSTALLED_APPS modules and fail silently when From 742ed9878e7edbb7a11667c489c719c4d9ab82de Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 18 Dec 2013 11:19:56 +0100 Subject: [PATCH 004/208] Refactored registration of models. Got rid of AppConfig._stub. As a side effect, app_cache.app_configs now only contains entries for applications that are in INSTALLED_APPS, which is a good thing and will allow dramatic simplifications (which I will perform in the next commit). That required adjusting all methods that iterate on app_configs without checking the "installed" flag, hence the large changes in get_model[s]. Introduced AppCache.all_models to store models: - while the app cache is being populated and a suitable app config object to register models isn't available yet; - for applications that aren't in INSTALLED_APPS since they don't have an app config any longer. Replaced get_model(seed_cache=False) by registered_model() which can be kept simple and safe to call at any time, and removed the seed_cache argument to get_model[s]. There's no replacement for that private API. Allowed non-master app caches to go through populate() as it is now safe to do so. They were introduced in 1.7 so backwards compatibility isn't a concern as long as the migrations framework keeps working. --- django/core/apps/base.py | 4 -- django/core/apps/cache.py | 80 ++++++++++++++------------ django/db/models/base.py | 6 +- django/db/models/fields/related.py | 3 +- tests/app_loading/tests.py | 15 ++--- tests/invalid_models/tests.py | 1 + tests/managers_regress/tests.py | 3 + tests/migrations/test_commands.py | 1 + tests/proxy_model_inheritance/tests.py | 2 + tests/proxy_models/tests.py | 1 + tests/tablespaces/tests.py | 1 + 11 files changed, 64 insertions(+), 53 deletions(-) diff --git a/django/core/apps/base.py b/django/core/apps/base.py index 860777bb03..5991029c09 100644 --- a/django/core/apps/base.py +++ b/django/core/apps/base.py @@ -39,9 +39,5 @@ class AppConfig(object): # This is a unicode object on Python 2 and a str on Python 3. self.path = upath(app_module.__path__[0]) if app_module is not None else None - @classmethod - def _stub(cls, label): - return cls(label, None, None) - def __repr__(self): return '' % self.label diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index d07c3bbe34..deffc84586 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -1,6 +1,6 @@ "Utilities for loading models and the modules that contain them." -from collections import OrderedDict +from collections import defaultdict, OrderedDict from importlib import import_module import os import sys @@ -10,7 +10,6 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils.module_loading import import_lock, module_has_submodule from django.utils._os import upath -from django.utils import six from .base import AppConfig @@ -39,6 +38,11 @@ class AppCache(object): # get_model[s]. self.master = master + # Mapping of app labels => model names => model classes. Used to + # register models before the app cache is populated and also for + # applications that aren't installed. + self.all_models = defaultdict(OrderedDict) + # Mapping of labels to AppConfig instances for installed apps. self.app_configs = OrderedDict() @@ -118,10 +122,7 @@ class AppCache(object): app_config = AppConfig( name=app_name, app_module=app_module, models_module=models_module) - # If a stub config existed for this app, preserve models registry. - old_app_config = self.app_configs.get(app_config.label) - if old_app_config is not None: - app_config.models = old_app_config.models + app_config.models = self.all_models[app_config.label] self.app_configs[app_config.label] = app_config return models_module @@ -145,6 +146,8 @@ class AppCache(object): If only_with_models_module in True (non-default), only applications containing a models module are considered. """ + if not only_installed: + raise ValueError("only_installed=False isn't supported any more.") self.populate() for app_config in self.app_configs.values(): if only_installed and not app_config.installed: @@ -170,6 +173,8 @@ class AppCache(object): If only_with_models_module in True (non-default), only applications containing a models module are considered. """ + if not only_installed: + raise ValueError("only_installed=False isn't supported any more.") self.populate() app_config = self.app_configs.get(app_label) if app_config is None: @@ -223,20 +228,22 @@ class AppCache(object): self.populate() if app_mod: app_label = app_mod.__name__.split('.')[-2] - try: - app_config = self.app_configs[app_label] - except KeyError: - app_list = [] - else: - app_list = [app_config] if app_config.installed else [] - else: - app_list = six.itervalues(self.app_configs) if only_installed: - app_list = (app for app in app_list if app.installed) + try: + model_dicts = [self.app_configs[app_label].models] + except KeyError: + model_dicts = [] + else: + model_dicts = [self.all_models[app_label]] + else: + if only_installed: + model_dicts = [app_config.models for app_config in self.app_configs.values()] + else: + model_dicts = self.all_models.values() model_list = [] - for app in app_list: + for model_dict in model_dicts: model_list.extend( - model for model in app.models.values() + model for model in model_dict.values() if ((not model._deferred or include_deferred) and (not model._meta.auto_created or include_auto_created) and (not model._meta.swapped or include_swapped)) @@ -249,8 +256,7 @@ class AppCache(object): ] return model_list - def get_model(self, app_label, model_name, - seed_cache=True, only_installed=True): + def get_model(self, app_label, model_name, only_installed=True): """ Returns the model matching the given app_label and case-insensitive model_name. @@ -262,43 +268,45 @@ class AppCache(object): """ if not self.master: only_installed = False - if seed_cache: - self.populate() + self.populate() if only_installed: app_config = self.app_configs.get(app_label) - if app_config is not None and not app_config.installed: + if app_config is None or not app_config.installed: 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) - try: - return self.app_configs[app_label].models[model_name.lower()] - except KeyError: - return None + return self.all_models[app_label].get(model_name.lower()) def register_model(self, app_label, model): - try: - app_config = self.app_configs[app_label] - except KeyError: - app_config = AppConfig._stub(app_label) - self.app_configs[app_label] = app_config - # Add the model to the app_config's models dictionary. + # Since this method is called when models are imported, it cannot + # perform imports because of the risk of import loops. It mustn't + # call get_app_config(). model_name = model._meta.model_name - model_dict = app_config.models - if model_name in model_dict: + models = self.all_models[app_label] + if model_name in models: # The same model may be imported via different paths (e.g. # appname.models and project.appname.models). We use the source # filename as a means to detect identity. fname1 = os.path.abspath(upath(sys.modules[model.__module__].__file__)) - fname2 = os.path.abspath(upath(sys.modules[model_dict[model_name].__module__].__file__)) + fname2 = os.path.abspath(upath(sys.modules[models[model_name].__module__].__file__)) # Since the filename extension could be .py the first time and # .pyc or .pyo the second time, ignore the extension when # comparing. if os.path.splitext(fname1)[0] == os.path.splitext(fname2)[0]: return - model_dict[model_name] = model + models[model_name] = model self._get_models_cache.clear() + def registered_model(self, app_label, model_name): + """ + Test if a model is registered and return the model class or None. + + It's safe to call this method at import time, even while the app cache + is being populated. + """ + return self.all_models[app_label].get(model_name.lower()) + def set_available_apps(self, available): available = set(available) installed = set(settings.INSTALLED_APPS) diff --git a/django/db/models/base.py b/django/db/models/base.py index 56973d4e38..a694350b97 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -151,8 +151,7 @@ class ModelBase(type): new_class._base_manager = new_class._base_manager._copy_to_model(new_class) # Bail out early if we have already created this class. - m = new_class._meta.app_cache.get_model(new_class._meta.app_label, name, - seed_cache=False, only_installed=False) + m = new_class._meta.app_cache.registered_model(new_class._meta.app_label, name) if m is not None: return m @@ -279,8 +278,7 @@ class ModelBase(type): # the first time this model tries to register with the framework. There # should only be one class for each model, so we always return the # registered version. - return new_class._meta.app_cache.get_model(new_class._meta.app_label, name, - seed_cache=False, only_installed=False) + return new_class._meta.app_cache.registered_model(new_class._meta.app_label, name) def copy_managers(cls, base_managers): # This is in-place sorting of an Options attribute, but that's fine. diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 3eb6799a5f..1b141af099 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -68,8 +68,7 @@ def add_lazy_relation(cls, field, relation, operation): # string right away. If get_model returns None, it means that the related # model isn't loaded yet, so we need to pend the relation until the class # is prepared. - model = cls._meta.app_cache.get_model(app_label, model_name, - seed_cache=False, only_installed=False) + model = cls._meta.app_cache.registered_model(app_label, model_name) if model: operation(field, model, cls) else: diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index b32f510a24..e282306ec0 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -22,6 +22,7 @@ class EggLoadingTest(TestCase): def tearDown(self): app_cache.app_configs['app_loading'].models = self._old_models + app_cache.all_models['app_loading'] = self._old_models app_cache._get_models_cache = {} sys.path = self.old_path @@ -80,9 +81,9 @@ class EggLoadingTest(TestCase): app_cache.master = True with override_settings(INSTALLED_APPS=('notexists',)): with self.assertRaises(ImportError): - app_cache.get_model('notexists', 'nomodel', seed_cache=True) + app_cache.get_model('notexists', 'nomodel') with self.assertRaises(ImportError): - app_cache.get_model('notexists', 'nomodel', seed_cache=True) + app_cache.get_model('notexists', 'nomodel') class GetModelsTest(TestCase): @@ -101,17 +102,17 @@ class GetModelsTest(TestCase): self.not_installed_module.NotInstalledModel) def test_get_models_only_returns_installed_models(self): - self.assertFalse( - "NotInstalledModel" in + self.assertNotIn( + "NotInstalledModel", [m.__name__ for m in app_cache.get_models()]) def test_get_models_with_app_label_only_returns_installed_models(self): self.assertEqual(app_cache.get_models(self.not_installed_module), []) def test_get_models_with_not_installed(self): - self.assertTrue( - "NotInstalledModel" in [ - m.__name__ for m in app_cache.get_models(only_installed=False)]) + self.assertIn( + "NotInstalledModel", + [m.__name__ for m in app_cache.get_models(only_installed=False)]) class NotInstalledModelsTest(TestCase): diff --git a/tests/invalid_models/tests.py b/tests/invalid_models/tests.py index 712484c611..10d8fa47a7 100644 --- a/tests/invalid_models/tests.py +++ b/tests/invalid_models/tests.py @@ -24,6 +24,7 @@ class InvalidModelTestCase(unittest.TestCase): def tearDown(self): app_cache.app_configs['invalid_models'].models = self._old_models + app_cache.all_models['invalid_models'] = self._old_models app_cache._get_models_cache = {} sys.stdout = self.old_stdout diff --git a/tests/managers_regress/tests.py b/tests/managers_regress/tests.py index f2897862ec..b9c8244612 100644 --- a/tests/managers_regress/tests.py +++ b/tests/managers_regress/tests.py @@ -128,6 +128,7 @@ class ManagersRegressionTests(TestCase): finally: app_cache.app_configs['managers_regress'].models = _old_models + app_cache.all_models['managers_regress'] = _old_models app_cache._get_models_cache = {} @override_settings(TEST_SWAPPABLE_MODEL='managers_regress.Parent') @@ -155,6 +156,7 @@ class ManagersRegressionTests(TestCase): finally: app_cache.app_configs['managers_regress'].models = _old_models + app_cache.all_models['managers_regress'] = _old_models app_cache._get_models_cache = {} @override_settings(TEST_SWAPPABLE_MODEL='managers_regress.Parent') @@ -182,6 +184,7 @@ class ManagersRegressionTests(TestCase): finally: app_cache.app_configs['managers_regress'].models = _old_models + app_cache.all_models['managers_regress'] = _old_models app_cache._get_models_cache = {} def test_regress_3871(self): diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 59cd56e78b..13514cac91 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -135,6 +135,7 @@ class MakeMigrationsTests(MigrationTestBase): def tearDown(self): app_cache.app_configs['migrations'].models = self._old_models + app_cache.all_models['migrations'] = self._old_models app_cache._get_models_cache = {} os.chdir(self.test_dir) diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 861ab4af17..601fdc5b42 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -34,6 +34,8 @@ class ProxyModelInheritanceTests(TransactionTestCase): sys.path = self.old_sys_path del app_cache.app_configs['app1'] del app_cache.app_configs['app2'] + del app_cache.all_models['app1'] + del app_cache.all_models['app2'] def test_table_exists(self): try: diff --git a/tests/proxy_models/tests.py b/tests/proxy_models/tests.py index 3389f3597f..0995a778c0 100644 --- a/tests/proxy_models/tests.py +++ b/tests/proxy_models/tests.py @@ -175,6 +175,7 @@ class ProxyModelTests(TestCase): proxy = True finally: app_cache.app_configs['proxy_models'].models = _old_models + app_cache.all_models['proxy_models'] = _old_models app_cache._get_models_cache = {} def test_myperson_manager(self): diff --git a/tests/tablespaces/tests.py b/tests/tablespaces/tests.py index 4a62ad5a45..fa90704c45 100644 --- a/tests/tablespaces/tests.py +++ b/tests/tablespaces/tests.py @@ -36,6 +36,7 @@ class TablespacesTests(TestCase): model._meta.managed = False app_cache.app_configs['tablespaces'].models = self._old_models + app_cache.all_models['tablespaces'] = self._old_models app_cache._get_models_cache = {} def assertNumContains(self, haystack, needle, count): From 972babc3b45971a69a6b2a49fc498d92db674cae Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 18 Dec 2013 13:10:23 +0100 Subject: [PATCH 005/208] Removed the only_installed argument of get_app_config[s]. It wasn't used anywhere and couldn't be implemented any more since non-installed apps no longer have a configuration. --- django/core/apps/cache.py | 20 +++----------------- tests/admin_scripts/tests.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index deffc84586..69f2f2c974 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -136,29 +136,22 @@ class AppCache(object): """ return self.loaded - def get_app_configs(self, only_installed=True, only_with_models_module=False): + def get_app_configs(self, only_with_models_module=False): """ Return an iterable of application configurations. - If only_installed is True (default), only applications explicitly - listed in INSTALLED_APPS are considered. - If only_with_models_module in True (non-default), only applications containing a models module are considered. """ - if not only_installed: - raise ValueError("only_installed=False isn't supported any more.") self.populate() for app_config in self.app_configs.values(): - if only_installed and not app_config.installed: - continue 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_installed=True, only_with_models_module=False): + def get_app_config(self, app_label, only_with_models_module=False): """ Returns the application configuration for the given app_label. @@ -167,20 +160,13 @@ class AppCache(object): Raises UnavailableApp when set_available_apps() disables the application with this app_label. - If only_installed is True (default), only applications explicitly - listed in INSTALLED_APPS are considered. - If only_with_models_module in True (non-default), only applications containing a models module are considered. """ - if not only_installed: - raise ValueError("only_installed=False isn't supported any more.") self.populate() app_config = self.app_configs.get(app_label) if app_config is None: - raise LookupError("No app with label %r." % app_label) - if only_installed and not app_config.installed: - raise LookupError("App with label %r isn't in INSTALLED_APPS." % app_label) + 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: diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 8f693bfd34..db0b8d6030 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -379,14 +379,14 @@ class DjangoAdminMinimalSettings(AdminScriptTestCase): args = ['sqlall', '--settings=test_project.settings', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, "No app with label 'admin_scripts'.") + self.assertOutput(err, "No installed app with label 'admin_scripts'.") def test_builtin_with_environment(self): "minimal: django-admin builtin commands fail if settings are provided in the environment" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args, 'test_project.settings') self.assertNoOutput(out) - self.assertOutput(err, "No app with label 'admin_scripts'.") + self.assertOutput(err, "No installed app with label 'admin_scripts'.") def test_builtin_with_bad_settings(self): "minimal: django-admin builtin commands fail if settings file (from argument) doesn't exist" @@ -815,21 +815,21 @@ class ManageMinimalSettings(AdminScriptTestCase): args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, "No app with label 'admin_scripts'.") + self.assertOutput(err, "No installed app with label 'admin_scripts'.") def test_builtin_with_settings(self): "minimal: manage.py builtin commands fail if settings are provided as argument" args = ['sqlall', '--settings=test_project.settings', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, "No app with label 'admin_scripts'.") + self.assertOutput(err, "No installed app with label 'admin_scripts'.") def test_builtin_with_environment(self): "minimal: manage.py builtin commands fail if settings are provided in the environment" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args, 'test_project.settings') self.assertNoOutput(out) - self.assertOutput(err, "No app with label 'admin_scripts'.") + self.assertOutput(err, "No installed app with label 'admin_scripts'.") def test_builtin_with_bad_settings(self): "minimal: manage.py builtin commands fail if settings file (from argument) doesn't exist" @@ -964,7 +964,7 @@ class ManageMultipleSettings(AdminScriptTestCase): args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, "No app with label 'admin_scripts'.") + self.assertOutput(err, "No installed app with label 'admin_scripts'.") def test_builtin_with_settings(self): "multiple: manage.py builtin commands succeed if settings are provided as argument" @@ -1442,13 +1442,13 @@ class CommandTypes(AdminScriptTestCase): "User AppCommands can execute when a single app name is provided" args = ['app_command', 'NOT_AN_APP'] out, err = self.run_manage(args) - self.assertOutput(err, "No app with label 'NOT_AN_APP'.") + self.assertOutput(err, "No installed app with label 'NOT_AN_APP'.") def test_app_command_some_invalid_appnames(self): "User AppCommands can execute when some of the provided app names are invalid" args = ['app_command', 'auth', 'NOT_AN_APP'] out, err = self.run_manage(args) - self.assertOutput(err, "No app with label 'NOT_AN_APP'.") + self.assertOutput(err, "No installed app with label 'NOT_AN_APP'.") def test_label_command(self): "User LabelCommands can execute when a label is provided" From 9b3389b7268e48c16ed79256e516af787ad652df Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 18 Dec 2013 13:16:33 +0100 Subject: [PATCH 006/208] Removed the app_config.installed flag. Since applications that aren't installed no longer have an application configuration, it is now always True in practice. Provided an abstraction to temporarily add or remove applications as several tests messed with app_config.installed to achieve this effect. For now this API is _-prefixed because it looks dangerous. --- django/contrib/contenttypes/tests.py | 23 ++++--- django/contrib/gis/tests/geoapp/test_feeds.py | 6 +- .../contrib/gis/tests/geoapp/test_sitemaps.py | 6 +- django/contrib/sitemaps/tests/base.py | 4 -- django/contrib/sitemaps/tests/test_http.py | 15 +++-- django/contrib/sites/tests.py | 14 ++--- django/core/apps/base.py | 6 +- django/core/apps/cache.py | 63 ++++++++++++++++++- django/db/models/options.py | 4 +- tests/admin_docs/tests.py | 18 ++---- 10 files changed, 99 insertions(+), 60 deletions(-) diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index 756430f393..b194755b80 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals -from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.views import shortcut -from django.contrib.sites.models import Site, get_current_site +from django.contrib.sites.models import get_current_site +from django.core.apps import app_cache +from django.db import models from django.http import HttpRequest, Http404 from django.test import TestCase from django.test.utils import override_settings @@ -54,11 +55,9 @@ class FooWithBrokenAbsoluteUrl(FooWithoutUrl): class ContentTypesTests(TestCase): def setUp(self): - self._old_installed = Site._meta.app_config.installed ContentType.objects.clear_cache() def tearDown(self): - Site._meta.app_config.installed = self._old_installed ContentType.objects.clear_cache() def test_lookup_cache(self): @@ -223,15 +222,15 @@ class ContentTypesTests(TestCase): user_ct = ContentType.objects.get_for_model(FooWithUrl) obj = FooWithUrl.objects.create(name="john") - Site._meta.app_config.installed = True - response = shortcut(request, user_ct.id, obj.id) - self.assertEqual("http://%s/users/john/" % get_current_site(request).domain, - response._headers.get("location")[1]) + with app_cache._with_app('django.contrib.sites'): + response = shortcut(request, user_ct.id, obj.id) + self.assertEqual("http://%s/users/john/" % get_current_site(request).domain, + response._headers.get("location")[1]) - Site._meta.app_config.installed = False - response = shortcut(request, user_ct.id, obj.id) - self.assertEqual("http://Example.com/users/john/", - response._headers.get("location")[1]) + with app_cache._without_app('django.contrib.sites'): + response = shortcut(request, user_ct.id, obj.id) + self.assertEqual("http://Example.com/users/john/", + response._headers.get("location")[1]) def test_shortcut_view_without_get_absolute_url(self): """ diff --git a/django/contrib/gis/tests/geoapp/test_feeds.py b/django/contrib/gis/tests/geoapp/test_feeds.py index 0817e65cb4..6f0d901a62 100644 --- a/django/contrib/gis/tests/geoapp/test_feeds.py +++ b/django/contrib/gis/tests/geoapp/test_feeds.py @@ -7,6 +7,7 @@ from django.conf import settings from django.contrib.sites.models import Site from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import HAS_SPATIAL_DB +from django.core.apps import app_cache from django.test import TestCase if HAS_GEOS: @@ -20,11 +21,10 @@ class GeoFeedTest(TestCase): def setUp(self): Site(id=settings.SITE_ID, domain="example.com", name="example.com").save() - self._old_installed = Site._meta.app_config.installed - Site._meta.app_config.installed = True + self._with_sites = app_cache._begin_with_app('django.contrib.sites') def tearDown(self): - Site._meta.app_config.installed = self._old_installed + app_cache._end_with_app(self._with_sites) def assertChildNodes(self, elem, expected): "Taken from syndication/tests.py." diff --git a/django/contrib/gis/tests/geoapp/test_sitemaps.py b/django/contrib/gis/tests/geoapp/test_sitemaps.py index ee9974ceaa..035d97af34 100644 --- a/django/contrib/gis/tests/geoapp/test_sitemaps.py +++ b/django/contrib/gis/tests/geoapp/test_sitemaps.py @@ -10,6 +10,7 @@ from django.conf import settings from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import HAS_SPATIAL_DB from django.contrib.sites.models import Site +from django.core.apps import app_cache from django.test import TestCase from django.test.utils import IgnoreDeprecationWarningsMixin from django.utils._os import upath @@ -26,11 +27,10 @@ class GeoSitemapTest(IgnoreDeprecationWarningsMixin, TestCase): def setUp(self): super(GeoSitemapTest, self).setUp() Site(id=settings.SITE_ID, domain="example.com", name="example.com").save() - self._old_installed = Site._meta.app_config.installed - Site._meta.app_config.installed = True + self._with_sites = app_cache._begin_with_app('django.contrib.sites') def tearDown(self): - Site._meta.app_config.installed = self._old_installed + app_cache._end_with_app(self._with_sites) super(GeoSitemapTest, self).tearDown() def assertChildNodes(self, elem, expected): diff --git a/django/contrib/sitemaps/tests/base.py b/django/contrib/sitemaps/tests/base.py index 98abb3dca4..ecddcc737b 100644 --- a/django/contrib/sitemaps/tests/base.py +++ b/django/contrib/sitemaps/tests/base.py @@ -25,10 +25,6 @@ class SitemapTestsBase(TestCase): def setUp(self): self.base_url = '%s://%s' % (self.protocol, self.domain) - self._old_installed = Site._meta.app_config.installed cache.clear() # Create an object for sitemap content. TestModel.objects.create(name='Test Object') - - def tearDown(self): - Site._meta.app_config.installed = self._old_installed diff --git a/django/contrib/sitemaps/tests/test_http.py b/django/contrib/sitemaps/tests/test_http.py index 61be8c842a..ed61c9950b 100644 --- a/django/contrib/sitemaps/tests/test_http.py +++ b/django/contrib/sitemaps/tests/test_http.py @@ -7,6 +7,7 @@ from unittest import skipUnless from django.conf import settings from django.contrib.sitemaps import Sitemap, GenericSitemap from django.contrib.sites.models import Site +from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.test.utils import override_settings from django.utils.formats import localize @@ -108,15 +109,14 @@ class HTTPSitemapTests(SitemapTestsBase): def test_requestsite_sitemap(self): # Make sure hitting the flatpages sitemap without the sites framework # installed doesn't raise an exception. - # Reset by SitemapTestsBase.tearDown(). - Site._meta.app_config.installed = False - response = self.client.get('/simple/sitemap.xml') - expected_content = """ + with app_cache._without_app('django.contrib.sites'): + response = self.client.get('/simple/sitemap.xml') + expected_content = """ http://testserver/location/%snever0.5 """ % date.today() - self.assertXMLEqual(response.content.decode('utf-8'), expected_content) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) @skipUnless("django.contrib.sites" in settings.INSTALLED_APPS, "django.contrib.sites app not installed.") @@ -134,9 +134,8 @@ class HTTPSitemapTests(SitemapTestsBase): Sitemap.get_urls if Site objects exists, but the sites framework is not actually installed. """ - # Reset by SitemapTestsBase.tearDown(). - Site._meta.app_config.installed = False - self.assertRaises(ImproperlyConfigured, Sitemap().get_urls) + with app_cache._without_app('django.contrib.sites'): + self.assertRaises(ImproperlyConfigured, Sitemap().get_urls) def test_sitemap_item(self): """ diff --git a/django/contrib/sites/tests.py b/django/contrib/sites/tests.py index 2be28a1429..8d76fd0d02 100644 --- a/django/contrib/sites/tests.py +++ b/django/contrib/sites/tests.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.conf import settings from django.contrib.sites.models import Site, RequestSite, get_current_site +from django.core.apps import app_cache from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.http import HttpRequest from django.test import TestCase @@ -12,11 +13,10 @@ class SitesFrameworkTests(TestCase): def setUp(self): Site(id=settings.SITE_ID, domain="example.com", name="example.com").save() - self._old_installed = Site._meta.app_config.installed - Site._meta.app_config.installed = True + self._with_sites = app_cache._begin_with_app('django.contrib.sites') def tearDown(self): - Site._meta.app_config.installed = self._old_installed + app_cache._end_with_app(self._with_sites) def test_save_another(self): # Regression for #17415 @@ -67,10 +67,10 @@ class SitesFrameworkTests(TestCase): self.assertRaises(ObjectDoesNotExist, get_current_site, request) # A RequestSite is returned if the sites framework is not installed - Site._meta.app_config.installed = False - site = get_current_site(request) - self.assertTrue(isinstance(site, RequestSite)) - self.assertEqual(site.name, "example.com") + with app_cache._without_app('django.contrib.sites'): + site = get_current_site(request) + self.assertTrue(isinstance(site, RequestSite)) + self.assertEqual(site.name, "example.com") def test_domain_name_with_whitespaces(self): # Regression for #17320 diff --git a/django/core/apps/base.py b/django/core/apps/base.py index 5991029c09..332a066bcb 100644 --- a/django/core/apps/base.py +++ b/django/core/apps/base.py @@ -30,14 +30,10 @@ class AppConfig(object): # Populated by calls to AppCache.register_model(). self.models = OrderedDict() - # Whether the app is in INSTALLED_APPS or was automatically created - # when one of its models was imported. - self.installed = app_module is not None - # Filesystem path to the application directory eg. # u'/usr/lib/python2.7/dist-packages/django/contrib/admin'. # This is a unicode object on Python 2 and a str on Python 3. - self.path = upath(app_module.__path__[0]) if app_module is not None else None + self.path = upath(app_module.__path__[0]) def __repr__(self): return '' % self.label diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index 69f2f2c974..fb7bb6172e 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -1,6 +1,7 @@ "Utilities for loading models and the modules that contain them." from collections import defaultdict, OrderedDict +from contextlib import contextmanager from importlib import import_module import os import sys @@ -120,8 +121,7 @@ class AppCache(object): finally: self.nesting_level -= 1 - app_config = AppConfig( - name=app_name, app_module=app_module, models_module=models_module) + app_config = AppConfig(app_name, app_module, models_module) app_config.models = self.all_models[app_config.label] self.app_configs[app_config.label] = app_config @@ -257,7 +257,7 @@ class AppCache(object): self.populate() if only_installed: app_config = self.app_configs.get(app_label) - if app_config is None or not app_config.installed: + if app_config is None: return None if (self.available_apps is not None and app_config.name not in self.available_apps): @@ -304,6 +304,63 @@ class AppCache(object): def unset_available_apps(self): self.available_apps = None + ### DANGEROUS METHODS ### (only used to preserve existing tests) + + def _begin_with_app(self, app_name): + # Returns an opaque value that can be passed to _end_with_app(). + app_module = import_module(app_name) + models_module = import_module('%s.models' % app_name) + app_config = AppConfig(app_name, app_module, models_module) + if app_config.label in self.app_configs: + return None + else: + app_config.models = self.all_models[app_config.label] + self.app_configs[app_config.label] = app_config + return app_config + + def _end_with_app(self, app_config): + if app_config is not None: + del self.app_configs[app_config.label] + + @contextmanager + def _with_app(self, app_name): + app_config = self._begin_with_app(app_name) + try: + yield + finally: + self._end_with_app(app_config) + + def _begin_without_app(self, app_name): + # Returns an opaque value that can be passed to _end_without_app(). + return self.app_configs.pop(app_name.rpartition(".")[2], None) + + def _end_without_app(self, app_config): + if app_config is not None: + self.app_configs[app_config.label] = app_config + + @contextmanager + def _without_app(self, app_name): + app_config = self._begin_without_app(app_name) + try: + yield + finally: + self._end_without_app(app_config) + + def _begin_empty(self): + app_configs, self.app_configs = self.app_configs, OrderedDict() + return app_configs + + def _end_empty(self, app_configs): + self.app_configs = app_configs + + @contextmanager + def _empty(self): + app_configs = self._begin_empty() + try: + yield + finally: + self._end_empty(app_configs) + ### DEPRECATED METHODS GO BELOW THIS LINE ### def get_app(self, app_label): diff --git a/django/db/models/options.py b/django/db/models/options.py index cd68d5eae9..f7be089539 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -94,11 +94,11 @@ class Options(object): @property def app_config(self): # Don't go through get_app_config to avoid triggering populate(). - return self.app_cache.app_configs[self.app_label] + return self.app_cache.app_configs.get(self.app_label) @property def installed(self): - return self.app_config.installed + return self.app_config is not None def contribute_to_class(self, cls, name): from django.db import connection diff --git a/tests/admin_docs/tests.py b/tests/admin_docs/tests.py index 047bf920a2..66c125490f 100644 --- a/tests/admin_docs/tests.py +++ b/tests/admin_docs/tests.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib.sites.models import Site from django.contrib.admindocs import utils from django.contrib.auth.models import User +from django.core.apps import app_cache from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings @@ -13,27 +14,18 @@ class MiscTests(TestCase): urls = 'admin_docs.urls' def setUp(self): - self._old_installed = Site._meta.app_config.installed User.objects.create_superuser('super', None, 'secret') self.client.login(username='super', password='secret') - def tearDown(self): - Site._meta.app_config.installed = self._old_installed - - @override_settings( - SITE_ID=None, - INSTALLED_APPS=[app for app in settings.INSTALLED_APPS - if app != 'django.contrib.sites'], - ) def test_no_sites_framework(self): """ Without the sites framework, should not access SITE_ID or Site objects. Deleting settings is fine here as UserSettingsHolder is used. """ - Site._meta.app_config.installed = False - Site.objects.all().delete() - del settings.SITE_ID - self.client.get('/admindocs/views/') # should not raise + with self.settings(SITE_ID=None), app_cache._without_app('django.contrib.sites'): + Site.objects.all().delete() + del settings.SITE_ID + self.client.get('/admindocs/views/') # should not raise @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) From 2b56d6910269ace19e3007d2b4a3445f7886314f Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 18 Dec 2013 11:28:21 +0100 Subject: [PATCH 007/208] Implemented two-stage app-cache population. First stage imports app modules. It doesn't catch import errors. This matches the previous behavior and keeps the code simple. Second stage import models modules. It catches import errors and retries them after walking through the entire list once. This matches the previous behavior and seems useful. populate_models() is intended to be equivalent to populate(). It isn't wired yet. That is coming in the next commit. --- django/core/apps/base.py | 38 ++++++++++----- django/core/apps/cache.py | 95 ++++++++++++++++++++++++++++++++++---- tests/test_runner/tests.py | 16 +++---- 3 files changed, 117 insertions(+), 32 deletions(-) diff --git a/django/core/apps/base.py b/django/core/apps/base.py index 332a066bcb..83c073a12a 100644 --- a/django/core/apps/base.py +++ b/django/core/apps/base.py @@ -1,39 +1,53 @@ -from collections import OrderedDict +from importlib import import_module +from django.utils.module_loading import module_has_submodule from django.utils._os import upath +MODELS_MODULE_NAME = 'models' + + class AppConfig(object): """ Class representing a Django application and its configuration. """ - def __init__(self, name, app_module, models_module): + def __init__(self, app_name): # Full Python path to the application eg. 'django.contrib.admin'. # This is the value that appears in INSTALLED_APPS. - self.name = name + self.name = app_name # Last component of the Python path to the application eg. 'admin'. # This value must be unique across a Django project. - self.label = name.rpartition(".")[2] + self.label = app_name.rpartition(".")[2] # Root module eg. . - self.app_module = app_module + self.app_module = import_module(app_name) # Module containing models eg. . None if the application - # doesn't have a models module. - self.models_module = models_module + # from 'django/contrib/admin/models.pyc'>. Set by import_models(). + # None if the application doesn't have a models module. + self.models_module = None - # Mapping of lower case model names to model classes. - # Populated by calls to AppCache.register_model(). - self.models = OrderedDict() + # Mapping of lower case model names to model classes. Initally set to + # None to prevent accidental access before import_models() runs. + self.models = None # Filesystem path to the application directory eg. # u'/usr/lib/python2.7/dist-packages/django/contrib/admin'. # This is a unicode object on Python 2 and a str on Python 3. - self.path = upath(app_module.__path__[0]) + self.path = upath(self.app_module.__path__[0]) def __repr__(self): return '' % self.label + + def import_models(self, all_models): + # Dictionary of models for this app, stored in the 'all_models' + # attribute of the AppCache this AppConfig is attached to. Injected as + # a parameter because it may get populated before this method has run. + self.models = all_models + + if module_has_submodule(self.app_module, MODELS_MODULE_NAME): + models_module_name = '%s.%s' % (self.name, MODELS_MODULE_NAME) + self.models_module = import_module(models_module_name) diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index fb7bb6172e..cf2489185e 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -12,10 +12,7 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.module_loading import import_lock, module_has_submodule from django.utils._os import upath -from .base import AppConfig - - -MODELS_MODULE_NAME = 'models' +from .base import AppConfig, MODELS_MODULE_NAME class UnavailableApp(Exception): @@ -54,6 +51,10 @@ class AppCache(object): # Used by TransactionTestCase.available_apps for performance reasons. self.available_apps = None + # Internal flags used when populating the cache. + self._apps_loaded = False + self._models_loaded = False + # -- Everything below here is only used when populating the cache -- self.loaded = False self.handled = set() @@ -61,6 +62,82 @@ class AppCache(object): self.nesting_level = 0 self._get_models_cache = {} + def populate_apps(self): + """ + Populate app-related information. + + This method imports each application module. + + It is thread safe and idempotent, but not reentrant. + """ + if self._apps_loaded: + return + # Since populate_apps() may be a side effect of imports, and since + # it will itself import modules, an ABBA deadlock between threads + # would be possible if we didn't take the import lock. See #18251. + with import_lock(): + if self._apps_loaded: + return + + # app_config should be pristine, otherwise the code below won't + # guarantee that the order matches the order in INSTALLED_APPS. + if self.app_configs: + raise RuntimeError("populate_apps() isn't reentrant") + + # Application modules aren't expected to import anything, and + # especially not other application modules, even indirectly. + # Therefore we simply import them sequentially. + for app_name in settings.INSTALLED_APPS: + app_config = AppConfig(app_name) + self.app_configs[app_config.label] = app_config + + self._apps_loaded = True + + def populate_models(self): + """ + Populate model-related information. + + This method imports each models module. + + It is thread safe, idempotent and reentrant. + """ + if self._models_loaded: + return + # Since populate_models() may be a side effect of imports, and since + # it will itself import modules, an ABBA deadlock between threads + # would be possible if we didn't take the import lock. See #18251. + with import_lock(): + if self._models_loaded: + return + + self.populate_apps() + + # Models modules are likely to import other models modules, for + # example to reference related objects. As a consequence: + # - we deal with import loops by postponing affected modules. + # - we provide reentrancy by making import_models() idempotent. + + outermost = not hasattr(self, '_postponed') + if outermost: + self._postponed = [] + + for app_config in self.app_configs.values(): + + try: + all_models = self.all_models[app_config.label] + app_config.import_models(all_models) + except ImportError: + self._postponed.append(app_config) + + if outermost: + for app_config in self._postponed: + all_models = self.all_models[app_config.label] + app_config.import_models(all_models) + + del self._postponed + + self._models_loaded = True + def populate(self): """ Fill in all the cache information. This method is threadsafe, in the @@ -121,8 +198,8 @@ class AppCache(object): finally: self.nesting_level -= 1 - app_config = AppConfig(app_name, app_module, models_module) - app_config.models = self.all_models[app_config.label] + app_config = AppConfig(app_name) + app_config.import_models(self.all_models[app_config.label]) self.app_configs[app_config.label] = app_config return models_module @@ -308,13 +385,11 @@ class AppCache(object): def _begin_with_app(self, app_name): # Returns an opaque value that can be passed to _end_with_app(). - app_module = import_module(app_name) - models_module = import_module('%s.models' % app_name) - app_config = AppConfig(app_name, app_module, models_module) + app_config = AppConfig(app_name) if app_config.label in self.app_configs: return None else: - app_config.models = self.all_models[app_config.label] + app_config.import_models(self.all_models[app_config.label]) self.app_configs[app_config.label] = app_config return app_config diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 04fa3b8784..a6dbeb0ae4 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -3,7 +3,6 @@ Tests for django test runner """ from __future__ import unicode_literals -from importlib import import_module from optparse import make_option import types import unittest @@ -227,10 +226,8 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase) "Check that the get_tests helper function can find tests in a directory" from django.core.apps.base import AppConfig from django.test.simple import get_tests - app_config = AppConfig( - 'test_runner.valid_app', - import_module('test_runner.valid_app'), - import_module('test_runner.valid_app.models')) + app_config = AppConfig('test_runner.valid_app') + app_config.import_models({}) tests = get_tests(app_config) self.assertIsInstance(tests, types.ModuleType) @@ -238,11 +235,10 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase) "Test for #12658 - Tests with ImportError's shouldn't fail silently" from django.core.apps.base import AppConfig from django.test.simple import get_tests - app_config = AppConfig( - 'test_runner_invalid_app', - import_module('test_runner_invalid_app'), - import_module('test_runner_invalid_app.models')) - self.assertRaises(ImportError, get_tests, app_config) + app_config = AppConfig('test_runner_invalid_app') + app_config.import_models({}) + with self.assertRaises(ImportError): + get_tests(app_config) class Sqlite3InMemoryTestDbs(TestCase): From 86804ab063cb9666b69638372abfe4174a2472b2 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 18 Dec 2013 14:49:29 +0100 Subject: [PATCH 008/208] Terminated AppCache._populate() with extreme prejudice. It was called _populate() before I renamed it to populate(). Since it has been superseded by populate_models() there's no reason to keep it. Removed the can_postpone argument of load_app() as it was only used by populate(). It's a private API and there's no replacement. Simplified load_app() accordingly. Then new version behaves exactly like the old one even though it's much shorter. --- django/contrib/admin/validation.py | 2 +- django/core/apps/cache.py | 89 +++++------------------------- django/core/serializers/base.py | 2 +- django/core/serializers/python.py | 2 +- django/db/models/base.py | 2 +- django/db/models/loading.py | 2 +- django/db/models/options.py | 2 +- tests/app_loading/tests.py | 5 +- tests/runtests.py | 2 +- 9 files changed, 24 insertions(+), 84 deletions(-) diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index a15967e024..ee945f73e5 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -18,7 +18,7 @@ class BaseValidator(object): def __init__(self): # Before we can introspect models, they need the app cache to be fully # loaded so that inter-relations are set up correctly. - app_cache.populate() + app_cache.populate_models() def validate(self, cls, model): for m in dir(self): diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index cf2489185e..e9a4215440 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -2,17 +2,16 @@ from collections import defaultdict, OrderedDict from contextlib import contextmanager -from importlib import import_module import os import sys import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils.module_loading import import_lock, module_has_submodule +from django.utils.module_loading import import_lock from django.utils._os import upath -from .base import AppConfig, MODELS_MODULE_NAME +from .base import AppConfig class UnavailableApp(Exception): @@ -51,15 +50,11 @@ class AppCache(object): # Used by TransactionTestCase.available_apps for performance reasons. self.available_apps = None - # Internal flags used when populating the cache. - self._apps_loaded = False - self._models_loaded = False + # Internal flags used when populating the master cache. + self._apps_loaded = not self.master + self._models_loaded = not self.master - # -- Everything below here is only used when populating the cache -- - self.loaded = False - self.handled = set() - self.postponed = [] - self.nesting_level = 0 + # Cache for get_models. self._get_models_cache = {} def populate_apps(self): @@ -138,71 +133,15 @@ class AppCache(object): self._models_loaded = True - def populate(self): - """ - Fill in all the cache information. This method is threadsafe, in the - sense that every caller will see the same state upon return, and if the - cache is already initialised, it does no work. - """ - if self.loaded: - return - if not self.master: - self.loaded = True - return - # Note that we want to use the import lock here - the app loading is - # in many cases initiated implicitly by importing, and thus it is - # possible to end up in deadlock when one thread initiates loading - # without holding the importer lock and another thread then tries to - # import something which also launches the app loading. For details of - # this situation see #18251. - with import_lock(): - if self.loaded: - return - for app_name in settings.INSTALLED_APPS: - if app_name in self.handled: - continue - self.load_app(app_name, can_postpone=True) - if not self.nesting_level: - for app_name in self.postponed: - self.load_app(app_name) - self.loaded = True - - def load_app(self, app_name, can_postpone=False): + def load_app(self, app_name): """ Loads the app with the provided fully qualified name, and returns the model module. """ - app_module = import_module(app_name) - self.handled.add(app_name) - self.nesting_level += 1 - try: - models_module = import_module('%s.%s' % (app_name, MODELS_MODULE_NAME)) - except ImportError: - # If the app doesn't have a models module, we can just swallow the - # ImportError and return no models for this app. - if not module_has_submodule(app_module, MODELS_MODULE_NAME): - models_module = None - # But if the app does have a models module, we need to figure out - # whether to suppress or propagate the error. If can_postpone is - # True then it may be that the package is still being imported by - # Python and the models module isn't available yet. So we add the - # app to the postponed list and we'll try it again after all the - # recursion has finished (in populate). If can_postpone is False - # then it's time to raise the ImportError. - else: - if can_postpone: - self.postponed.append(app_name) - return - else: - raise - finally: - self.nesting_level -= 1 - app_config = AppConfig(app_name) app_config.import_models(self.all_models[app_config.label]) self.app_configs[app_config.label] = app_config - - return models_module + return app_config.models_module def app_cache_ready(self): """ @@ -211,7 +150,7 @@ class AppCache(object): Useful for code that wants to cache the results of get_models() for themselves once it is safe to do so. """ - return self.loaded + return self._models_loaded # implies self._apps_loaded. def get_app_configs(self, only_with_models_module=False): """ @@ -220,7 +159,7 @@ class AppCache(object): If only_with_models_module in True (non-default), only applications containing a models module are considered. """ - self.populate() + self.populate_models() for app_config in self.app_configs.values(): if only_with_models_module and app_config.models_module is None: continue @@ -240,7 +179,7 @@ class AppCache(object): If only_with_models_module in True (non-default), only applications containing a models module are considered. """ - self.populate() + self.populate_models() app_config = self.app_configs.get(app_label) if app_config is None: raise LookupError("No installed app with label %r." % app_label) @@ -288,7 +227,7 @@ class AppCache(object): return model_list except KeyError: pass - self.populate() + self.populate_models() if app_mod: app_label = app_mod.__name__.split('.')[-2] if only_installed: @@ -331,7 +270,7 @@ class AppCache(object): """ if not self.master: only_installed = False - self.populate() + self.populate_models() if only_installed: app_config = self.app_configs.get(app_label) if app_config is None: @@ -496,7 +435,7 @@ class AppCache(object): "[a.path for a in get_app_configs()] supersedes get_app_paths().", PendingDeprecationWarning, stacklevel=2) - self.populate() + self.populate_models() app_paths = [] for app in self.get_apps(): diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index db4b79a020..b0bc8138ab 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -139,7 +139,7 @@ class Deserializer(six.Iterator): self.stream = stream_or_string # Make sure the app cache is loaded before deserialization starts # (otherwise subclass calls to get_model() and friends might fail...) - app_cache.populate() + app_cache.populate_models() def __iter__(self): return self diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 13d6f01a4e..1e4c37e2ae 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -88,7 +88,7 @@ def Deserializer(object_list, **options): db = options.pop('using', DEFAULT_DB_ALIAS) ignore = options.pop('ignorenonexistent', False) - app_cache.populate() + app_cache.populate_models() for d in object_list: # Look up the model and starting build a dict of data for it. diff --git a/django/db/models/base.py b/django/db/models/base.py index a694350b97..d1e7323d17 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -6,7 +6,7 @@ from functools import update_wrapper from django.utils.six.moves import zip from django.core.apps import app_cache -from django.core.apps.cache import MODELS_MODULE_NAME +from django.core.apps.base import MODELS_MODULE_NAME import django.db.models.manager # NOQA: Imported to register signal handler. from django.conf import settings from django.core.exceptions import (ObjectDoesNotExist, diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 5745dbed4d..00fae36769 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -30,6 +30,6 @@ def get_app_errors(): try: return app_cache.app_errors except AttributeError: - app_cache.populate() + app_cache.populate_models() app_cache.app_errors = {} return app_cache.app_errors diff --git a/django/db/models/options.py b/django/db/models/options.py index f7be089539..fec9aa4fc4 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -93,7 +93,7 @@ class Options(object): @property def app_config(self): - # Don't go through get_app_config to avoid triggering populate(). + # Don't go through get_app_config to avoid triggering imports. return self.app_cache.app_configs.get(self.app_label) @property diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index e282306ec0..45f6cd6c31 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -77,8 +77,9 @@ class EggLoadingTest(TestCase): error. Refs #17667. """ app_cache = AppCache() - # Pretend we're the master app cache to test populate(). - app_cache.master = True + # Pretend we're the master app cache to test the population process. + app_cache._apps_loaded = False + app_cache._models_loaded = False with override_settings(INSTALLED_APPS=('notexists',)): with self.assertRaises(ImportError): app_cache.get_model('notexists', 'nomodel') diff --git a/tests/runtests.py b/tests/runtests.py index badbd6fbec..77cf005267 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -128,7 +128,7 @@ def setup(verbosity, test_labels): # Load all the ALWAYS_INSTALLED_APPS. with warnings.catch_warnings(): warnings.filterwarnings('ignore', 'django.contrib.comments is deprecated and will be removed before Django 1.8.', DeprecationWarning) - app_cache.populate() + app_cache.populate_models() # Load all the test model apps. test_modules = get_test_modules() From 439b364e1e81371bd87488beed35ce051be8aaaa Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 18 Dec 2013 16:49:42 +0100 Subject: [PATCH 009/208] Added an _-prefix to pending lookups because it's transient. --- django/core/apps/cache.py | 6 +++--- django/db/models/fields/related.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index e9a4215440..74488325a8 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -43,9 +43,6 @@ class AppCache(object): # Mapping of labels to AppConfig instances for installed apps. self.app_configs = OrderedDict() - # Pending lookups for lazy relations - self.pending_lookups = {} - # Set of app names. Allows restricting the set of installed apps. # Used by TransactionTestCase.available_apps for performance reasons. self.available_apps = None @@ -54,6 +51,9 @@ class AppCache(object): self._apps_loaded = not self.master self._models_loaded = not self.master + # Pending lookups for lazy relations. + self._pending_lookups = {} + # Cache for get_models. self._get_models_cache = {} diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 1b141af099..cf35504afe 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -74,7 +74,7 @@ def add_lazy_relation(cls, field, relation, operation): else: key = (app_label, model_name) value = (cls, field, operation) - cls._meta.app_cache.pending_lookups.setdefault(key, []).append(value) + cls._meta.app_cache._pending_lookups.setdefault(key, []).append(value) def do_pending_lookups(sender, **kwargs): @@ -82,7 +82,7 @@ def do_pending_lookups(sender, **kwargs): Handle any pending relations to the sending model. Sent from class_prepared. """ key = (sender._meta.app_label, sender.__name__) - for cls, field, operation in sender._meta.app_cache.pending_lookups.pop(key, []): + for cls, field, operation in sender._meta.app_cache._pending_lookups.pop(key, []): operation(field, sender, cls) signals.class_prepared.connect(do_pending_lookups) From f25fa9d8590c7759c1ec7ffecf1d67be81237674 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 18 Dec 2013 17:56:11 +0100 Subject: [PATCH 010/208] Deprecated load_app(). Adjusted several tests that used it to add apps to the app cache and then attempted to remove them by manipulating attributes directly. Also renamed invalid_models to invalid_models_tests to avoid clashing application labels between the outer and the inner invalid_models applications. --- django/core/apps/cache.py | 25 +++++++------ tests/admin_scripts/tests.py | 2 +- tests/app_loading/tests.py | 35 +++++++++---------- .../__init__.py | 0 .../invalid_models/__init__.py | 0 .../invalid_models/models.py | 0 .../tests.py | 15 ++------ tests/migrations/test_writer.py | 10 +++--- tests/proxy_model_inheritance/tests.py | 21 ++++------- tests/runtests.py | 3 +- 10 files changed, 47 insertions(+), 64 deletions(-) rename tests/{invalid_models => invalid_models_tests}/__init__.py (100%) rename tests/{invalid_models => invalid_models_tests}/invalid_models/__init__.py (100%) rename tests/{invalid_models => invalid_models_tests}/invalid_models/models.py (100%) rename tests/{invalid_models => invalid_models_tests}/tests.py (72%) diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index 74488325a8..65f51801ec 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -133,16 +133,6 @@ class AppCache(object): self._models_loaded = True - def load_app(self, app_name): - """ - Loads the app with the provided fully qualified name, and returns the - model module. - """ - app_config = AppConfig(app_name) - app_config.import_models(self.all_models[app_config.label]) - self.app_configs[app_config.label] = app_config - return app_config.models_module - def app_cache_ready(self): """ Returns true if the model cache is fully populated. @@ -377,6 +367,19 @@ class AppCache(object): ### DEPRECATED METHODS GO BELOW THIS LINE ### + def load_app(self, app_name): + """ + Loads the app with the provided fully qualified name, and returns the + model module. + """ + warnings.warn( + "load_app(app_name) is deprecated.", + PendingDeprecationWarning, stacklevel=2) + app_config = AppConfig(app_name) + app_config.import_models(self.all_models[app_config.label]) + self.app_configs[app_config.label] = app_config + return app_config.models_module + def get_app(self, app_label): """ Returns the module containing the models for the given app_label. @@ -447,7 +450,7 @@ class AppCache(object): Register a set of models as belonging to an app. """ warnings.warn( - "register_models(app_label, models) is deprecated.", + "register_models(app_label, *models) is deprecated.", PendingDeprecationWarning, stacklevel=2) for model in models: self.register_model(app_label, model) diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index db0b8d6030..3f9a336c16 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -1094,7 +1094,7 @@ class ManageValidate(AdminScriptTestCase): self.assertOutput(err, 'ImportError') def test_complex_app(self): - "manage.py validate does not raise an ImportError validating a complex app with nested calls to load_app" + "manage.py validate does not raise an ImportError validating a complex app" self.write_settings('settings.py', apps=['admin_scripts.complex_app', 'admin_scripts.simple_app'], sdict={'DEBUG': True}) diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index 45f6cd6c31..bb71ad84a2 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -8,6 +8,7 @@ from django.core.apps import app_cache from django.core.apps.cache import AppCache from django.test.utils import override_settings from django.utils._os import upath +from django.utils import six class EggLoadingTest(TestCase): @@ -31,45 +32,41 @@ class EggLoadingTest(TestCase): """Models module can be loaded from an app in an egg""" egg_name = '%s/modelapp.egg' % self.egg_dir sys.path.append(egg_name) - models = app_cache.load_app('app_with_models') - self.assertFalse(models is None) + with app_cache._with_app('app_with_models'): + models_module = app_cache.get_app_config('app_with_models').models_module + self.assertIsNotNone(models_module) def test_egg2(self): """Loading an app from an egg that has no models returns no models (and no error)""" egg_name = '%s/nomodelapp.egg' % self.egg_dir sys.path.append(egg_name) - models = app_cache.load_app('app_no_models') - self.assertTrue(models is None) + with app_cache._with_app('app_no_models'): + models_module = app_cache.get_app_config('app_no_models').models_module + self.assertIsNone(models_module) def test_egg3(self): """Models module can be loaded from an app located under an egg's top-level package""" egg_name = '%s/omelet.egg' % self.egg_dir sys.path.append(egg_name) - models = app_cache.load_app('omelet.app_with_models') - self.assertFalse(models is None) + with app_cache._with_app('omelet.app_with_models'): + models_module = app_cache.get_app_config('app_with_models').models_module + self.assertIsNotNone(models_module) def test_egg4(self): """Loading an app with no models from under the top-level egg package generates no error""" egg_name = '%s/omelet.egg' % self.egg_dir sys.path.append(egg_name) - models = app_cache.load_app('omelet.app_no_models') - self.assertTrue(models is None) + with app_cache._with_app('omelet.app_no_models'): + models_module = app_cache.get_app_config('app_no_models').models_module + self.assertIsNone(models_module) def test_egg5(self): """Loading an app from an egg that has an import error in its models module raises that error""" egg_name = '%s/brokenapp.egg' % self.egg_dir sys.path.append(egg_name) - self.assertRaises(ImportError, app_cache.load_app, 'broken_app') - raised = None - try: - app_cache.load_app('broken_app') - except ImportError as e: - raised = e - - # Make sure the message is indicating the actual - # problem in the broken app. - self.assertTrue(raised is not None) - self.assertTrue("modelz" in raised.args[0]) + with six.assertRaisesRegex(self, ImportError, 'modelz'): + with app_cache._with_app('broken_app'): + app_cache.get_app_config('omelet.app_no_models').models_module def test_missing_app(self): """ diff --git a/tests/invalid_models/__init__.py b/tests/invalid_models_tests/__init__.py similarity index 100% rename from tests/invalid_models/__init__.py rename to tests/invalid_models_tests/__init__.py diff --git a/tests/invalid_models/invalid_models/__init__.py b/tests/invalid_models_tests/invalid_models/__init__.py similarity index 100% rename from tests/invalid_models/invalid_models/__init__.py rename to tests/invalid_models_tests/invalid_models/__init__.py diff --git a/tests/invalid_models/invalid_models/models.py b/tests/invalid_models_tests/invalid_models/models.py similarity index 100% rename from tests/invalid_models/invalid_models/models.py rename to tests/invalid_models_tests/invalid_models/models.py diff --git a/tests/invalid_models/tests.py b/tests/invalid_models_tests/tests.py similarity index 72% rename from tests/invalid_models/tests.py rename to tests/invalid_models_tests/tests.py index 10d8fa47a7..4e0cef546b 100644 --- a/tests/invalid_models/tests.py +++ b/tests/invalid_models_tests/tests.py @@ -18,14 +18,7 @@ class InvalidModelTestCase(unittest.TestCase): self.stdout = StringIO() sys.stdout = self.stdout - # The models need to be removed after the test in order to prevent bad - # interactions with the flush operation in other tests. - self._old_models = app_cache.app_configs['invalid_models'].models.copy() - def tearDown(self): - app_cache.app_configs['invalid_models'].models = self._old_models - app_cache.all_models['invalid_models'] = self._old_models - app_cache._get_models_cache = {} sys.stdout = self.old_stdout # Technically, this isn't an override -- TEST_SWAPPED_MODEL must be @@ -38,12 +31,10 @@ class InvalidModelTestCase(unittest.TestCase): TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target', ) def test_invalid_models(self): - try: - module = app_cache.load_app("invalid_models.invalid_models") - except Exception: - self.fail('Unable to load invalid model module') + with app_cache._with_app("invalid_models_tests.invalid_models"): + module = app_cache.get_app_config("invalid_models").models_module + get_validation_errors(self.stdout, module) - get_validation_errors(self.stdout, module) self.stdout.seek(0) error_log = self.stdout.read() actual = error_log.split('\n') diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 76fe5b5a9e..f921f9f03c 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -124,8 +124,8 @@ class WriterTests(TestCase): with override_settings(INSTALLED_APPS=test_apps): for app in test_apps: - app_cache.load_app(app) - migration = migrations.Migration('0001_initial', app.split('.')[-1]) - expected_path = os.path.join(base_dir, *(app.split('.') + ['migrations', '0001_initial.py'])) - writer = MigrationWriter(migration) - self.assertEqual(writer.path, expected_path) + with app_cache._with_app(app): + migration = migrations.Migration('0001_initial', app.split('.')[-1]) + expected_path = os.path.join(base_dir, *(app.split('.') + ['migrations', '0001_initial.py'])) + writer = MigrationWriter(migration) + self.assertEqual(writer.path, expected_path) diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 601fdc5b42..01a2ddfef6 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import os import sys -from django.conf import settings from django.core.apps import app_cache from django.core.management import call_command from django.test import TestCase, TransactionTestCase @@ -21,28 +20,21 @@ class ProxyModelInheritanceTests(TransactionTestCase): for the proxied model (as described in #12286). This test creates two dummy apps and calls migrate, then verifies that the table has been created. """ - - available_apps = [] + available_apps = ['app1', 'app2'] def setUp(self): self.old_sys_path = sys.path[:] sys.path.append(os.path.dirname(os.path.abspath(upath(__file__)))) - for app in settings.INSTALLED_APPS: - app_cache.load_app(app) + self._with_app1 = app_cache._begin_with_app('app1') + self._with_app2 = app_cache._begin_with_app('app2') def tearDown(self): + app_cache._end_with_app(self._with_app1) + app_cache._end_with_app(self._with_app2) sys.path = self.old_sys_path - del app_cache.app_configs['app1'] - del app_cache.app_configs['app2'] - del app_cache.all_models['app1'] - del app_cache.all_models['app2'] def test_table_exists(self): - try: - app_cache.set_available_apps(settings.INSTALLED_APPS) - call_command('migrate', verbosity=0) - finally: - app_cache.unset_available_apps() + call_command('migrate', verbosity=0) from .app1.models import ProxyModel from .app2.models import NiceModel self.assertEqual(NiceModel.objects.all().count(), 0) @@ -56,7 +48,6 @@ class MultiTableInheritanceProxyTest(TestCase): Deleting an instance of a model proxying a multi-table inherited subclass should cascade delete down the whole inheritance chain (see #18083). - """ instance = ConcreteModelSubclassProxy.objects.create() instance.delete() diff --git a/tests/runtests.py b/tests/runtests.py index 77cf005267..64d363a095 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -164,7 +164,8 @@ def setup(verbosity, test_labels): if module_found_in_labels: if verbosity >= 2: print("Importing application %s" % module_name) - app_cache.load_app(module_label) + # HACK. + app_cache._begin_with_app(module_label) if module_label not in settings.INSTALLED_APPS: settings.INSTALLED_APPS.append(module_label) From 4582993302262dec06a39028cc878247542c2334 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 19 Dec 2013 10:26:18 +0100 Subject: [PATCH 011/208] Populated models only when necessary in get_app_config[s]. Took this opportunity to change get_app[s] to only consider applications containing a model module as that seems slightly more backwards compatible. Since callers that care about models pass only_with_models_module=True, this has very few consequences. Only AppCommand needed a change. --- django/core/apps/cache.py | 34 ++++++++++++++++++++++------------ django/core/management/base.py | 5 +++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index 65f51801ec..0eddbed935 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -144,12 +144,16 @@ class AppCache(object): def get_app_configs(self, only_with_models_module=False): """ - Return an iterable of application configurations. + Imports applications and returns an iterable of app configs. - If only_with_models_module in True (non-default), only applications - containing a models module are considered. + If only_with_models_module in True (non-default), imports models and + considers only applications containing a models module. """ - self.populate_models() + if only_with_models_module: + self.populate_models() + else: + self.populate_apps() + for app_config in self.app_configs.values(): if only_with_models_module and app_config.models_module is None: continue @@ -159,17 +163,21 @@ class AppCache(object): def get_app_config(self, app_label, only_with_models_module=False): """ - Returns the application configuration for the given app_label. + Imports applications and returns an app config for the given label. - Raises LookupError if no application exists with this app_label. + Raises LookupError if no application exists with this label. Raises UnavailableApp when set_available_apps() disables the - application with this app_label. + application with this label. - If only_with_models_module in True (non-default), only applications - containing a models module are considered. + If only_with_models_module in True (non-default), imports models and + considers only applications containing a models module. """ - self.populate_models() + if only_with_models_module: + self.populate_models() + else: + self.populate_apps() + app_config = self.app_configs.get(app_label) if app_config is None: raise LookupError("No installed app with label %r." % app_label) @@ -391,7 +399,8 @@ class AppCache(object): "get_app_config(app_label).models_module supersedes get_app(app_label).", PendingDeprecationWarning, stacklevel=2) try: - return self.get_app_config(app_label).models_module + return self.get_app_config( + app_label, only_with_models_module=True).models_module except LookupError as exc: # Change the exception type for backwards compatibility. raise ImproperlyConfigured(*exc.args) @@ -403,7 +412,8 @@ class AppCache(object): warnings.warn( "[a.models_module for a in get_app_configs()] supersedes get_apps().", PendingDeprecationWarning, stacklevel=2) - return [app_config.models_module for app_config in self.get_app_configs()] + app_configs = self.get_app_configs(only_with_models_module=True) + return [app_config.models_module for app_config in app_configs] def _get_app_package(self, app): return '.'.join(app.__name__.split('.')[:-1]) diff --git a/django/core/management/base.py b/django/core/management/base.py index aeb27e1f0b..b9404ec1aa 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -344,6 +344,11 @@ class AppCommand(BaseCommand): from django.core.apps import app_cache if not app_labels: raise CommandError('Enter at least one appname.') + # Populate models and don't use only_with_models_module=True when + # calling get_app_config() to tell apart missing apps from apps + # without a model module -- which can't be supported with the legacy + # API since it passes the models module to handle_app(). + app_cache.populate_models() try: app_configs = [app_cache.get_app_config(app_label) for app_label in app_labels] except (LookupError, ImportError) as e: From d4733b6df07b1f8a869eed8eac86869d1d14472c Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 19 Dec 2013 20:32:00 +0100 Subject: [PATCH 012/208] Not all Python modules have a __path__. --- django/core/apps/base.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/django/core/apps/base.py b/django/core/apps/base.py index 83c073a12a..2503d9e3ff 100644 --- a/django/core/apps/base.py +++ b/django/core/apps/base.py @@ -35,9 +35,13 @@ class AppConfig(object): self.models = None # Filesystem path to the application directory eg. - # u'/usr/lib/python2.7/dist-packages/django/contrib/admin'. - # This is a unicode object on Python 2 and a str on Python 3. - self.path = upath(self.app_module.__path__[0]) + # u'/usr/lib/python2.7/dist-packages/django/contrib/admin'. May be + # None if the application isn't a bona fide package eg. if it's an + # egg. Otherwise it's a unicode on Python 2 and a str on Python 3. + try: + self.path = upath(self.app_module.__path__[0]) + except AttributeError: + self.path = None def __repr__(self): return '' % self.label From 65cd74be8e99d06c7861edc5050e34d6444e4d56 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 19 Dec 2013 15:57:23 +0100 Subject: [PATCH 013/208] Stopped iterating on INSTALLED_APPS. Used the app cache's get_app_configs() method instead. --- django/contrib/staticfiles/finders.py | 6 +++-- django/core/management/__init__.py | 20 +++++++++++----- django/core/management/commands/flush.py | 5 ++-- django/core/management/commands/migrate.py | 7 +++--- django/template/base.py | 8 +++++-- django/template/loaders/app_directories.py | 11 +++------ django/template/loaders/eggs.py | 7 +++--- django/utils/autoreload.py | 9 +++----- django/utils/module_loading.py | 9 ++++---- django/utils/translation/trans_real.py | 8 +++---- django/views/i18n.py | 6 ++++- tests/bash_completion/tests.py | 8 ++++--- tests/i18n/tests.py | 26 +++++++++++++++++---- tests/staticfiles_tests/test_liveserver.py | 3 +++ tests/template_tests/test_loaders.py | 27 +++++++++++----------- tests/template_tests/tests.py | 13 +++++++---- tests/utils_tests/test_module_loading.py | 9 ++++++-- tests/view_tests/tests/test_i18n.py | 16 ++++++------- 18 files changed, 116 insertions(+), 82 deletions(-) diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py index cf5eb4f85b..cadc8bffa7 100644 --- a/django/contrib/staticfiles/finders.py +++ b/django/contrib/staticfiles/finders.py @@ -2,6 +2,7 @@ from collections import OrderedDict import os from django.conf import settings +from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import default_storage, Storage, FileSystemStorage from django.utils.functional import empty, LazyObject @@ -116,10 +117,11 @@ class AppDirectoriesFinder(BaseFinder): def __init__(self, apps=None, *args, **kwargs): # The list of apps that are handled self.apps = [] - # Mapping of app module paths to storage instances + # Mapping of app names to storage instances self.storages = OrderedDict() if apps is None: - apps = settings.INSTALLED_APPS + app_configs = app_cache.get_app_configs() + apps = [app_config.name for app_config in app_configs] for app in apps: app_storage = self.storage_class(app) if os.path.isdir(app_storage.location): diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 0b47e179db..a7d074e149 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -5,6 +5,7 @@ from optparse import OptionParser, NO_DEFAULT import os import sys +from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand, CommandError, handle_default_options from django.core.management.color import color_style @@ -106,13 +107,19 @@ def get_commands(): _commands = dict((name, 'django.core') for name in find_commands(__path__[0])) # Find the installed apps - from django.conf import settings try: - apps = settings.INSTALLED_APPS + settings.INSTALLED_APPS except ImproperlyConfigured: - # Still useful for commands that do not require functional settings, - # like startproject or help + # Still useful for commands that do not require functional + # settings, like startproject or help. apps = [] + else: + # Populate the app cache outside of the try/except block to avoid + # catching ImproperlyConfigured errors that aren't caused by the + # absence of a settings module. + from django.core.apps import app_cache + app_configs = app_cache.get_app_configs() + apps = [app_config.name for app_config in app_configs] # Find and load the management module for each installed app. for app_name in apps: @@ -339,9 +346,10 @@ class ManagementUtility(object): elif cwords[0] in ('dumpdata', 'sql', 'sqlall', 'sqlclear', 'sqlcustom', 'sqlindexes', 'sqlsequencereset', 'test'): try: - from django.conf import settings + from django.core.apps import app_cache + app_configs = app_cache.get_app_configs() # Get the last part of the dotted path as the app name. - options += [(a.split('.')[-1], 0) for a in settings.INSTALLED_APPS] + options += [(app_config.label, 0) for app_config in app_configs] except ImportError: # Fail silently if DJANGO_SETTINGS_MODULE isn't set. The # user will find out once they execute the command. diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index 562147e403..f13e948ae5 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -2,7 +2,6 @@ import sys from importlib import import_module from optparse import make_option -from django.conf import settings from django.core.apps import app_cache from django.db import connections, router, transaction, DEFAULT_DB_ALIAS from django.core.management import call_command @@ -42,9 +41,9 @@ class Command(NoArgsCommand): # Import the 'management' module within each installed app, to register # dispatcher events. - for app_name in settings.INSTALLED_APPS: + for app_config in app_cache.get_app_configs(): try: - import_module('.management', app_name) + import_module('.management', app_config.name) except ImportError: pass diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index b607e8ee43..c99f26aa0c 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -6,7 +6,6 @@ from importlib import import_module import itertools import traceback -from django.conf import settings from django.core.apps import app_cache from django.core.management import call_command from django.core.management.base import BaseCommand, CommandError @@ -47,9 +46,9 @@ class Command(BaseCommand): # Import the 'management' module within each installed app, to register # dispatcher events. - for app_name in settings.INSTALLED_APPS: - if module_has_submodule(import_module(app_name), "management"): - import_module('.management', app_name) + for app_config in app_cache.get_app_configs(): + if module_has_submodule(app_config.app_module, "management"): + import_module('.management', app_config.name) # Get the database we're operating from db = options.get('database') diff --git a/django/template/base.py b/django/template/base.py index c314637c42..7098ea60f8 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -6,6 +6,7 @@ from importlib import import_module from inspect import getargspec, getcallargs from django.conf import settings +from django.core.apps import app_cache from django.template.context import (BaseContext, Context, RequestContext, # NOQA: imported for backwards compatibility ContextPopException) from django.utils.itercompat import is_iterable @@ -1302,9 +1303,12 @@ def get_templatetags_modules(): # Populate list once per process. Mutate the local list first, and # then assign it to the global name to ensure there are no cases where # two threads try to populate it simultaneously. - for app_module in ['django'] + list(settings.INSTALLED_APPS): + + templatetags_modules_candidates = ['django.templatetags'] + templatetags_modules_candidates += ['%s.templatetags' % app_config.name + for app_config in app_cache.get_app_configs()] + for templatetag_module in templatetags_modules_candidates: try: - templatetag_module = '%s.templatetags' % app_module import_module(templatetag_module) _templatetags_modules.append(templatetag_module) except ImportError: diff --git a/django/template/loaders/app_directories.py b/django/template/loaders/app_directories.py index 4f8ddfccac..90baa34c7a 100644 --- a/django/template/loaders/app_directories.py +++ b/django/template/loaders/app_directories.py @@ -3,12 +3,11 @@ Wrapper for loading templates from "templates" directories in INSTALLED_APPS packages. """ -from importlib import import_module import os import sys from django.conf import settings -from django.core.exceptions import ImproperlyConfigured +from django.core.apps import app_cache from django.template.base import TemplateDoesNotExist from django.template.loader import BaseLoader from django.utils._os import safe_join @@ -18,12 +17,8 @@ from django.utils import six if six.PY2: fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() app_template_dirs = [] -for app in settings.INSTALLED_APPS: - try: - mod = import_module(app) - except ImportError as e: - raise ImproperlyConfigured('ImportError %s: %s' % (app, e.args[0])) - template_dir = os.path.join(os.path.dirname(mod.__file__), 'templates') +for app_config in app_cache.get_app_configs(): + template_dir = os.path.join(app_config.path, 'templates') if os.path.isdir(template_dir): if six.PY2: template_dir = template_dir.decode(fs_encoding) diff --git a/django/template/loaders/eggs.py b/django/template/loaders/eggs.py index 04e4f73b7b..e9b46b15de 100644 --- a/django/template/loaders/eggs.py +++ b/django/template/loaders/eggs.py @@ -7,6 +7,7 @@ except ImportError: resource_string = None from django.conf import settings +from django.core.apps import app_cache from django.template.base import TemplateDoesNotExist from django.template.loader import BaseLoader from django.utils import six @@ -23,12 +24,12 @@ class Loader(BaseLoader): """ if resource_string is not None: pkg_name = 'templates/' + template_name - for app in settings.INSTALLED_APPS: + for app_config in app_cache.get_app_configs(): try: - resource = resource_string(app, pkg_name) + resource = resource_string(app_config.name, pkg_name) except Exception: continue if six.PY2: resource = resource.decode(settings.FILE_CHARSET) - return (resource, 'egg:%s:%s' % (app, pkg_name)) + return (resource, 'egg:%s:%s' % (app_config.name, pkg_name)) raise TemplateDoesNotExist(template_name) diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index 1913317822..05ffb12f74 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -37,9 +37,8 @@ import time import traceback from django.conf import settings +from django.core.apps import app_cache from django.core.signals import request_finished -from django.utils._os import upath -from importlib import import_module try: from django.utils.six.moves import _thread as thread except ImportError: @@ -91,10 +90,8 @@ def gen_filenames(): basedirs = [os.path.join(os.path.dirname(os.path.dirname(__file__)), 'conf', 'locale'), 'locale'] - for appname in reversed(settings.INSTALLED_APPS): - app = import_module(appname) - basedirs.append(os.path.join(os.path.dirname(upath(app.__file__)), - 'locale')) + for app_config in reversed(list(app_cache.get_app_configs())): + basedirs.append(os.path.join(app_config.path, 'locale')) basedirs.extend(settings.LOCALE_PATHS) basedirs = [os.path.abspath(basedir) for basedir in basedirs if os.path.isdir(basedir)] diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index 0c6bad2a7b..8d95fc00a3 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -58,18 +58,17 @@ def autodiscover_modules(*args, **kwargs): registry. This register_to object must have a _registry instance variable to access it. """ - from django.conf import settings + from django.core.apps import app_cache register_to = kwargs.get('register_to') - for app in settings.INSTALLED_APPS: - mod = import_module(app) + for app_config in app_cache.get_app_configs(): # Attempt to import the app's module. try: if register_to: before_import_registry = copy.copy(register_to._registry) for module_to_search in args: - import_module('%s.%s' % (app, module_to_search)) + import_module('%s.%s' % (app_config.name, module_to_search)) except: # Reset the model registry to the state before the last import as # this import will have to reoccur on the next request and this @@ -81,7 +80,7 @@ def autodiscover_modules(*args, **kwargs): # Decide whether to bubble up this error. If the app just # doesn't have an admin module, we can ignore the error # attempting to import it, otherwise we want it to bubble up. - if module_has_submodule(mod, module_to_search): + if module_has_submodule(app_config.app_module, module_to_search): raise diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index e547800b92..3ed6371966 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -7,10 +7,10 @@ import os import re import sys import gettext as gettext_module -from importlib import import_module from threading import local import warnings +from django.core.apps import app_cache from django.dispatch import receiver from django.test.signals import setting_changed from django.utils.encoding import force_str, force_text @@ -179,10 +179,8 @@ def translation(language): res.merge(t) return res - for appname in reversed(settings.INSTALLED_APPS): - app = import_module(appname) - apppath = os.path.join(os.path.dirname(upath(app.__file__)), 'locale') - + for app_config in reversed(list(app_cache.get_app_configs())): + apppath = os.path.join(app_config.path, 'locale') if os.path.isdir(apppath): res = _merge(apppath) diff --git a/django/views/i18n.py b/django/views/i18n.py index 751905efaf..2f0c89a037 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -5,6 +5,7 @@ import gettext as gettext_module from django import http from django.conf import settings +from django.core.apps import app_cache from django.template import Context, Template from django.utils.translation import check_for_language, to_locale, get_language from django.utils.encoding import smart_text @@ -187,7 +188,10 @@ def render_javascript_catalog(catalog=None, plural=None): def get_javascript_catalog(locale, domain, packages): default_locale = to_locale(settings.LANGUAGE_CODE) - packages = [p for p in packages if p == 'django.conf' or p in settings.INSTALLED_APPS] + app_configs = app_cache.get_app_configs() + allowable_packages = set(app_config.name for app_config in app_configs) + allowable_packages.add('django.conf') + packages = [p for p in packages if p in allowable_packages] t = {} paths = [] en_selected = locale.startswith('en') diff --git a/tests/bash_completion/tests.py b/tests/bash_completion/tests.py index 6ff709809d..85fb58904c 100644 --- a/tests/bash_completion/tests.py +++ b/tests/bash_completion/tests.py @@ -5,7 +5,7 @@ import os import sys import unittest -from django.conf import settings +from django.core.apps import app_cache from django.core.management import ManagementUtility from django.utils.six import StringIO @@ -84,5 +84,7 @@ class BashCompletionTests(unittest.TestCase): "Application names will be autocompleted for an AppCommand" self._user_input('django-admin.py sqlall a') output = self._run_autocomplete() - app_labels = [name.split('.')[-1] for name in settings.INSTALLED_APPS] - self.assertEqual(output, sorted(label for label in app_labels if label.startswith('a'))) + a_labels = sorted(app_config.label + for app_config in app_cache.get_app_configs() + if app_config.label.startswith('a')) + self.assertEqual(output, a_labels) diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 77b7d18455..339317c541 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -9,6 +9,7 @@ import pickle from threading import local from django.conf import settings +from django.core.apps import app_cache from django.template import Template, Context from django.template.base import TemplateSyntaxError from django.test import TestCase, RequestFactory @@ -1035,11 +1036,29 @@ class ResolutionOrderI18NTests(TransRealMixin, TestCase): "translation of '%s'; the actual result is '%s'." % (msgstr, msgid, result))) -@override_settings(INSTALLED_APPS=['i18n.resolution'] + list(settings.INSTALLED_APPS)) class AppResolutionOrderI18NTests(ResolutionOrderI18NTests): def test_app_translation(self): - self.assertUgettext('Date/time', 'APP') + # This test relies on an implementation detail, namely the fact that + # _with_app adds the app at the list. Adjust the test if this changes. + + # Original translation. + self.assertUgettext('Date/time', 'Datum/Zeit') + + # Different translation. + with app_cache._with_app('i18n.resolution'): + self.flush_caches() + activate('de') + + # Doesn't work because it's added later in the list. + self.assertUgettext('Date/time', 'Datum/Zeit') + + with app_cache._without_app('admin'): + self.flush_caches() + activate('de') + + # Unless the original is removed from the list. + self.assertUgettext('Date/time', 'Datum/Zeit (APP)') @override_settings(LOCALE_PATHS=extended_locale_paths) @@ -1049,8 +1068,7 @@ class LocalePathsResolutionOrderI18NTests(ResolutionOrderI18NTests): self.assertUgettext('Time', 'LOCALE_PATHS') def test_locale_paths_override_app_translation(self): - extended_apps = list(settings.INSTALLED_APPS) + ['i18n.resolution'] - with self.settings(INSTALLED_APPS=extended_apps): + with app_cache._with_app('i18n.resolution'): self.assertUgettext('Time', 'LOCALE_PATHS') diff --git a/tests/staticfiles_tests/test_liveserver.py b/tests/staticfiles_tests/test_liveserver.py index 71f4bb89a1..6fa64240cc 100644 --- a/tests/staticfiles_tests/test_liveserver.py +++ b/tests/staticfiles_tests/test_liveserver.py @@ -86,6 +86,9 @@ class StaticLiveServerChecks(LiveServerBase): class StaticLiveServerView(LiveServerBase): + # The test is going to access a static file stored in this application. + available_apps = ['staticfiles_tests.apps.test'] + def urlopen(self, url): return urlopen(self.live_server_url + url) diff --git a/tests/template_tests/test_loaders.py b/tests/template_tests/test_loaders.py index 6e74477e83..551bd7fb72 100644 --- a/tests/template_tests/test_loaders.py +++ b/tests/template_tests/test_loaders.py @@ -20,6 +20,7 @@ except ImportError: pkg_resources = None +from django.core.apps import app_cache from django.template import TemplateDoesNotExist, Context from django.template.loaders.eggs import Loader as EggLoader from django.template import loader @@ -49,7 +50,6 @@ def create_egg(name, resources): @unittest.skipUnless(pkg_resources, 'setuptools is not installed') -@override_settings(INSTALLED_APPS=[]) class EggLoaderTest(TestCase): def setUp(self): # Defined here b/c at module scope we may not have pkg_resources @@ -78,29 +78,28 @@ class EggLoaderTest(TestCase): os.path.normcase('templates/x.txt'): StringIO("x"), }) - @override_settings(INSTALLED_APPS=['egg_empty']) def test_empty(self): "Loading any template on an empty egg should fail" - egg_loader = EggLoader() - self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "not-existing.html") + with app_cache._with_app('egg_empty'): + egg_loader = EggLoader() + self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "not-existing.html") - @override_settings(INSTALLED_APPS=['egg_1']) def test_non_existing(self): "Template loading fails if the template is not in the egg" - egg_loader = EggLoader() - self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "not-existing.html") + with app_cache._with_app('egg_1'): + egg_loader = EggLoader() + self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "not-existing.html") - @override_settings(INSTALLED_APPS=['egg_1']) def test_existing(self): "A template can be loaded from an egg" - egg_loader = EggLoader() - contents, template_name = egg_loader.load_template_source("y.html") - self.assertEqual(contents, "y") - self.assertEqual(template_name, "egg:egg_1:templates/y.html") + with app_cache._with_app('egg_1'): + egg_loader = EggLoader() + contents, template_name = egg_loader.load_template_source("y.html") + self.assertEqual(contents, "y") + self.assertEqual(template_name, "egg:egg_1:templates/y.html") - @override_settings(INSTALLED_APPS=[]) def test_not_installed(self): - "Loading an existent template from an egg not included in INSTALLED_APPS should fail" + "Loading an existent template from an egg not included in any app should fail" egg_loader = EggLoader() self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "y.html") diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 651d46d4f1..61a46a81f9 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -16,6 +16,7 @@ import unittest import warnings from django import template +from django.core.apps import app_cache from django.core import urlresolvers from django.template import (base as template_base, loader, Context, RequestContext, Template, TemplateSyntaxError) @@ -1873,24 +1874,26 @@ class TemplateTagLoading(TestCase): self.assertTrue('ImportError' in e.args[0]) self.assertTrue('Xtemplate' in e.args[0]) - @override_settings(INSTALLED_APPS=('tagsegg',)) def test_load_error_egg(self): ttext = "{% load broken_egg %}" egg_name = '%s/tagsegg.egg' % self.egg_dir sys.path.append(egg_name) - self.assertRaises(template.TemplateSyntaxError, template.Template, ttext) + with self.assertRaises(template.TemplateSyntaxError): + with app_cache._with_app('tagsegg'): + template.Template(ttext) try: - template.Template(ttext) + with app_cache._with_app('tagsegg'): + template.Template(ttext) except template.TemplateSyntaxError as e: self.assertTrue('ImportError' in e.args[0]) self.assertTrue('Xtemplate' in e.args[0]) - @override_settings(INSTALLED_APPS=('tagsegg',)) def test_load_working_egg(self): ttext = "{% load working_egg %}" egg_name = '%s/tagsegg.egg' % self.egg_dir sys.path.append(egg_name) - template.Template(ttext) + with app_cache._with_app('tagsegg'): + template.Template(ttext) class RequestContextTests(unittest.TestCase): diff --git a/tests/utils_tests/test_module_loading.py b/tests/utils_tests/test_module_loading.py index 5a7eadcedf..0c8dfe1e12 100644 --- a/tests/utils_tests/test_module_loading.py +++ b/tests/utils_tests/test_module_loading.py @@ -5,9 +5,9 @@ import sys import unittest from zipimport import zipimporter +from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.test import SimpleTestCase -from django.test.utils import override_settings from django.utils import six from django.utils.module_loading import autodiscover_modules, import_by_path, module_has_submodule from django.utils._os import upath @@ -135,9 +135,14 @@ class ModuleImportTestCase(unittest.TestCase): 'Should have more than the calling frame in the traceback.') -@override_settings(INSTALLED_APPS=('utils_tests.test_module',)) class AutodiscoverModulesTestCase(SimpleTestCase): + def setUp(self): + self._with_test_module = app_cache._begin_with_app('utils_tests.test_module') + + def tearDown(self): + app_cache._end_with_app(self._with_test_module) + def test_autodiscover_modules_found(self): autodiscover_modules('good_module') diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index 42f82251f5..19bbde286c 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -5,6 +5,7 @@ from os import path import unittest from django.conf import settings +from django.core.apps import app_cache from django.core.urlresolvers import reverse from django.test import LiveServerTestCase, TestCase from django.test.utils import override_settings @@ -115,9 +116,8 @@ class JsI18NTests(TestCase): available. The Javascript i18n view must return a NON empty language catalog with the proper English translations. See #13726 for more details. """ - extended_apps = list(settings.INSTALLED_APPS) + ['view_tests.app0'] - with self.settings(LANGUAGE_CODE='fr', INSTALLED_APPS=extended_apps): - with override('en-us'): + with app_cache._with_app('view_tests.app0'): + with self.settings(LANGUAGE_CODE='fr'), override('en-us'): response = self.client.get('/views/jsi18n_english_translation/') self.assertContains(response, javascript_quote('this app0 string is to be translated')) @@ -144,9 +144,8 @@ class JsI18NTestsMultiPackage(TestCase): translations of multiple Python packages is requested. See #13388, #3594 and #13514 for more details. """ - extended_apps = list(settings.INSTALLED_APPS) + ['view_tests.app1', 'view_tests.app2'] - with self.settings(LANGUAGE_CODE='en-us', INSTALLED_APPS=extended_apps): - with override('fr'): + with app_cache._with_app('view_tests.app1'), app_cache._with_app('view_tests.app2'): + with self.settings(LANGUAGE_CODE='en-us'), override('fr'): response = self.client.get('/views/jsi18n_multi_packages1/') self.assertContains(response, javascript_quote('il faut traduire cette chaîne de caractères de app1')) @@ -155,9 +154,8 @@ class JsI18NTestsMultiPackage(TestCase): Similar to above but with neither default or requested language being English. """ - extended_apps = list(settings.INSTALLED_APPS) + ['view_tests.app3', 'view_tests.app4'] - with self.settings(LANGUAGE_CODE='fr', INSTALLED_APPS=extended_apps): - with override('es-ar'): + with app_cache._with_app('view_tests.app3'), app_cache._with_app('view_tests.app4'): + with self.settings(LANGUAGE_CODE='fr'), override('es-ar'): response = self.client.get('/views/jsi18n_multi_packages2/') self.assertContains(response, javascript_quote('este texto de app3 debe ser traducido')) From 70c9654d454fe7a2206319a20e0b4158a6de639d Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 19 Dec 2013 21:36:02 +0100 Subject: [PATCH 014/208] Renamed registered_model to has_model. This avoids possible confusion with register_model. Thanks Marc Tamlyn for the suggestion. --- django/core/apps/cache.py | 6 +++--- django/db/models/base.py | 4 ++-- django/db/models/fields/related.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index 0eddbed935..41196e5f05 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -298,12 +298,12 @@ class AppCache(object): models[model_name] = model self._get_models_cache.clear() - def registered_model(self, app_label, model_name): + def has_model(self, app_label, model_name): """ - Test if a model is registered and return the model class or None. + Returns the model class if one is registered and None otherwise. It's safe to call this method at import time, even while the app cache - is being populated. + is being populated. It returns None for models that aren't loaded yet. """ return self.all_models[app_label].get(model_name.lower()) diff --git a/django/db/models/base.py b/django/db/models/base.py index d1e7323d17..5599d81b4b 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -151,7 +151,7 @@ class ModelBase(type): new_class._base_manager = new_class._base_manager._copy_to_model(new_class) # Bail out early if we have already created this class. - m = new_class._meta.app_cache.registered_model(new_class._meta.app_label, name) + m = new_class._meta.app_cache.has_model(new_class._meta.app_label, name) if m is not None: return m @@ -278,7 +278,7 @@ class ModelBase(type): # the first time this model tries to register with the framework. There # should only be one class for each model, so we always return the # registered version. - return new_class._meta.app_cache.registered_model(new_class._meta.app_label, name) + return new_class._meta.app_cache.has_model(new_class._meta.app_label, name) def copy_managers(cls, base_managers): # This is in-place sorting of an Options attribute, but that's fine. diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index cf35504afe..0218c8c693 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -68,7 +68,7 @@ def add_lazy_relation(cls, field, relation, operation): # string right away. If get_model returns None, it means that the related # model isn't loaded yet, so we need to pend the relation until the class # is prepared. - model = cls._meta.app_cache.registered_model(app_label, model_name) + model = cls._meta.app_cache.has_model(app_label, model_name) if model: operation(field, model, cls) else: From 9cdf1f6d547b6dffcee26f0a525429452551bb4a Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 19 Dec 2013 21:57:09 +0100 Subject: [PATCH 015/208] Stop testing for inclusion in INSTALLED_APPS. Removed some exception masking in the comments app that was harmful and couldn't be preserved easily. --- .../admin/templatetags/admin_static.py | 4 +- django/contrib/comments/__init__.py | 19 +++---- django/contrib/messages/tests/base.py | 53 +++++++++---------- django/contrib/redirects/middleware.py | 3 +- django/contrib/redirects/tests.py | 9 ++-- .../contrib/sitemaps/tests/test_flatpages.py | 3 +- django/contrib/sitemaps/tests/test_http.py | 2 +- django/core/apps/cache.py | 12 +++++ django/test/client.py | 5 +- tests/comment_tests/tests/test_app_api.py | 21 +++----- 10 files changed, 66 insertions(+), 65 deletions(-) diff --git a/django/contrib/admin/templatetags/admin_static.py b/django/contrib/admin/templatetags/admin_static.py index 5ea3ba5838..75b077057e 100644 --- a/django/contrib/admin/templatetags/admin_static.py +++ b/django/contrib/admin/templatetags/admin_static.py @@ -1,9 +1,9 @@ -from django.conf import settings +from django.core.apps import app_cache from django.template import Library register = Library() -if 'django.contrib.staticfiles' in settings.INSTALLED_APPS: +if app_cache.has_app('django.contrib.staticfiles'): from django.contrib.staticfiles.templatetags.staticfiles import static else: from django.templatetags.static import static diff --git a/django/contrib/comments/__init__.py b/django/contrib/comments/__init__.py index 56ed32bafe..8d4b922966 100644 --- a/django/contrib/comments/__init__.py +++ b/django/contrib/comments/__init__.py @@ -2,6 +2,7 @@ from importlib import import_module import warnings from django.conf import settings from django.core import urlresolvers +from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.contrib.comments.models import Comment from django.contrib.comments.forms import CommentForm @@ -14,20 +15,12 @@ def get_comment_app(): """ Get the comment app (i.e. "django.contrib.comments") as defined in the settings """ - # Make sure the app's in INSTALLED_APPS - comments_app = get_comment_app_name() - if comments_app not in settings.INSTALLED_APPS: - raise ImproperlyConfigured("The COMMENTS_APP (%r) "\ - "must be in INSTALLED_APPS" % settings.COMMENTS_APP) - - # Try to import the package try: - package = import_module(comments_app) - except ImportError as e: - raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\ - "a non-existing package. (%s)" % e) - - return package + app_config = app_cache.get_app_config(get_comment_app_name().rpartition(".")[2]) + except LookupError: + raise ImproperlyConfigured("The COMMENTS_APP (%r) " + "must be in INSTALLED_APPS" % settings.COMMENTS_APP) + return app_config.app_module def get_comment_app_name(): """ diff --git a/django/contrib/messages/tests/base.py b/django/contrib/messages/tests/base.py index f3e676fac5..8d7906ba52 100644 --- a/django/contrib/messages/tests/base.py +++ b/django/contrib/messages/tests/base.py @@ -1,4 +1,4 @@ -from unittest import skipIf +from unittest import skipUnless from django import http from django.conf import settings, global_settings @@ -7,14 +7,15 @@ from django.contrib.messages.api import MessageFailure from django.contrib.messages.constants import DEFAULT_LEVELS from django.contrib.messages.storage import default_storage, base from django.contrib.messages.storage.base import Message +from django.core.apps import app_cache from django.core.urlresolvers import reverse from django.test.utils import override_settings from django.utils.translation import ugettext_lazy def skipUnlessAuthIsInstalled(func): - return skipIf( - 'django.contrib.auth' not in settings.INSTALLED_APPS, + return skipUnless( + app_cache.has_app('django.contrib.auth'), "django.contrib.auth isn't installed")(func) @@ -219,8 +220,6 @@ class BaseTests(object): self.assertContains(response, msg) @override_settings( - INSTALLED_APPS=filter( - lambda app: app != 'django.contrib.messages', settings.INSTALLED_APPS), MIDDLEWARE_CLASSES=filter( lambda m: 'MessageMiddleware' not in m, settings.MIDDLEWARE_CLASSES), TEMPLATE_CONTEXT_PROCESSORS=filter( @@ -233,19 +232,18 @@ class BaseTests(object): Tests that, when the middleware is disabled, an exception is raised when one attempts to store a message. """ - data = { - 'messages': ['Test message %d' % x for x in range(5)], - } - reverse('django.contrib.messages.tests.urls.show') - for level in ('debug', 'info', 'success', 'warning', 'error'): - add_url = reverse('django.contrib.messages.tests.urls.add', - args=(level,)) - self.assertRaises(MessageFailure, self.client.post, add_url, - data, follow=True) + with app_cache._without_app('django.contrib.messages'): + data = { + 'messages': ['Test message %d' % x for x in range(5)], + } + reverse('django.contrib.messages.tests.urls.show') + for level in ('debug', 'info', 'success', 'warning', 'error'): + add_url = reverse('django.contrib.messages.tests.urls.add', + args=(level,)) + self.assertRaises(MessageFailure, self.client.post, add_url, + data, follow=True) @override_settings( - INSTALLED_APPS=filter( - lambda app: app != 'django.contrib.messages', settings.INSTALLED_APPS), MIDDLEWARE_CLASSES=filter( lambda m: 'MessageMiddleware' not in m, settings.MIDDLEWARE_CLASSES), TEMPLATE_CONTEXT_PROCESSORS=filter( @@ -258,17 +256,18 @@ class BaseTests(object): Tests that, when the middleware is disabled, an exception is not raised if 'fail_silently' = True """ - data = { - 'messages': ['Test message %d' % x for x in range(5)], - 'fail_silently': True, - } - show_url = reverse('django.contrib.messages.tests.urls.show') - for level in ('debug', 'info', 'success', 'warning', 'error'): - add_url = reverse('django.contrib.messages.tests.urls.add', - args=(level,)) - response = self.client.post(add_url, data, follow=True) - self.assertRedirects(response, show_url) - self.assertFalse('messages' in response.context) + with app_cache._without_app('django.contrib.messages'): + data = { + 'messages': ['Test message %d' % x for x in range(5)], + 'fail_silently': True, + } + show_url = reverse('django.contrib.messages.tests.urls.show') + for level in ('debug', 'info', 'success', 'warning', 'error'): + add_url = reverse('django.contrib.messages.tests.urls.add', + args=(level,)) + response = self.client.post(add_url, data, follow=True) + self.assertRedirects(response, show_url) + self.assertFalse('messages' in response.context) def stored_messages_count(self, storage, response): """ diff --git a/django/contrib/redirects/middleware.py b/django/contrib/redirects/middleware.py index 9053c64a9a..e8aa06ebb1 100644 --- a/django/contrib/redirects/middleware.py +++ b/django/contrib/redirects/middleware.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from django.conf import settings from django.contrib.redirects.models import Redirect from django.contrib.sites.models import get_current_site +from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django import http @@ -14,7 +15,7 @@ class RedirectFallbackMiddleware(object): response_redirect_class = http.HttpResponsePermanentRedirect def __init__(self): - if 'django.contrib.sites' not in settings.INSTALLED_APPS: + if not app_cache.has_app('django.contrib.sites'): raise ImproperlyConfigured( "You cannot use RedirectFallbackMiddleware when " "django.contrib.sites is not installed." diff --git a/django/contrib/redirects/tests.py b/django/contrib/redirects/tests.py index 84638d6ab6..8566a34c68 100644 --- a/django/contrib/redirects/tests.py +++ b/django/contrib/redirects/tests.py @@ -1,6 +1,7 @@ from django import http from django.conf import settings from django.contrib.sites.models import Site +from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings @@ -56,12 +57,10 @@ class RedirectTests(TestCase): response = self.client.get('/initial') self.assertEqual(response.status_code, 410) - @override_settings( - INSTALLED_APPS=[app for app in settings.INSTALLED_APPS - if app != 'django.contrib.sites']) def test_sites_not_installed(self): - with self.assertRaises(ImproperlyConfigured): - RedirectFallbackMiddleware() + with app_cache._without_app('django.contrib.sites'): + with self.assertRaises(ImproperlyConfigured): + RedirectFallbackMiddleware() class OverriddenRedirectFallbackMiddleware(RedirectFallbackMiddleware): diff --git a/django/contrib/sitemaps/tests/test_flatpages.py b/django/contrib/sitemaps/tests/test_flatpages.py index 75ebb10f96..33ba168a45 100644 --- a/django/contrib/sitemaps/tests/test_flatpages.py +++ b/django/contrib/sitemaps/tests/test_flatpages.py @@ -3,13 +3,14 @@ from __future__ import unicode_literals from unittest import skipUnless from django.conf import settings +from django.core.apps import app_cache from .base import SitemapTestsBase class FlatpagesSitemapTests(SitemapTestsBase): - @skipUnless("django.contrib.flatpages" in settings.INSTALLED_APPS, + @skipUnless(app_cache.has_app('django.contrib.flatpages'), "django.contrib.flatpages app not installed.") def test_flatpage_sitemap(self): "Basic FlatPage sitemap test" diff --git a/django/contrib/sitemaps/tests/test_http.py b/django/contrib/sitemaps/tests/test_http.py index ed61c9950b..94d9adea49 100644 --- a/django/contrib/sitemaps/tests/test_http.py +++ b/django/contrib/sitemaps/tests/test_http.py @@ -118,7 +118,7 @@ class HTTPSitemapTests(SitemapTestsBase): """ % date.today() self.assertXMLEqual(response.content.decode('utf-8'), expected_content) - @skipUnless("django.contrib.sites" in settings.INSTALLED_APPS, + @skipUnless(app_cache.has_app('django.contrib.sites'), "django.contrib.sites app not installed.") def test_sitemap_get_urls_no_site_1(self): """ diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index 41196e5f05..697cf9e516 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -298,6 +298,18 @@ class AppCache(object): models[model_name] = model self._get_models_cache.clear() + def has_app(self, app_name): + """ + Returns the application config if one is registered and None otherwise. + + It's safe to call this method at import time, even while the app cache + is being populated. It returns None for apps that aren't loaded yet. + """ + app_config = self.app_configs.get(app_name.rpartition(".")[2]) + if app_config is not None and app_config.name != app_name: + app_config = None + return app_config + def has_model(self, app_label, model_name): """ Returns the model class if one is registered and None otherwise. diff --git a/django/test/client.py b/django/test/client.py index 8c9a17564c..cccc3ecc42 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -10,6 +10,7 @@ from io import BytesIO from django.conf import settings from django.contrib.auth import authenticate, login, logout, get_user_model +from django.core.apps import app_cache from django.core.handlers.base import BaseHandler from django.core.handlers.wsgi import WSGIRequest from django.core.signals import (request_started, request_finished, @@ -389,7 +390,7 @@ class Client(RequestFactory): """ Obtains the current session variables. """ - if 'django.contrib.sessions' in settings.INSTALLED_APPS: + if app_cache.has_app('django.contrib.sessions'): engine = import_module(settings.SESSION_ENGINE) cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None) if cookie: @@ -550,7 +551,7 @@ class Client(RequestFactory): """ user = authenticate(**credentials) if (user and user.is_active and - 'django.contrib.sessions' in settings.INSTALLED_APPS): + app_cache.has_app('django.contrib.sessions')): engine = import_module(settings.SESSION_ENGINE) # Create a fake request that goes through request middleware diff --git a/tests/comment_tests/tests/test_app_api.py b/tests/comment_tests/tests/test_app_api.py index 384802d1b2..645daf123d 100644 --- a/tests/comment_tests/tests/test_app_api.py +++ b/tests/comment_tests/tests/test_app_api.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib import comments from django.contrib.comments.models import Comment from django.contrib.comments.forms import CommentForm +from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.test.utils import override_settings from django.utils import six @@ -15,14 +16,6 @@ class CommentAppAPITests(CommentTestCase): def testGetCommentApp(self): self.assertEqual(comments.get_comment_app(), comments) - @override_settings( - COMMENTS_APP='missing_app', - INSTALLED_APPS=list(settings.INSTALLED_APPS) + ['missing_app'], - ) - def testGetMissingCommentApp(self): - with six.assertRaisesRegex(self, ImproperlyConfigured, 'missing_app'): - comments.get_comment_app() - def testGetForm(self): self.assertEqual(comments.get_form(), CommentForm) @@ -42,14 +35,16 @@ class CommentAppAPITests(CommentTestCase): self.assertEqual(comments.get_approve_url(c), "/approve/12345/") -@override_settings( - COMMENTS_APP='comment_tests.custom_comments', - INSTALLED_APPS=list(settings.INSTALLED_APPS) + [ - 'comment_tests.custom_comments'], -) +@override_settings(COMMENTS_APP='comment_tests.custom_comments') class CustomCommentTest(CommentTestCase): urls = 'comment_tests.urls' + def setUp(self): + self._with_custom_comments = app_cache._begin_with_app('comment_tests.custom_comments') + + def tearDown(self): + app_cache._end_with_app(self._with_custom_comments) + def testGetCommentApp(self): from comment_tests import custom_comments self.assertEqual(comments.get_comment_app(), custom_comments) From 2239081ff16aab1eaafea003433f7139bf13e097 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 19 Dec 2013 22:33:46 +0100 Subject: [PATCH 016/208] Expurged INSTALLED_APPS from code and tests. Except the app cache code and a few specific tests, of course. --- django/contrib/admin/sites.py | 16 +++++++--------- tests/migrations/test_writer.py | 15 +++++++-------- tests/proxy_model_inheritance/tests.py | 18 ++++++++---------- tests/staticfiles_tests/tests.py | 6 +++--- tests/utils_tests/test_autoreload.py | 13 ++++++++----- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 7b633bef89..a4caa863eb 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -6,6 +6,7 @@ from django.contrib.auth import logout as auth_logout, REDIRECT_FIELD_NAME from django.contrib.contenttypes import views as contenttype_views from django.views.decorators.csrf import csrf_protect from django.db.models.base import ModelBase +from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.urlresolvers import reverse, NoReverseMatch from django.template.response import TemplateResponse @@ -156,20 +157,17 @@ class AdminSite(object): """ Check that all things needed to run the admin have been correctly installed. - The default implementation checks that LogEntry, ContentType and the - auth context processor are installed. + The default implementation checks that admin and contenttypes apps are + installed, as well as the auth context processor. """ - from django.contrib.admin.models import LogEntry - from django.contrib.contenttypes.models import ContentType - - if not LogEntry._meta.installed: + app_cache.populate_apps() + if not app_cache.has_app('django.contrib.admin'): raise ImproperlyConfigured("Put 'django.contrib.admin' in your " "INSTALLED_APPS setting in order to use the admin application.") - if not ContentType._meta.installed: + if not app_cache.has_app('django.contrib.contenttypes'): raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in " "your INSTALLED_APPS setting in order to use the admin application.") - if not ('django.contrib.auth.context_processors.auth' in settings.TEMPLATE_CONTEXT_PROCESSORS or - 'django.core.context_processors.auth' in settings.TEMPLATE_CONTEXT_PROCESSORS): + if 'django.contrib.auth.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: raise ImproperlyConfigured("Put 'django.contrib.auth.context_processors.auth' " "in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.") diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index f921f9f03c..3f46703207 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -9,7 +9,7 @@ from django.core.apps import app_cache from django.core.validators import RegexValidator, EmailValidator from django.db import models, migrations from django.db.migrations.writer import MigrationWriter -from django.test import TestCase, override_settings +from django.test import TestCase from django.utils import six from django.utils.deconstruct import deconstructible from django.utils.translation import ugettext_lazy as _ @@ -122,10 +122,9 @@ class WriterTests(TestCase): base_dir = os.path.dirname(os.path.dirname(__file__)) - with override_settings(INSTALLED_APPS=test_apps): - for app in test_apps: - with app_cache._with_app(app): - migration = migrations.Migration('0001_initial', app.split('.')[-1]) - expected_path = os.path.join(base_dir, *(app.split('.') + ['migrations', '0001_initial.py'])) - writer = MigrationWriter(migration) - self.assertEqual(writer.path, expected_path) + for app in test_apps: + with app_cache._with_app(app): + migration = migrations.Migration('0001_initial', app.split('.')[-1]) + expected_path = os.path.join(base_dir, *(app.split('.') + ['migrations', '0001_initial.py'])) + writer = MigrationWriter(migration) + self.assertEqual(writer.path, expected_path) diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 01a2ddfef6..91fcbbcc09 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -13,7 +13,8 @@ from .models import (ConcreteModel, ConcreteModelSubclass, ConcreteModelSubclassProxy) -@override_settings(INSTALLED_APPS=('app1', 'app2')) +# Required for available_apps. +@override_settings(INSTALLED_APPS=['app1', 'app2']) class ProxyModelInheritanceTests(TransactionTestCase): """ Proxy model inheritance across apps can result in migrate not creating the table @@ -25,20 +26,17 @@ class ProxyModelInheritanceTests(TransactionTestCase): def setUp(self): self.old_sys_path = sys.path[:] sys.path.append(os.path.dirname(os.path.abspath(upath(__file__)))) - self._with_app1 = app_cache._begin_with_app('app1') - self._with_app2 = app_cache._begin_with_app('app2') def tearDown(self): - app_cache._end_with_app(self._with_app1) - app_cache._end_with_app(self._with_app2) sys.path = self.old_sys_path def test_table_exists(self): - call_command('migrate', verbosity=0) - from .app1.models import ProxyModel - from .app2.models import NiceModel - self.assertEqual(NiceModel.objects.all().count(), 0) - self.assertEqual(ProxyModel.objects.all().count(), 0) + with app_cache._with_app('app1'), app_cache._with_app('app2'): + call_command('migrate', verbosity=0) + from .app1.models import ProxyModel + from .app2.models import NiceModel + self.assertEqual(NiceModel.objects.all().count(), 0) + self.assertEqual(ProxyModel.objects.all().count(), 0) class MultiTableInheritanceProxyTest(TestCase): diff --git a/tests/staticfiles_tests/tests.py b/tests/staticfiles_tests/tests.py index f5d871587c..3ad8249e76 100644 --- a/tests/staticfiles_tests/tests.py +++ b/tests/staticfiles_tests/tests.py @@ -298,7 +298,7 @@ class TestCollectionDryRun(CollectionTestCase, TestNoFilesCreated): class TestCollectionFilesOverride(CollectionTestCase): """ Test overriding duplicated files by ``collectstatic`` management command. - Check for proper handling of apps order in INSTALLED_APPS even if file modification + Check for proper handling of apps order in installed apps even if file modification dates are in different order: 'staticfiles_tests.apps.test', @@ -314,7 +314,7 @@ class TestCollectionFilesOverride(CollectionTestCase): # prepare duplicate of file2.txt from no_label app # this file will have modification time older than no_label/static/file2.txt # anyway it should be taken to STATIC_ROOT because 'test' app is before - # 'no_label' app in INSTALLED_APPS + # 'no_label' app in installed apps self.testfile_path = os.path.join(TEST_ROOT, 'apps', 'test', 'static', 'file2.txt') with open(self.testfile_path, 'w+') as f: f.write('duplicate of file2.txt') @@ -340,7 +340,7 @@ class TestCollectionFilesOverride(CollectionTestCase): self.assertFileContains('file2.txt', 'duplicate of file2.txt') # and now change modification time of no_label/static/file2.txt - # test app is first in INSTALLED_APPS so file2.txt should remain unmodified + # test app is first in installed apps so file2.txt should remain unmodified mtime = os.path.getmtime(self.testfile_path) atime = os.path.getatime(self.testfile_path) os.utime(self.orig_path, (mtime + 1, atime + 1)) diff --git a/tests/utils_tests/test_autoreload.py b/tests/utils_tests/test_autoreload.py index d6e22ad8ee..47c70fae92 100644 --- a/tests/utils_tests/test_autoreload.py +++ b/tests/utils_tests/test_autoreload.py @@ -2,6 +2,7 @@ import os from django import conf from django.contrib import admin +from django.core.apps import app_cache from django.test import TestCase, override_settings from django.utils.autoreload import gen_filenames @@ -27,7 +28,6 @@ class TestFilenameGenerator(TestCase): self.assertIn(os.path.join(LOCALE_PATH, 'nl', 'LC_MESSAGES', 'django.mo'), filenames) - @override_settings(INSTALLED_APPS=()) def test_project_root_locale(self): """ Test that gen_filenames also yields from the current directory (project @@ -36,19 +36,22 @@ class TestFilenameGenerator(TestCase): old_cwd = os.getcwd() os.chdir(os.path.dirname(__file__)) try: - filenames = list(gen_filenames()) + # Remove the current app from the app cache to guarantee that the + # files will be found thanks to the current working directory. + with app_cache._empty(): + filenames = list(gen_filenames()) self.assertIn( os.path.join(LOCALE_PATH, 'nl', 'LC_MESSAGES', 'django.mo'), filenames) finally: os.chdir(old_cwd) - @override_settings(INSTALLED_APPS=('django.contrib.admin',)) def test_app_locales(self): """ - Test that gen_filenames also yields from INSTALLED_APPS locales. + Test that gen_filenames also yields from locale dirs in installed apps. """ - filenames = list(gen_filenames()) + with app_cache._empty(), app_cache._with_app('django.contrib.admin'): + filenames = list(gen_filenames()) self.assertIn(os.path.join(os.path.dirname(admin.__file__), 'locale', 'nl', 'LC_MESSAGES', 'django.mo'), filenames) From 16aae35ca8508dff2e0b94da67b8c880fe98e9d1 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 20 Dec 2013 10:39:12 +0100 Subject: [PATCH 017/208] Improved set_available_apps() in several ways. - Tested consistency the current app_configs instead of INSTALLED_APPS. - Considered applications added with _with_app as available. - Added docstrings. --- django/core/apps/cache.py | 19 ++++++++++++++++++- tests/proxy_model_inheritance/tests.py | 5 +---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index 697cf9e516..92b9f8d8d4 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -320,14 +320,27 @@ class AppCache(object): return self.all_models[app_label].get(model_name.lower()) def set_available_apps(self, available): + """ + Restricts the set of installed apps used by get_app_config[s]. + + available must be an iterable of application names. + + Primarily used for performance optimization in TransactionTestCase. + """ + 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(settings.INSTALLED_APPS) + 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 def unset_available_apps(self): + """ + Cancels a previous call to set_available_apps(). + """ self.available_apps = None ### DANGEROUS METHODS ### (only used to preserve existing tests) @@ -340,11 +353,15 @@ 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): diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 91fcbbcc09..5f33ec2fee 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -6,22 +6,19 @@ import sys from django.core.apps import app_cache from django.core.management import call_command from django.test import TestCase, TransactionTestCase -from django.test.utils import override_settings from django.utils._os import upath from .models import (ConcreteModel, ConcreteModelSubclass, ConcreteModelSubclassProxy) -# Required for available_apps. -@override_settings(INSTALLED_APPS=['app1', 'app2']) class ProxyModelInheritanceTests(TransactionTestCase): """ Proxy model inheritance across apps can result in migrate not creating the table for the proxied model (as described in #12286). This test creates two dummy apps and calls migrate, then verifies that the table has been created. """ - available_apps = ['app1', 'app2'] + available_apps = [] def setUp(self): self.old_sys_path = sys.path[:] From 517c24bcfabcc704a6b3b3f1eb39b05a3cf41954 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 20 Dec 2013 10:44:32 +0100 Subject: [PATCH 018/208] Complained on override_settings(INSTALLED_APPS=...). Currently such overrides aren't reflected in the app cache. It would be possible to handle them. But that doesn't look like a very good API. It makes it complicated to express "add this app" and "remove this app", which are the most common operations on INSTALLED_APPS. --- django/test/signals.py | 2 +- tests/app_loading/tests.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/django/test/signals.py b/django/test/signals.py index 8086d50b32..0d6b7bedf1 100644 --- a/django/test/signals.py +++ b/django/test/signals.py @@ -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']) +COMPLEX_OVERRIDE_SETTINGS = set(['DATABASES', 'INSTALLED_APPS']) @receiver(setting_changed) diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index bb71ad84a2..dcd0fa34c0 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import os import sys from unittest import TestCase +import warnings from django.core.apps import app_cache from django.core.apps.cache import AppCache @@ -77,11 +78,13 @@ class EggLoadingTest(TestCase): # Pretend we're the master app cache to test the population process. app_cache._apps_loaded = False app_cache._models_loaded = False - 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') + 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): From 99bd39ef6e1017f75c71fc276f43a941fe6081ea Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 20 Dec 2013 14:03:14 +0100 Subject: [PATCH 019/208] Added the ability to supply custom app configs. --- django/core/apps/base.py | 79 ++++++++++++++++++++++++++++++-------- django/core/apps/cache.py | 6 +-- tests/test_runner/tests.py | 4 +- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/django/core/apps/base.py b/django/core/apps/base.py index 2503d9e3ff..706ed54ef6 100644 --- a/django/core/apps/base.py +++ b/django/core/apps/base.py @@ -1,6 +1,7 @@ from importlib import import_module -from django.utils.module_loading import module_has_submodule +from django.core.exceptions import ImproperlyConfigured +from django.utils.module_loading import import_by_path, module_has_submodule from django.utils._os import upath @@ -14,16 +15,33 @@ class AppConfig(object): def __init__(self, app_name): # Full Python path to the application eg. 'django.contrib.admin'. - # This is the value that appears in INSTALLED_APPS. self.name = app_name + # Root module for the application eg. . + self.app_module = import_module(app_name) + + # The following attributes could be defined at the class level in a + # subclass, hence the test-and-set pattern. + # Last component of the Python path to the application eg. 'admin'. # This value must be unique across a Django project. - self.label = app_name.rpartition(".")[2] + if not hasattr(self, 'label'): + self.label = app_name.rpartition(".")[2] - # Root module eg. . - self.app_module = import_module(app_name) + # Human-readable name for the application eg. "Admin". + if not hasattr(self, 'verbose_name'): + self.verbose_name = self.label.title() + + # Filesystem path to the application directory eg. + # u'/usr/lib/python2.7/dist-packages/django/contrib/admin'. May be + # None if the application isn't a bona fide package eg. if it's an + # egg. Otherwise it's a unicode on Python 2 and a str on Python 3. + if not hasattr(self, 'path'): + try: + self.path = upath(self.app_module.__path__[0]) + except AttributeError: + self.path = None # Module containing models eg. . Set by import_models(). @@ -34,18 +52,49 @@ class AppConfig(object): # None to prevent accidental access before import_models() runs. self.models = None - # Filesystem path to the application directory eg. - # u'/usr/lib/python2.7/dist-packages/django/contrib/admin'. May be - # None if the application isn't a bona fide package eg. if it's an - # egg. Otherwise it's a unicode on Python 2 and a str on Python 3. - try: - self.path = upath(self.app_module.__path__[0]) - except AttributeError: - self.path = None - def __repr__(self): return '' % self.label + @classmethod + def create(cls, entry): + """ + Factory that creates an app config from an entry in INSTALLED_APPS. + """ + try: + # If import_module succeeds, entry is a path to an app module. + # Otherwise, entry is a path to an app config class or an error. + import_module(entry) + + except ImportError: + # Raise the original exception when entry cannot be a path to an + # app config class. Since module names are allowable here, the + # standard exception message from import_by_path is unsuitable. + if '.' not in entry: + raise + + cls = import_by_path(entry) + + # Check for obvious errors. (This check prevents duck typing, but + # it could be removed if it became a problem in practice.) + if not issubclass(cls, AppConfig): + raise ImproperlyConfigured( + "%r isn't a subclass of AppConfig." % entry) + + # Obtain app name here rather than in AppClass.__init__ to keep + # all error checking for entries in INSTALLED_APPS in one place. + try: + app_name = cls.name + except AttributeError: + raise ImproperlyConfigured( + "%r must supply a name attribute." % entry) + + # Entry is a path to an app config class. + return cls(app_name) + + else: + # Entry is a path to an app module. + return cls(entry) + def import_models(self, all_models): # Dictionary of models for this app, stored in the 'all_models' # attribute of the AppCache this AppConfig is attached to. Injected as diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index 92b9f8d8d4..056aa9a60b 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -83,7 +83,7 @@ class AppCache(object): # especially not other application modules, even indirectly. # Therefore we simply import them sequentially. for app_name in settings.INSTALLED_APPS: - app_config = AppConfig(app_name) + app_config = AppConfig.create(app_name) self.app_configs[app_config.label] = app_config self._apps_loaded = True @@ -347,7 +347,7 @@ class AppCache(object): def _begin_with_app(self, app_name): # Returns an opaque value that can be passed to _end_with_app(). - app_config = AppConfig(app_name) + app_config = AppConfig.create(app_name) if app_config.label in self.app_configs: return None else: @@ -412,7 +412,7 @@ class AppCache(object): warnings.warn( "load_app(app_name) is deprecated.", PendingDeprecationWarning, stacklevel=2) - app_config = AppConfig(app_name) + app_config = AppConfig.create(app_name) app_config.import_models(self.all_models[app_config.label]) self.app_configs[app_config.label] = app_config return app_config.models_module diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index a6dbeb0ae4..3c365b0225 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -226,7 +226,7 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase) "Check that the get_tests helper function can find tests in a directory" from django.core.apps.base import AppConfig from django.test.simple import get_tests - app_config = AppConfig('test_runner.valid_app') + app_config = AppConfig.create('test_runner.valid_app') app_config.import_models({}) tests = get_tests(app_config) self.assertIsInstance(tests, types.ModuleType) @@ -235,7 +235,7 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase) "Test for #12658 - Tests with ImportError's shouldn't fail silently" from django.core.apps.base import AppConfig from django.test.simple import get_tests - app_config = AppConfig('test_runner_invalid_app') + app_config = AppConfig.create('test_runner_invalid_app') app_config.import_models({}) with self.assertRaises(ImportError): get_tests(app_config) From 2fef9e5375b0267734392080858d9fced15ee2f9 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 22 Dec 2013 11:35:17 +0100 Subject: [PATCH 020/208] Moved apps back in the toplevel django namespace. Reverted 4a56a93cc458e9ab4dcab95d9f5067d4975dd1a2. --- django/{core => }/apps/__init__.py | 0 django/{core => }/apps/base.py | 0 django/{core => }/apps/cache.py | 0 django/contrib/admin/sites.py | 2 +- django/contrib/admin/templatetags/admin_static.py | 2 +- django/contrib/admin/validation.py | 2 +- django/contrib/admindocs/views.py | 2 +- django/contrib/auth/__init__.py | 2 +- django/contrib/auth/management/__init__.py | 2 +- django/contrib/auth/tests/test_management.py | 2 +- django/contrib/comments/__init__.py | 2 +- django/contrib/comments/views/comments.py | 2 +- django/contrib/contenttypes/management.py | 2 +- django/contrib/contenttypes/models.py | 2 +- django/contrib/contenttypes/tests.py | 2 +- django/contrib/gis/sitemaps/kml.py | 2 +- django/contrib/gis/sitemaps/views.py | 2 +- django/contrib/gis/tests/geoapp/test_feeds.py | 2 +- django/contrib/gis/tests/geoapp/test_sitemaps.py | 2 +- django/contrib/messages/tests/base.py | 2 +- django/contrib/redirects/middleware.py | 2 +- django/contrib/redirects/tests.py | 2 +- django/contrib/sitemaps/tests/test_flatpages.py | 2 +- django/contrib/sitemaps/tests/test_http.py | 2 +- django/contrib/sites/tests.py | 2 +- django/contrib/staticfiles/finders.py | 2 +- django/core/checks/compatibility/django_1_6_0.py | 2 +- django/core/management/__init__.py | 4 ++-- django/core/management/base.py | 2 +- django/core/management/commands/dumpdata.py | 4 ++-- django/core/management/commands/flush.py | 2 +- django/core/management/commands/loaddata.py | 2 +- django/core/management/commands/makemigrations.py | 2 +- django/core/management/commands/migrate.py | 2 +- django/core/management/commands/shell.py | 2 +- django/core/management/commands/sqlsequencereset.py | 2 +- django/core/management/sql.py | 2 +- django/core/management/validation.py | 2 +- django/core/serializers/base.py | 2 +- django/core/serializers/python.py | 2 +- django/core/serializers/xml_serializer.py | 2 +- django/db/backends/__init__.py | 6 +++--- django/db/backends/sqlite3/schema.py | 2 +- django/db/migrations/loader.py | 2 +- django/db/migrations/questioner.py | 2 +- django/db/migrations/recorder.py | 2 +- django/db/migrations/state.py | 2 +- django/db/migrations/writer.py | 2 +- django/db/models/base.py | 4 ++-- django/db/models/fields/__init__.py | 2 +- django/db/models/loading.py | 2 +- django/db/models/options.py | 2 +- django/db/models/signals.py | 2 +- django/db/utils.py | 2 +- django/template/base.py | 2 +- django/template/loaders/app_directories.py | 2 +- django/template/loaders/eggs.py | 2 +- django/test/client.py | 2 +- django/test/simple.py | 2 +- django/test/testcases.py | 2 +- django/utils/autoreload.py | 2 +- django/utils/module_loading.py | 2 +- django/utils/translation/trans_real.py | 2 +- django/views/i18n.py | 2 +- tests/admin_docs/tests.py | 2 +- tests/app_cache/models.py | 2 +- tests/app_cache/tests.py | 4 ++-- tests/app_loading/tests.py | 4 ++-- tests/bash_completion/tests.py | 2 +- tests/commands_sql/tests.py | 2 +- tests/comment_tests/tests/test_app_api.py | 2 +- tests/contenttypes_tests/tests.py | 2 +- tests/defer_regress/tests.py | 2 +- tests/i18n/tests.py | 2 +- tests/invalid_models_tests/tests.py | 2 +- tests/managers_regress/tests.py | 2 +- tests/migrations/models.py | 2 +- tests/migrations/test_commands.py | 2 +- tests/migrations/test_state.py | 2 +- tests/migrations/test_writer.py | 2 +- tests/no_models/tests.py | 2 +- tests/proxy_model_inheritance/tests.py | 2 +- tests/proxy_models/tests.py | 2 +- tests/runtests.py | 4 ++-- tests/schema/models.py | 2 +- tests/swappable_models/tests.py | 2 +- tests/tablespaces/tests.py | 2 +- tests/template_tests/test_loaders.py | 2 +- tests/template_tests/tests.py | 2 +- tests/test_runner/tests.py | 4 ++-- tests/test_suite_override/tests.py | 2 +- tests/utils_tests/test_autoreload.py | 2 +- tests/utils_tests/test_module_loading.py | 2 +- tests/validation/test_unique.py | 2 +- tests/view_tests/tests/test_i18n.py | 2 +- 95 files changed, 101 insertions(+), 101 deletions(-) rename django/{core => }/apps/__init__.py (100%) rename django/{core => }/apps/base.py (100%) rename django/{core => }/apps/cache.py (100%) diff --git a/django/core/apps/__init__.py b/django/apps/__init__.py similarity index 100% rename from django/core/apps/__init__.py rename to django/apps/__init__.py diff --git a/django/core/apps/base.py b/django/apps/base.py similarity index 100% rename from django/core/apps/base.py rename to django/apps/base.py diff --git a/django/core/apps/cache.py b/django/apps/cache.py similarity index 100% rename from django/core/apps/cache.py rename to django/apps/cache.py diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index a4caa863eb..d72666a257 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -6,7 +6,7 @@ from django.contrib.auth import logout as auth_logout, REDIRECT_FIELD_NAME from django.contrib.contenttypes import views as contenttype_views from django.views.decorators.csrf import csrf_protect from django.db.models.base import ModelBase -from django.core.apps import app_cache +from django.apps import app_cache from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.urlresolvers import reverse, NoReverseMatch from django.template.response import TemplateResponse diff --git a/django/contrib/admin/templatetags/admin_static.py b/django/contrib/admin/templatetags/admin_static.py index 75b077057e..7c7a5e5654 100644 --- a/django/contrib/admin/templatetags/admin_static.py +++ b/django/contrib/admin/templatetags/admin_static.py @@ -1,4 +1,4 @@ -from django.core.apps import app_cache +from django.apps import app_cache from django.template import Library register = Library() diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index ee945f73e5..18e3ad48f8 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -1,4 +1,4 @@ -from django.core.apps import app_cache +from django.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.fields import FieldDoesNotExist diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 9e6dd85595..973c81ddaf 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -5,10 +5,10 @@ import re import warnings from django import template +from django.apps import app_cache from django.conf import settings from django.contrib import admin from django.contrib.admin.views.decorators import staff_member_required -from django.core.apps import app_cache from django.db import models from django.core.exceptions import ViewDoesNotExist from django.http import Http404 diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index d5fc14dd2a..3c5a40c184 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -123,7 +123,7 @@ def get_user_model(): """ Returns the User model that is active in this project. """ - from django.core.apps import app_cache + from django.apps import app_cache try: app_label, model_name = settings.AUTH_USER_MODEL.split('.') diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index ec963becc4..5f24bf069e 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -6,9 +6,9 @@ from __future__ import unicode_literals import getpass import unicodedata +from django.apps import app_cache, UnavailableApp from django.contrib.auth import (models as auth_app, get_permission_codename, get_user_model) -from django.core.apps import app_cache, UnavailableApp from django.core import exceptions from django.core.management.base import CommandError from django.db import DEFAULT_DB_ALIAS, router diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index d9de5a2ad0..c005e74442 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from datetime import date +from django.apps import app_cache from django.contrib.auth import models, management from django.contrib.auth.management import create_permissions from django.contrib.auth.management.commands import changepassword @@ -8,7 +9,6 @@ from django.contrib.auth.models import User from django.contrib.auth.tests.custom_user import CustomUser from django.contrib.auth.tests.utils import skipIfCustomUser from django.contrib.contenttypes.models import ContentType -from django.core.apps import app_cache from django.core import exceptions from django.core.management import call_command from django.core.management.base import CommandError diff --git a/django/contrib/comments/__init__.py b/django/contrib/comments/__init__.py index 8d4b922966..f265875c36 100644 --- a/django/contrib/comments/__init__.py +++ b/django/contrib/comments/__init__.py @@ -1,8 +1,8 @@ from importlib import import_module import warnings +from django.apps import app_cache from django.conf import settings from django.core import urlresolvers -from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.contrib.comments.models import Comment from django.contrib.comments.forms import CommentForm diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 294c7c8e42..5d7c543adb 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -1,9 +1,9 @@ from django import http +from django.apps import app_cache from django.conf import settings from django.contrib import comments from django.contrib.comments import signals from django.contrib.comments.views.utils import next_redirect, confirmation_view -from django.core.apps import app_cache from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import models from django.shortcuts import render_to_response diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py index 7ff08b70f8..35a8a7fdc8 100644 --- a/django/contrib/contenttypes/management.py +++ b/django/contrib/contenttypes/management.py @@ -1,5 +1,5 @@ +from django.apps import app_cache, UnavailableApp from django.contrib.contenttypes.models import ContentType -from django.core.apps import app_cache, UnavailableApp from django.db import DEFAULT_DB_ALIAS, router from django.db.models import signals from django.utils.encoding import smart_text diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index 98eae0219c..90dea5b811 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -1,4 +1,4 @@ -from django.core.apps import app_cache +from django.apps import app_cache from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_text, force_text diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index b194755b80..302cdbb5cf 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals +from django.apps import app_cache from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.views import shortcut from django.contrib.sites.models import get_current_site -from django.core.apps import app_cache from django.db import models from django.http import HttpRequest, Http404 from django.test import TestCase diff --git a/django/contrib/gis/sitemaps/kml.py b/django/contrib/gis/sitemaps/kml.py index aa9ae69f5d..1e4fc82550 100644 --- a/django/contrib/gis/sitemaps/kml.py +++ b/django/contrib/gis/sitemaps/kml.py @@ -1,4 +1,4 @@ -from django.core.apps import app_cache +from django.apps import app_cache from django.core import urlresolvers from django.contrib.sitemaps import Sitemap from django.contrib.gis.db.models.fields import GeometryField diff --git a/django/contrib/gis/sitemaps/views.py b/django/contrib/gis/sitemaps/views.py index e55a371672..e68523981e 100644 --- a/django/contrib/gis/sitemaps/views.py +++ b/django/contrib/gis/sitemaps/views.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import warnings -from django.core.apps import app_cache +from django.apps import app_cache from django.http import HttpResponse, Http404 from django.template import loader from django.contrib.sites.models import get_current_site diff --git a/django/contrib/gis/tests/geoapp/test_feeds.py b/django/contrib/gis/tests/geoapp/test_feeds.py index 6f0d901a62..638944db63 100644 --- a/django/contrib/gis/tests/geoapp/test_feeds.py +++ b/django/contrib/gis/tests/geoapp/test_feeds.py @@ -3,11 +3,11 @@ from __future__ import unicode_literals from unittest import skipUnless from xml.dom import minidom +from django.apps import app_cache from django.conf import settings from django.contrib.sites.models import Site from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import HAS_SPATIAL_DB -from django.core.apps import app_cache from django.test import TestCase if HAS_GEOS: diff --git a/django/contrib/gis/tests/geoapp/test_sitemaps.py b/django/contrib/gis/tests/geoapp/test_sitemaps.py index 035d97af34..909a543bb6 100644 --- a/django/contrib/gis/tests/geoapp/test_sitemaps.py +++ b/django/contrib/gis/tests/geoapp/test_sitemaps.py @@ -6,11 +6,11 @@ from xml.dom import minidom import os import zipfile +from django.apps import app_cache from django.conf import settings from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import HAS_SPATIAL_DB from django.contrib.sites.models import Site -from django.core.apps import app_cache from django.test import TestCase from django.test.utils import IgnoreDeprecationWarningsMixin from django.utils._os import upath diff --git a/django/contrib/messages/tests/base.py b/django/contrib/messages/tests/base.py index 8d7906ba52..e03d229be3 100644 --- a/django/contrib/messages/tests/base.py +++ b/django/contrib/messages/tests/base.py @@ -1,13 +1,13 @@ from unittest import skipUnless from django import http +from django.apps import app_cache from django.conf import settings, global_settings from django.contrib.messages import constants, utils, get_level, set_level from django.contrib.messages.api import MessageFailure from django.contrib.messages.constants import DEFAULT_LEVELS from django.contrib.messages.storage import default_storage, base from django.contrib.messages.storage.base import Message -from django.core.apps import app_cache from django.core.urlresolvers import reverse from django.test.utils import override_settings from django.utils.translation import ugettext_lazy diff --git a/django/contrib/redirects/middleware.py b/django/contrib/redirects/middleware.py index e8aa06ebb1..c5b672659e 100644 --- a/django/contrib/redirects/middleware.py +++ b/django/contrib/redirects/middleware.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals +from django.apps import app_cache from django.conf import settings from django.contrib.redirects.models import Redirect from django.contrib.sites.models import get_current_site -from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django import http diff --git a/django/contrib/redirects/tests.py b/django/contrib/redirects/tests.py index 8566a34c68..0784a6a6f3 100644 --- a/django/contrib/redirects/tests.py +++ b/django/contrib/redirects/tests.py @@ -1,7 +1,7 @@ from django import http +from django.apps import app_cache from django.conf import settings from django.contrib.sites.models import Site -from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings diff --git a/django/contrib/sitemaps/tests/test_flatpages.py b/django/contrib/sitemaps/tests/test_flatpages.py index 33ba168a45..90172e558f 100644 --- a/django/contrib/sitemaps/tests/test_flatpages.py +++ b/django/contrib/sitemaps/tests/test_flatpages.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from unittest import skipUnless +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from .base import SitemapTestsBase diff --git a/django/contrib/sitemaps/tests/test_http.py b/django/contrib/sitemaps/tests/test_http.py index 94d9adea49..fca644538b 100644 --- a/django/contrib/sitemaps/tests/test_http.py +++ b/django/contrib/sitemaps/tests/test_http.py @@ -4,10 +4,10 @@ import os from datetime import date from unittest import skipUnless +from django.apps import app_cache from django.conf import settings from django.contrib.sitemaps import Sitemap, GenericSitemap from django.contrib.sites.models import Site -from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.test.utils import override_settings from django.utils.formats import localize diff --git a/django/contrib/sites/tests.py b/django/contrib/sites/tests.py index 8d76fd0d02..79bcae9aa6 100644 --- a/django/contrib/sites/tests.py +++ b/django/contrib/sites/tests.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals +from django.apps import app_cache from django.conf import settings from django.contrib.sites.models import Site, RequestSite, get_current_site -from django.core.apps import app_cache from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.http import HttpRequest from django.test import TestCase diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py index cadc8bffa7..de907ef8ef 100644 --- a/django/contrib/staticfiles/finders.py +++ b/django/contrib/staticfiles/finders.py @@ -1,8 +1,8 @@ from collections import OrderedDict import os +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import default_storage, Storage, FileSystemStorage from django.utils.functional import empty, LazyObject diff --git a/django/core/checks/compatibility/django_1_6_0.py b/django/core/checks/compatibility/django_1_6_0.py index fef8bc3a51..a42249de60 100644 --- a/django/core/checks/compatibility/django_1_6_0.py +++ b/django/core/checks/compatibility/django_1_6_0.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.core.apps import app_cache +from django.apps import app_cache from django.db import models diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index a7d074e149..bc81b8f9d2 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -117,7 +117,7 @@ def get_commands(): # Populate the app cache outside of the try/except block to avoid # catching ImproperlyConfigured errors that aren't caused by the # absence of a settings module. - from django.core.apps import app_cache + from django.apps import app_cache app_configs = app_cache.get_app_configs() apps = [app_config.name for app_config in app_configs] @@ -346,7 +346,7 @@ class ManagementUtility(object): elif cwords[0] in ('dumpdata', 'sql', 'sqlall', 'sqlclear', 'sqlcustom', 'sqlindexes', 'sqlsequencereset', 'test'): try: - from django.core.apps import app_cache + from django.apps import app_cache app_configs = app_cache.get_app_configs() # Get the last part of the dotted path as the app name. options += [(app_config.label, 0) for app_config in app_configs] diff --git a/django/core/management/base.py b/django/core/management/base.py index b9404ec1aa..101a486b40 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -341,7 +341,7 @@ class AppCommand(BaseCommand): args = '' def handle(self, *app_labels, **options): - from django.core.apps import app_cache + from django.apps import app_cache if not app_labels: raise CommandError('Enter at least one appname.') # Populate models and don't use only_with_models_module=True when diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index fa657bcd72..9aebb6c7d6 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -37,7 +37,7 @@ class Command(BaseCommand): args = '[appname appname.ModelName ...]' def handle(self, *app_labels, **options): - from django.core.apps import app_cache + from django.apps import app_cache format = options.get('format') indent = options.get('indent') @@ -162,7 +162,7 @@ def sort_dependencies(app_list): is serialized before a normal model, and any model with a natural key dependency has it's dependencies serialized first. """ - from django.core.apps import app_cache + from django.apps import app_cache # Process the list of models, and get the list of dependencies model_dependencies = [] models = set() diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index f13e948ae5..49007bf0a2 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -2,7 +2,7 @@ import sys from importlib import import_module from optparse import make_option -from django.core.apps import app_cache +from django.apps import app_cache from django.db import connections, router, transaction, DEFAULT_DB_ALIAS from django.core.management import call_command from django.core.management.base import NoArgsCommand, CommandError diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index ee88232d9f..bfeba68aa6 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -7,8 +7,8 @@ import warnings import zipfile from optparse import make_option +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.core import serializers from django.core.management.base import BaseCommand, CommandError from django.core.management.color import no_style diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index 3bd7ad42be..aaf0270840 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -3,7 +3,7 @@ import os import operator from optparse import make_option -from django.core.apps import app_cache +from django.apps import app_cache from django.core.management.base import BaseCommand, CommandError from django.db import connections, DEFAULT_DB_ALIAS, migrations from django.db.migrations.loader import MigrationLoader diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index c99f26aa0c..05b4a62e87 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -6,7 +6,7 @@ from importlib import import_module import itertools import traceback -from django.core.apps import app_cache +from django.apps import app_cache from django.core.management import call_command from django.core.management.base import BaseCommand, CommandError from django.core.management.color import no_style diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index 5c189ac980..12af814161 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -66,7 +66,7 @@ class Command(NoArgsCommand): def handle_noargs(self, **options): # XXX: (Temporary) workaround for ticket #1796: force early loading of all # models from installed apps. - from django.core.apps import app_cache + from django.apps import app_cache app_cache.get_models() use_plain = options.get('plain', False) diff --git a/django/core/management/commands/sqlsequencereset.py b/django/core/management/commands/sqlsequencereset.py index cc35030a91..24ec25e8ed 100644 --- a/django/core/management/commands/sqlsequencereset.py +++ b/django/core/management/commands/sqlsequencereset.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from optparse import make_option -from django.core.apps import app_cache +from django.apps import app_cache from django.core.management.base import AppCommand from django.db import connections, DEFAULT_DB_ALIAS diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 0b6e38124e..b2500d3787 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -5,8 +5,8 @@ import os import re import warnings +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.core.management.base import CommandError from django.db import models, router diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 1cedcb9925..2b27a7edce 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -26,7 +26,7 @@ def get_validation_errors(outfile, app=None): validates all models of all installed apps. Writes errors, if any, to outfile. Returns number of errors. """ - from django.core.apps import app_cache + from django.apps import app_cache from django.db import connection, models from django.db.models.deletion import SET_NULL, SET_DEFAULT diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index b0bc8138ab..d516656a69 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -3,7 +3,7 @@ Module for abstract serializer/unserializer base classes. """ import warnings -from django.core.apps import app_cache +from django.apps import app_cache from django.db import models from django.utils import six diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 1e4c37e2ae..c8a2b7eff2 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -5,8 +5,8 @@ other serializers. """ from __future__ import unicode_literals +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.core.serializers import base from django.db import models, DEFAULT_DB_ALIAS from django.utils.encoding import smart_text, is_protected_type diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 90ad2cf398..e9bea84bb1 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -4,8 +4,8 @@ XML serializer. from __future__ import unicode_literals +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.core.serializers import base from django.db import models, DEFAULT_DB_ALIAS from django.utils.xmlutils import SimplerXMLGenerator diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index adf3b236ac..86905afe77 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1268,7 +1268,7 @@ class BaseDatabaseIntrospection(object): If only_existing is True, the resulting list will only include the tables that actually exist in the database. """ - from django.core.apps import app_cache + from django.apps import app_cache from django.db import router tables = set() for app_config in app_cache.get_app_configs(only_with_models_module=True): @@ -1289,7 +1289,7 @@ class BaseDatabaseIntrospection(object): def installed_models(self, tables): "Returns a set of all models represented by the provided list of table names." - from django.core.apps import app_cache + from django.apps import app_cache from django.db import router all_models = [] for app_config in app_cache.get_app_configs(only_with_models_module=True): @@ -1302,7 +1302,7 @@ class BaseDatabaseIntrospection(object): def sequence_list(self): "Returns a list of information about all DB sequences for all models in all apps." - from django.core.apps import app_cache + from django.apps import app_cache from django.db import models, router sequence_list = [] diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index e2b9603819..52ca2de8ed 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -1,4 +1,4 @@ -from django.core.apps.cache import AppCache +from django.apps.cache import AppCache from django.db.backends.schema import BaseDatabaseSchemaEditor from django.db.models.fields.related import ManyToManyField diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py index 4c12e05add..9a54f14e75 100644 --- a/django/db/migrations/loader.py +++ b/django/db/migrations/loader.py @@ -2,7 +2,7 @@ from importlib import import_module import os import sys -from django.core.apps import app_cache +from django.apps import app_cache from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.graph import MigrationGraph from django.utils import six diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py index 8a11559993..4a860dcf7e 100644 --- a/django/db/migrations/questioner.py +++ b/django/db/migrations/questioner.py @@ -2,7 +2,7 @@ import importlib import os import sys -from django.core.apps import app_cache +from django.apps import app_cache from django.utils import datetime_safe from django.utils.six.moves import input diff --git a/django/db/migrations/recorder.py b/django/db/migrations/recorder.py index c2cfd2e9af..8b0403e88f 100644 --- a/django/db/migrations/recorder.py +++ b/django/db/migrations/recorder.py @@ -1,4 +1,4 @@ -from django.core.apps.cache import AppCache +from django.apps.cache import AppCache from django.db import models from django.utils.timezone import now diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index c43728d58e..a2716e864a 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -1,4 +1,4 @@ -from django.core.apps.cache import AppCache +from django.apps.cache import AppCache from django.db import models from django.db.models.options import DEFAULT_NAMES, normalize_unique_together from django.utils import six diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index 04e00501f9..a23a9b1253 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -5,7 +5,7 @@ from importlib import import_module import os import types -from django.core.apps import app_cache +from django.apps import app_cache from django.db import models from django.db.migrations.loader import MigrationLoader from django.utils.encoding import force_text diff --git a/django/db/models/base.py b/django/db/models/base.py index 5599d81b4b..3ae0fe34bd 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -5,8 +5,8 @@ import sys from functools import update_wrapper from django.utils.six.moves import zip -from django.core.apps import app_cache -from django.core.apps.base import MODELS_MODULE_NAME +from django.apps import app_cache +from django.apps.base import MODELS_MODULE_NAME import django.db.models.manager # NOQA: Imported to register signal handler. from django.conf import settings from django.core.exceptions import (ObjectDoesNotExist, diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index b24323d1cf..830ff2efa2 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -9,7 +9,7 @@ import warnings from base64 import b64decode, b64encode from itertools import tee -from django.core.apps import app_cache +from django.apps import app_cache from django.db import connection from django.db.models.query_utils import QueryWrapper from django.conf import settings diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 00fae36769..4b58ba00b5 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -1,6 +1,6 @@ import warnings -from django.core.apps import app_cache +from django.apps import app_cache warnings.warn( "The utilities in django.db.models.loading are deprecated " diff --git a/django/db/models/options.py b/django/db/models/options.py index fec9aa4fc4..8f181c8854 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -5,8 +5,8 @@ import re from bisect import bisect import warnings +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.db.models.fields.related import ManyToManyRel from django.db.models.fields import AutoField, FieldDoesNotExist from django.db.models.fields.proxy import OrderWrt diff --git a/django/db/models/signals.py b/django/db/models/signals.py index a6822309a3..8c835e5f5f 100644 --- a/django/db/models/signals.py +++ b/django/db/models/signals.py @@ -1,6 +1,6 @@ from collections import defaultdict -from django.core.apps import app_cache +from django.apps import app_cache from django.dispatch import Signal from django.utils import six diff --git a/django/db/utils.py b/django/db/utils.py index 4d53d252bf..702b1b4ebc 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -282,6 +282,6 @@ class ConnectionRouter(object): """ Return app models allowed to be synchronized on provided db. """ - from django.core.apps import app_cache + from django.apps import app_cache return [model for model in app_cache.get_models(app, include_auto_created=include_auto_created) if self.allow_migrate(db, model)] diff --git a/django/template/base.py b/django/template/base.py index 7098ea60f8..197238b1e3 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -5,8 +5,8 @@ from functools import partial from importlib import import_module from inspect import getargspec, getcallargs +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.template.context import (BaseContext, Context, RequestContext, # NOQA: imported for backwards compatibility ContextPopException) from django.utils.itercompat import is_iterable diff --git a/django/template/loaders/app_directories.py b/django/template/loaders/app_directories.py index 90baa34c7a..ff3d3b93f8 100644 --- a/django/template/loaders/app_directories.py +++ b/django/template/loaders/app_directories.py @@ -6,8 +6,8 @@ packages. import os import sys +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.template.base import TemplateDoesNotExist from django.template.loader import BaseLoader from django.utils._os import safe_join diff --git a/django/template/loaders/eggs.py b/django/template/loaders/eggs.py index e9b46b15de..99c19aed93 100644 --- a/django/template/loaders/eggs.py +++ b/django/template/loaders/eggs.py @@ -6,8 +6,8 @@ try: except ImportError: resource_string = None +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.template.base import TemplateDoesNotExist from django.template.loader import BaseLoader from django.utils import six diff --git a/django/test/client.py b/django/test/client.py index cccc3ecc42..eea9b43010 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -8,9 +8,9 @@ from copy import copy from importlib import import_module from io import BytesIO +from django.apps import app_cache from django.conf import settings from django.contrib.auth import authenticate, login, logout, get_user_model -from django.core.apps import app_cache from django.core.handlers.base import BaseHandler from django.core.handlers.wsgi import WSGIRequest from django.core.signals import (request_started, request_finished, diff --git a/django/test/simple.py b/django/test/simple.py index 6129ce1305..f2d26376df 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -9,7 +9,7 @@ import re import unittest as real_unittest import warnings -from django.core.apps import app_cache +from django.apps import app_cache from django.test import _doctest as doctest from django.test import runner from django.test.utils import compare_xml, strip_quotes diff --git a/django/test/testcases.py b/django/test/testcases.py index 13f56fa6cc..1480889565 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -15,9 +15,9 @@ import unittest from unittest import skipIf # NOQA: Imported here for backward compatibility from unittest.util import safe_repr +from django.apps import app_cache from django.conf import settings from django.core import mail -from django.core.apps import app_cache from django.core.exceptions import ValidationError, ImproperlyConfigured from django.core.handlers.wsgi import get_path_info, WSGIHandler from django.core.management import call_command diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index 05ffb12f74..b7a889aa61 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -36,8 +36,8 @@ import sys import time import traceback +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.core.signals import request_finished try: from django.utils.six.moves import _thread as thread diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index 8d95fc00a3..c7826be5e5 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -58,7 +58,7 @@ def autodiscover_modules(*args, **kwargs): registry. This register_to object must have a _registry instance variable to access it. """ - from django.core.apps import app_cache + from django.apps import app_cache register_to = kwargs.get('register_to') for app_config in app_cache.get_app_configs(): diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 3ed6371966..1eced98d31 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -10,7 +10,7 @@ import gettext as gettext_module from threading import local import warnings -from django.core.apps import app_cache +from django.apps import app_cache from django.dispatch import receiver from django.test.signals import setting_changed from django.utils.encoding import force_str, force_text diff --git a/django/views/i18n.py b/django/views/i18n.py index 2f0c89a037..2cc2e35e5e 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -4,8 +4,8 @@ import os import gettext as gettext_module from django import http +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.template import Context, Template from django.utils.translation import check_for_language, to_locale, get_language from django.utils.encoding import smart_text diff --git a/tests/admin_docs/tests.py b/tests/admin_docs/tests.py index 66c125490f..9e004a5a07 100644 --- a/tests/admin_docs/tests.py +++ b/tests/admin_docs/tests.py @@ -1,10 +1,10 @@ import unittest +from django.apps import app_cache from django.conf import settings from django.contrib.sites.models import Site from django.contrib.admindocs import utils from django.contrib.auth.models import User -from django.core.apps import app_cache from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings diff --git a/tests/app_cache/models.py b/tests/app_cache/models.py index 10e9e6f1de..9306830f9c 100644 --- a/tests/app_cache/models.py +++ b/tests/app_cache/models.py @@ -1,4 +1,4 @@ -from django.core.apps.cache import AppCache +from django.apps.cache import AppCache from django.db import models # We're testing app cache presence on load, so this is handy. diff --git a/tests/app_cache/tests.py b/tests/app_cache/tests.py index a531a22e8b..43133ab38f 100644 --- a/tests/app_cache/tests.py +++ b/tests/app_cache/tests.py @@ -1,7 +1,7 @@ from __future__ import absolute_import -from django.core.apps import app_cache -from django.core.apps.cache import AppCache +from django.apps import app_cache +from django.apps.cache import AppCache from django.db import models from django.test import TestCase diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index dcd0fa34c0..0692597a5b 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -5,8 +5,8 @@ import sys from unittest import TestCase import warnings -from django.core.apps import app_cache -from django.core.apps.cache import AppCache +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 diff --git a/tests/bash_completion/tests.py b/tests/bash_completion/tests.py index 85fb58904c..5a6a6c48d2 100644 --- a/tests/bash_completion/tests.py +++ b/tests/bash_completion/tests.py @@ -5,7 +5,7 @@ import os import sys import unittest -from django.core.apps import app_cache +from django.apps import app_cache from django.core.management import ManagementUtility from django.utils.six import StringIO diff --git a/tests/commands_sql/tests.py b/tests/commands_sql/tests.py index 3088ea4d48..1c2a7f0ffa 100644 --- a/tests/commands_sql/tests.py +++ b/tests/commands_sql/tests.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.core.apps import app_cache +from django.apps import app_cache from django.core.management.color import no_style from django.core.management.sql import (sql_create, sql_delete, sql_indexes, sql_destroy_indexes, sql_all) diff --git a/tests/comment_tests/tests/test_app_api.py b/tests/comment_tests/tests/test_app_api.py index 645daf123d..7098def28a 100644 --- a/tests/comment_tests/tests/test_app_api.py +++ b/tests/comment_tests/tests/test_app_api.py @@ -1,8 +1,8 @@ +from django.apps import app_cache from django.conf import settings from django.contrib import comments from django.contrib.comments.models import Comment from django.contrib.comments.forms import CommentForm -from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.test.utils import override_settings from django.utils import six diff --git a/tests/contenttypes_tests/tests.py b/tests/contenttypes_tests/tests.py index 9cc254e665..d3c19fd910 100644 --- a/tests/contenttypes_tests/tests.py +++ b/tests/contenttypes_tests/tests.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals +from django.apps.cache import AppCache from django.contrib.contenttypes.models import ContentType -from django.core.apps.cache import AppCache from django.db import models from django.test import TestCase diff --git a/tests/defer_regress/tests.py b/tests/defer_regress/tests.py index 3e9651b2d2..442af9f7cf 100644 --- a/tests/defer_regress/tests.py +++ b/tests/defer_regress/tests.py @@ -2,9 +2,9 @@ from __future__ import unicode_literals from operator import attrgetter +from django.apps import app_cache from django.contrib.contenttypes.models import ContentType from django.contrib.sessions.backends.db import SessionStore -from django.core.apps import app_cache from django.db.models import Count from django.test import TestCase from django.test.utils import override_settings diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 339317c541..8d8b5fbe9c 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -8,8 +8,8 @@ import os import pickle from threading import local +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.template import Template, Context from django.template.base import TemplateSyntaxError from django.test import TestCase, RequestFactory diff --git a/tests/invalid_models_tests/tests.py b/tests/invalid_models_tests/tests.py index 4e0cef546b..09c19d453c 100644 --- a/tests/invalid_models_tests/tests.py +++ b/tests/invalid_models_tests/tests.py @@ -1,7 +1,7 @@ import sys import unittest -from django.core.apps import app_cache +from django.apps import app_cache from django.core.management.validation import get_validation_errors from django.test.utils import override_settings from django.utils.six import StringIO diff --git a/tests/managers_regress/tests.py b/tests/managers_regress/tests.py index b9c8244612..9d7e0d8174 100644 --- a/tests/managers_regress/tests.py +++ b/tests/managers_regress/tests.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.core.apps import app_cache +from django.apps import app_cache from django.db import models from django.template import Context, Template from django.test import TestCase diff --git a/tests/migrations/models.py b/tests/migrations/models.py index cfc9604551..b1ef6c1dd1 100644 --- a/tests/migrations/models.py +++ b/tests/migrations/models.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.core.apps.cache import AppCache +from django.apps.cache import AppCache from django.db import models from django.utils.encoding import python_2_unicode_compatible diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 13514cac91..fcb460decc 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -5,7 +5,7 @@ import codecs import os import shutil -from django.core.apps import app_cache +from django.apps import app_cache from django.core.management import call_command, CommandError from django.test.utils import override_settings from django.utils import six diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index 7a06ecd474..e695248cd2 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -1,4 +1,4 @@ -from django.core.apps.cache import AppCache +from django.apps.cache import AppCache from django.db import models from django.db.migrations.state import ProjectState, ModelState, InvalidBasesError from django.test import TestCase diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 3f46703207..df3d052610 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import datetime import os -from django.core.apps import app_cache +from django.apps import app_cache from django.core.validators import RegexValidator, EmailValidator from django.db import models, migrations from django.db.migrations.writer import MigrationWriter diff --git a/tests/no_models/tests.py b/tests/no_models/tests.py index 34ef724446..f9ff80485e 100644 --- a/tests/no_models/tests.py +++ b/tests/no_models/tests.py @@ -1,4 +1,4 @@ -from django.core.apps import app_cache +from django.apps import app_cache from django.test import TestCase diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 5f33ec2fee..c0b564746d 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import os import sys -from django.core.apps import app_cache +from django.apps import app_cache from django.core.management import call_command from django.test import TestCase, TransactionTestCase from django.utils._os import upath diff --git a/tests/proxy_models/tests.py b/tests/proxy_models/tests.py index 0995a778c0..800fc42e6d 100644 --- a/tests/proxy_models/tests.py +++ b/tests/proxy_models/tests.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals +from django.apps import app_cache from django.contrib import admin from django.contrib.contenttypes.models import ContentType from django.core import management -from django.core.apps import app_cache from django.core.exceptions import FieldError from django.db import models, DEFAULT_DB_ALIAS from django.db.models import signals diff --git a/tests/runtests.py b/tests/runtests.py index 64d363a095..88973308cf 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -80,14 +80,14 @@ def get_test_modules(): def get_installed(): - from django.core.apps import app_cache + from django.apps import app_cache return [app_config.name for app_config in app_cache.get_app_configs()] def setup(verbosity, test_labels): import django + from django.apps import app_cache from django.conf import settings - from django.core.apps import app_cache from django.test import TransactionTestCase, TestCase print("Testing against Django installed in '%s'" % os.path.dirname(django.__file__)) diff --git a/tests/schema/models.py b/tests/schema/models.py index 8b24b88c05..b294647f9b 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -1,4 +1,4 @@ -from django.core.apps.cache import AppCache +from django.apps.cache import AppCache from django.db import models # Because we want to test creation and deletion of these as separate things, diff --git a/tests/swappable_models/tests.py b/tests/swappable_models/tests.py index fec9db5af8..0beb95af3e 100644 --- a/tests/swappable_models/tests.py +++ b/tests/swappable_models/tests.py @@ -2,9 +2,9 @@ from __future__ import unicode_literals from django.utils.six import StringIO +from django.apps import app_cache from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType -from django.core.apps import app_cache from django.core import management from django.test import TestCase from django.test.utils import override_settings diff --git a/tests/tablespaces/tests.py b/tests/tablespaces/tests.py index fa90704c45..d761a93ab9 100644 --- a/tests/tablespaces/tests.py +++ b/tests/tablespaces/tests.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.db import connection from django.core.management.color import no_style from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature diff --git a/tests/template_tests/test_loaders.py b/tests/template_tests/test_loaders.py index 551bd7fb72..eadb3fb02a 100644 --- a/tests/template_tests/test_loaders.py +++ b/tests/template_tests/test_loaders.py @@ -20,7 +20,7 @@ except ImportError: pkg_resources = None -from django.core.apps import app_cache +from django.apps import app_cache from django.template import TemplateDoesNotExist, Context from django.template.loaders.eggs import Loader as EggLoader from django.template import loader diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 61a46a81f9..4dc0925070 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -16,7 +16,7 @@ import unittest import warnings from django import template -from django.core.apps import app_cache +from django.apps import app_cache from django.core import urlresolvers from django.template import (base as template_base, loader, Context, RequestContext, Template, TemplateSyntaxError) diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 3c365b0225..5393ea75a2 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -224,7 +224,7 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase) def test_get_tests(self): "Check that the get_tests helper function can find tests in a directory" - from django.core.apps.base import AppConfig + from django.apps.base import AppConfig from django.test.simple import get_tests app_config = AppConfig.create('test_runner.valid_app') app_config.import_models({}) @@ -233,7 +233,7 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase) def test_import_error(self): "Test for #12658 - Tests with ImportError's shouldn't fail silently" - from django.core.apps.base import AppConfig + from django.apps.base import AppConfig from django.test.simple import get_tests app_config = AppConfig.create('test_runner_invalid_app') app_config.import_models({}) diff --git a/tests/test_suite_override/tests.py b/tests/test_suite_override/tests.py index b464659275..9666df1c19 100644 --- a/tests/test_suite_override/tests.py +++ b/tests/test_suite_override/tests.py @@ -1,6 +1,6 @@ import unittest -from django.core.apps import app_cache +from django.apps import app_cache from django.test.utils import IgnoreAllDeprecationWarningsMixin diff --git a/tests/utils_tests/test_autoreload.py b/tests/utils_tests/test_autoreload.py index 47c70fae92..5071566e92 100644 --- a/tests/utils_tests/test_autoreload.py +++ b/tests/utils_tests/test_autoreload.py @@ -1,8 +1,8 @@ import os from django import conf +from django.apps import app_cache from django.contrib import admin -from django.core.apps import app_cache from django.test import TestCase, override_settings from django.utils.autoreload import gen_filenames diff --git a/tests/utils_tests/test_module_loading.py b/tests/utils_tests/test_module_loading.py index 0c8dfe1e12..baaab36494 100644 --- a/tests/utils_tests/test_module_loading.py +++ b/tests/utils_tests/test_module_loading.py @@ -5,7 +5,7 @@ import sys import unittest from zipimport import zipimporter -from django.core.apps import app_cache +from django.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.test import SimpleTestCase from django.utils import six diff --git a/tests/validation/test_unique.py b/tests/validation/test_unique.py index f05256a6f1..be7808f460 100644 --- a/tests/validation/test_unique.py +++ b/tests/validation/test_unique.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import datetime import unittest -from django.core.apps.cache import AppCache +from django.apps.cache import AppCache from django.core.exceptions import ValidationError from django.db import models from django.test import TestCase diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index 19bbde286c..1e4eff66df 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -4,8 +4,8 @@ import os from os import path import unittest +from django.apps import app_cache from django.conf import settings -from django.core.apps import app_cache from django.core.urlresolvers import reverse from django.test import LiveServerTestCase, TestCase from django.test.utils import override_settings From 690d1cb8d0341ac71613f6bb93f0793335ec6dfd Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 22 Dec 2013 11:36:49 +0100 Subject: [PATCH 021/208] Made AppConfig importable from django.apps. It is a public API. --- django/apps/__init__.py | 1 + tests/test_runner/tests.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/django/apps/__init__.py b/django/apps/__init__.py index 0384b1257d..a1193eafcc 100644 --- a/django/apps/__init__.py +++ b/django/apps/__init__.py @@ -1 +1,2 @@ +from .base import AppConfig # NOQA from .cache import app_cache, UnavailableApp # NOQA diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 5393ea75a2..0daf4ab3b7 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -224,7 +224,7 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase) def test_get_tests(self): "Check that the get_tests helper function can find tests in a directory" - from django.apps.base import AppConfig + from django.apps import AppConfig from django.test.simple import get_tests app_config = AppConfig.create('test_runner.valid_app') app_config.import_models({}) @@ -233,7 +233,7 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase) def test_import_error(self): "Test for #12658 - Tests with ImportError's shouldn't fail silently" - from django.apps.base import AppConfig + from django.apps import AppConfig from django.test.simple import get_tests app_config = AppConfig.create('test_runner_invalid_app') app_config.import_models({}) From ba60fcbcf7645afd82f3135ecb56562f8a9c9824 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 20 Dec 2013 14:12:25 +0100 Subject: [PATCH 022/208] Used application verbose names in the admin. --- django/contrib/admin/actions.py | 1 - django/contrib/admin/options.py | 3 --- django/contrib/admin/sites.py | 9 +++++---- .../admin/templates/admin/auth/user/change_password.html | 2 +- django/contrib/admin/templates/admin/change_form.html | 2 +- django/contrib/admin/templates/admin/change_list.html | 2 +- .../templates/admin/delete_selected_confirmation.html | 2 +- django/contrib/admin/templates/admin/object_history.html | 2 +- 8 files changed, 10 insertions(+), 13 deletions(-) diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py index d215aa6259..158c436c71 100644 --- a/django/contrib/admin/actions.py +++ b/django/contrib/admin/actions.py @@ -71,7 +71,6 @@ def delete_selected(modeladmin, request, queryset): "perms_lacking": perms_needed, "protected": protected, "opts": opts, - "app_label": app_label, 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, } diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 4c5adaf0f8..02f9ed3a84 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1308,7 +1308,6 @@ class ModelAdmin(BaseModelAdmin): media=media, inline_admin_formsets=inline_admin_formsets, errors=helpers.AdminErrorList(form, formsets), - app_label=opts.app_label, preserved_filters=self.get_preserved_filters(request), ) context.update(extra_context or {}) @@ -1532,7 +1531,6 @@ class ModelAdmin(BaseModelAdmin): media=media, has_add_permission=self.has_add_permission(request), opts=cl.opts, - app_label=app_label, action_form=action_form, actions_on_top=self.actions_on_top, actions_on_bottom=self.actions_on_bottom, @@ -1627,7 +1625,6 @@ class ModelAdmin(BaseModelAdmin): action_list=action_list, module_name=capfirst(force_text(opts.verbose_name_plural)), object=obj, - app_label=app_label, opts=opts, preserved_filters=self.get_preserved_filters(request), ) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index d72666a257..22c1b29ab4 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -381,7 +381,7 @@ class AdminSite(object): app_dict[app_label]['models'].append(model_dict) else: app_dict[app_label] = { - 'name': app_label.title(), + 'name': app_cache.get_app_config(app_label).verbose_name, 'app_label': app_label, 'app_url': reverse('admin:app_list', kwargs={'app_label': app_label}, current_app=self.name), 'has_module_perms': has_module_perms, @@ -390,7 +390,7 @@ class AdminSite(object): # Sort the apps alphabetically. app_list = list(six.itervalues(app_dict)) - app_list.sort(key=lambda x: x['name']) + app_list.sort(key=lambda x: x['name'].lower()) # Sort the models alphabetically within each app. for app in app_list: @@ -408,6 +408,7 @@ class AdminSite(object): def app_index(self, request, app_label, extra_context=None): user = request.user + app_name = app_cache.get_app_config(app_label).verbose_name has_module_perms = user.has_module_perms(app_label) if not has_module_perms: raise PermissionDenied @@ -442,7 +443,7 @@ class AdminSite(object): # something to display, add in the necessary meta # information. app_dict = { - 'name': app_label.title(), + 'name': app_name, 'app_label': app_label, 'app_url': '', 'has_module_perms': has_module_perms, @@ -453,7 +454,7 @@ class AdminSite(object): # Sort the models alphabetically within each app. app_dict['models'].sort(key=lambda x: x['name']) context = dict(self.each_context(), - title=_('%s administration') % capfirst(app_label), + title=_('%s administration') % app_name, app_list=[app_dict], app_label=app_label, ) diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index 2a1b4d3c90..3634ed4083 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -11,7 +11,7 @@ {% block breadcrumbs %}