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',))