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