diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 9079678639cdf..a15967e024a2a 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -1,3 +1,4 @@ +from django.core.apps import app_cache from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.fields import FieldDoesNotExist @@ -15,9 +16,9 @@ class BaseValidator(object): def __init__(self): - # Before we can introspect models, they need to be fully loaded so that - # inter-relations are set up correctly. We force that here. - models.get_apps() + # 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() def validate(self, cls, model): for m in dir(self): diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 482d33e76f75f..dee75fa678bde 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -7,8 +7,9 @@ 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 ImproperlyConfigured, ViewDoesNotExist +from django.core.exceptions import ViewDoesNotExist from django.http import Http404 from django.core import urlresolvers from django.contrib.admindocs import utils @@ -182,7 +183,7 @@ class ModelIndexView(BaseAdminDocsView): template_name = 'admin_doc/model_index.html' def get_context_data(self, **kwargs): - m_list = [m._meta for m in models.get_models()] + m_list = [m._meta for m in app_cache.get_models()] kwargs.update({'models': m_list}) return super(ModelIndexView, self).get_context_data(**kwargs) @@ -193,17 +194,12 @@ class ModelDetailView(BaseAdminDocsView): def get_context_data(self, **kwargs): # Get the model class. try: - app_mod = models.get_app(self.kwargs['app_label']) - except ImproperlyConfigured: - raise Http404(_("App %r not found") % self.kwargs['app_label']) - model = None - for m in models.get_models(app_mod): - if m._meta.model_name == self.kwargs['model_name']: - model = m - break + app_cache.get_app_config(self.kwargs['app_label']) + except LookupError: + raise Http404(_("App %(app_label)r not found") % self.kwargs) + model = app_cache.get_model(self.kwargs['app_label'], self.kwargs['model_name']) if model is None: - raise Http404(_("Model %(model_name)r not found in app %(app_label)r") % { - 'model_name': self.kwargs['model_name'], 'app_label': self.kwargs['app_label']}) + raise Http404(_("Model %(model_name)r not found in app %(app_label)r") % self.kwargs) opts = model._meta diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 83aa445bac631..d5fc14dd2a60d 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -123,13 +123,13 @@ def get_user_model(): """ Returns the User model that is active in this project. """ - from django.db.models import get_model + from django.core.apps import app_cache try: app_label, model_name = settings.AUTH_USER_MODEL.split('.') except ValueError: raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'") - user_model = get_model(app_label, model_name) + user_model = app_cache.get_model(app_label, model_name) if user_model is None: raise ImproperlyConfigured("AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL) return user_model diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 8fd08ac57cf6c..ec963becc47a1 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -8,10 +8,11 @@ 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 -from django.db.models import get_model, get_models, signals, UnavailableApp +from django.db.models import signals from django.utils.encoding import DEFAULT_LOCALE_ENCODING from django.utils import six from django.utils.six.moves import input @@ -61,7 +62,7 @@ def _check_permission_clashing(custom, builtin, ctype): def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs): try: - get_model('auth', 'Permission') + app_cache.get_model('auth', 'Permission') except UnavailableApp: return @@ -70,7 +71,7 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw from django.contrib.contenttypes.models import ContentType - app_models = get_models(app) + app_models = app_cache.get_models(app) # This will hold the permissions we're looking for as # (content_type, (codename, name)) @@ -119,7 +120,7 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw def create_superuser(app, created_models, verbosity, db, **kwargs): try: - get_model('auth', 'Permission') + app_cache.get_model('auth', 'Permission') UserModel = get_user_model() except UnavailableApp: return diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index bd51f39977ada..d9de5a2ad0984 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -8,11 +8,11 @@ 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 from django.core.management.validation import get_validation_errors -from django.db.models.loading import get_app from django.test import TestCase from django.test.utils import override_settings from django.utils import six @@ -184,21 +184,21 @@ class CustomUserModelValidationTestCase(TestCase): def test_required_fields_is_list(self): "REQUIRED_FIELDS should be a list." new_io = StringIO() - get_validation_errors(new_io, get_app('auth')) + get_validation_errors(new_io, app_cache.get_app_config('auth').models_module) self.assertIn("The REQUIRED_FIELDS must be a list or tuple.", new_io.getvalue()) @override_settings(AUTH_USER_MODEL='auth.CustomUserBadRequiredFields') def test_username_not_in_required_fields(self): "USERNAME_FIELD should not appear in REQUIRED_FIELDS." new_io = StringIO() - get_validation_errors(new_io, get_app('auth')) + get_validation_errors(new_io, app_cache.get_app_config('auth').models_module) self.assertIn("The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.", new_io.getvalue()) @override_settings(AUTH_USER_MODEL='auth.CustomUserNonUniqueUsername') def test_username_non_unique(self): "A non-unique USERNAME_FIELD should raise a model validation error." new_io = StringIO() - get_validation_errors(new_io, get_app('auth')) + get_validation_errors(new_io, app_cache.get_app_config('auth').models_module) self.assertIn("The USERNAME_FIELD must be unique. Add unique=True to the field parameters.", new_io.getvalue()) diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index a2cbe33c0efc4..294c7c8e42b72 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -3,6 +3,7 @@ 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 @@ -48,7 +49,7 @@ def post_comment(request, next=None, using=None): if ctype is None or object_pk is None: return CommentPostBadRequest("Missing content_type or object_pk field.") try: - model = models.get_model(*ctype.split(".", 1)) + model = app_cache.get_model(*ctype.split(".", 1)) target = model._default_manager.using(using).get(pk=object_pk) except TypeError: return CommentPostBadRequest( diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py index 3d2fc4b95ec2c..7ff08b70f8902 100644 --- a/django/contrib/contenttypes/management.py +++ b/django/contrib/contenttypes/management.py @@ -1,6 +1,7 @@ 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 get_apps, get_model, get_models, signals, UnavailableApp +from django.db.models import signals from django.utils.encoding import smart_text from django.utils import six from django.utils.six.moves import input @@ -12,7 +13,7 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, * entries that no longer have a matching model class. """ try: - get_model('contenttypes', 'ContentType') + app_cache.get_model('contenttypes', 'ContentType') except UnavailableApp: return @@ -20,7 +21,7 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, * return ContentType.objects.clear_cache() - app_models = get_models(app) + app_models = app_cache.get_models(app) if not app_models: return # They all have the same app_label, get the first one. @@ -85,8 +86,8 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, * def update_all_contenttypes(verbosity=2, **kwargs): - for app in get_apps(): - update_contenttypes(app, None, verbosity, **kwargs) + for app_config in app_cache.get_app_configs(only_with_models_module=True): + update_contenttypes(app_config.models_module, None, verbosity, **kwargs) signals.post_migrate.connect(update_contenttypes) diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index aec15d6664896..98eae0219c390 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -1,3 +1,4 @@ +from django.core.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 @@ -156,7 +157,7 @@ def __str__(self): def model_class(self): "Returns the Python model class for this type of content." - return models.get_model(self.app_label, self.model, + return app_cache.get_model(self.app_label, self.model, only_installed=False) def get_object_for_this_type(self, **kwargs): diff --git a/django/contrib/gis/sitemaps/kml.py b/django/contrib/gis/sitemaps/kml.py index 5e74c6c47dc0c..aa9ae69f5d35e 100644 --- a/django/contrib/gis/sitemaps/kml.py +++ b/django/contrib/gis/sitemaps/kml.py @@ -1,3 +1,4 @@ +from django.core.apps import app_cache from django.core import urlresolvers from django.contrib.sitemaps import Sitemap from django.contrib.gis.db.models.fields import GeometryField @@ -25,7 +26,7 @@ def _build_kml_sources(self, sources): """ kml_sources = [] if sources is None: - sources = models.get_models() + sources = app_cache.get_models() for source in sources: if isinstance(source, models.base.ModelBase): for field in source._meta.fields: diff --git a/django/contrib/gis/sitemaps/views.py b/django/contrib/gis/sitemaps/views.py index 0672b800cc2ec..e55a3716723e3 100644 --- a/django/contrib/gis/sitemaps/views.py +++ b/django/contrib/gis/sitemaps/views.py @@ -2,6 +2,7 @@ import warnings +from django.core.apps import app_cache from django.http import HttpResponse, Http404 from django.template import loader from django.contrib.sites.models import get_current_site @@ -9,7 +10,6 @@ from django.core.paginator import EmptyPage, PageNotAnInteger from django.contrib.gis.db.models.fields import GeometryField from django.db import connections, DEFAULT_DB_ALIAS -from django.db.models import get_model from django.db.models.fields import FieldDoesNotExist from django.utils import six from django.utils.translation import ugettext as _ @@ -81,7 +81,7 @@ def kml(request, label, model, field_name=None, compress=False, using=DEFAULT_DB must be that of a geographic field. """ placemarks = [] - klass = get_model(label, model) + klass = app_cache.get_model(label, model) if not klass: raise Http404('You must supply a valid app label and module name. Got "%s.%s"' % (label, model)) diff --git a/django/core/apps/__init__.py b/django/core/apps/__init__.py new file mode 100644 index 0000000000000..0384b1257d2ff --- /dev/null +++ b/django/core/apps/__init__.py @@ -0,0 +1 @@ +from .cache import app_cache, UnavailableApp # NOQA diff --git a/django/core/apps/base.py b/django/core/apps/base.py new file mode 100644 index 0000000000000..860777bb0301b --- /dev/null +++ b/django/core/apps/base.py @@ -0,0 +1,47 @@ +from collections import OrderedDict + +from django.utils._os import upath + + +class AppConfig(object): + """ + Class representing a Django application and its configuration. + """ + + def __init__(self, name, app_module, models_module): + # Full Python path to the application eg. 'django.contrib.admin'. + # This is the value that appears in INSTALLED_APPS. + self.name = 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] + + # Root module eg. . + self.app_module = app_module + + # Module containing models eg. . None if the application + # doesn't have a models module. + self.models_module = models_module + + # Mapping of lower case model names to model classes. + # 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 + + @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 new file mode 100644 index 0000000000000..9b847cc3091bd --- /dev/null +++ b/django/core/apps/cache.py @@ -0,0 +1,416 @@ +"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 +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._os import upath +from django.utils import six + +from .base import AppConfig + + +MODELS_MODULE_NAME = 'models' + + +class UnavailableApp(Exception): + pass + + +def _initialize(): + """ + Returns a dictionary to be used as the initial value of the + [shared] state of the app cache. + """ + return dict( + # Mapping of labels to AppConfig instances for installed apps. + app_configs=OrderedDict(), + + # Pending lookups for lazy relations + pending_lookups={}, + + # Set of app names. Allows restricting the set of installed apps. + # Used by TransactionTestCase.available_apps for performance reasons. + available_apps=None, + + # -- Everything below here is only used when populating the cache -- + loads_installed=True, + loaded=False, + handled=set(), + postponed=[], + nesting_level=0, + _get_models_cache={}, + ) + + +class BaseAppCache(object): + """ + A cache that stores installed applications and their models. Used to + provide reverse-relations and for app introspection (e.g. admin). + + This provides the base (non-Borg) AppCache class - the AppCache + subclass adds borg-like behaviour for the few cases where it's needed. + """ + + def __init__(self): + self.__dict__ = _initialize() + # This stops populate loading from INSTALLED_APPS and ignores the + # only_installed arguments to get_model[s] + self.loads_installed = False + + 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.loads_installed: + 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. + imp.acquire_lock() + try: + 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 + finally: + imp.release_lock() + + def load_app(self, app_name, can_postpone=False): + """ + 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( + 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 + self.app_configs[app_config.label] = app_config + + return models_module + + def app_cache_ready(self): + """ + Returns true if the model cache is fully populated. + + Useful for code that wants to cache the results of get_models() for + themselves once it is safe to do so. + """ + return self.loaded + + def get_app_configs(self, only_installed=True, 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. + """ + 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): + """ + Returns the application configuration for the given app_label. + + Raises LookupError if no application exists with this app_label. + + 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. + """ + 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) + if only_with_models_module and app_config.models_module is None: + raise LookupError("App with label %r doesn't have a models module." % app_label) + if self.available_apps is not None and app_config.name not in self.available_apps: + raise UnavailableApp("App with label %r isn't available." % app_label) + return app_config + + def get_models(self, app_mod=None, + include_auto_created=False, include_deferred=False, + only_installed=True, include_swapped=False): + """ + Given a module containing models, returns a list of the models. + Otherwise returns a list of all installed models. + + By default, auto-created models (i.e., m2m models without an + explicit intermediate table) are not included. However, if you + specify include_auto_created=True, they will be. + + By default, models created to satisfy deferred attribute + queries are *not* included in the list of models. However, if + you specify include_deferred, they will be. + + By default, models that aren't part of installed apps will *not* + be included in the list of models. However, if you specify + only_installed=False, they will be. If you're using a non-default + AppCache, this argument does nothing - all models will be included. + + By default, models that have been swapped out will *not* be + included in the list of models. However, if you specify + include_swapped, they will be. + """ + if not self.loads_installed: + only_installed = False + cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped) + model_list = None + try: + model_list = self._get_models_cache[cache_key] + if self.available_apps is not None and only_installed: + model_list = [ + m for m in model_list + if self.app_configs[m._meta.app_label].name in self.available_apps + ] + return model_list + except KeyError: + pass + 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) + model_list = [] + for app in app_list: + model_list.extend( + model for model in app.models.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)) + ) + self._get_models_cache[cache_key] = model_list + if self.available_apps is not None and only_installed: + model_list = [ + m for m in model_list + if self.app_configs[m._meta.app_label].name in self.available_apps + ] + return model_list + + def get_model(self, app_label, model_name, + seed_cache=True, only_installed=True): + """ + Returns the model matching the given app_label and case-insensitive + model_name. + + Returns None if no model is found. + + Raises UnavailableApp when set_available_apps() in in effect and + doesn't include app_label. + """ + if not self.loads_installed: + only_installed = False + if seed_cache: + self.populate() + if only_installed: + app_config = self.app_configs.get(app_label) + if app_config is not None and 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 + + 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. + model_name = model._meta.model_name + model_dict = app_config.models + if model_name in model_dict: + # 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__)) + # 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 + self._get_models_cache.clear() + + def set_available_apps(self, available): + available = set(available) + installed = set(settings.INSTALLED_APPS) + 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): + self.available_apps = None + + ### DEPRECATED METHODS GO BELOW THIS LINE ### + + def get_app(self, app_label): + """ + Returns the module containing the models for the given app_label. + + Raises UnavailableApp when set_available_apps() in in effect and + doesn't include app_label. + """ + warnings.warn( + "get_app_config(app_label).models_module supersedes get_app(app_label).", + PendingDeprecationWarning, stacklevel=2) + try: + return self.get_app_config(app_label).models_module + except LookupError as exc: + # Change the exception type for backwards compatibility. + raise ImproperlyConfigured(*exc.args) + + def get_apps(self): + """ + Returns a list of all installed modules that contain models. + """ + 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()] + + def _get_app_package(self, app): + return '.'.join(app.__name__.split('.')[:-1]) + + def get_app_package(self, app_label): + warnings.warn( + "get_app_config(label).name supersedes get_app_package(label).", + PendingDeprecationWarning, stacklevel=2) + return self._get_app_package(self.get_app(app_label)) + + def _get_app_path(self, app): + if hasattr(app, '__path__'): # models/__init__.py package + app_path = app.__path__[0] + else: # models.py module + app_path = app.__file__ + return os.path.dirname(upath(app_path)) + + def get_app_path(self, app_label): + warnings.warn( + "get_app_config(label).path supersedes get_app_path(label).", + PendingDeprecationWarning, stacklevel=2) + return self._get_app_path(self.get_app(app_label)) + + def get_app_paths(self): + """ + Returns a list of paths to all installed apps. + + Useful for discovering files at conventional locations inside apps + (static files, templates, etc.) + """ + warnings.warn( + "[a.path for a in get_app_configs()] supersedes get_app_paths().", + PendingDeprecationWarning, stacklevel=2) + + self.populate() + + app_paths = [] + for app in self.get_apps(): + app_paths.append(self._get_app_path(app)) + return app_paths + + def register_models(self, app_label, *models): + """ + Register a set of models as belonging to an app. + """ + warnings.warn( + "register_models(app_label, models) is deprecated.", + PendingDeprecationWarning, stacklevel=2) + for model in models: + self.register_model(app_label, model) + + +class AppCache(BaseAppCache): + """ + A cache that stores installed applications and their models. Used to + provide reverse-relations and for app introspection (e.g. admin). + + Borg version of the BaseAppCache class. + """ + + __shared_state = _initialize() + + def __init__(self): + self.__dict__ = self.__shared_state + + +app_cache = AppCache() diff --git a/django/core/checks/compatibility/django_1_6_0.py b/django/core/checks/compatibility/django_1_6_0.py index a00196f2c0f9f..fef8bc3a518be 100644 --- a/django/core/checks/compatibility/django_1_6_0.py +++ b/django/core/checks/compatibility/django_1_6_0.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from django.core.apps import app_cache from django.db import models @@ -31,7 +32,7 @@ def check_boolean_field_default_value(): warns the user that the default has changed from False to Null. """ fields = [] - for cls in models.get_models(): + for cls in app_cache.get_models(): opts = cls._meta for f in opts.local_fields: if isinstance(f, models.BooleanField) and not f.has_default(): diff --git a/django/core/management/base.py b/django/core/management/base.py index ef967d021fd15..aeb27e1f0bd9f 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -11,7 +11,6 @@ from optparse import make_option, OptionParser import django -from django.core.exceptions import ImproperlyConfigured from django.core.management.color import color_style, no_style from django.utils.encoding import force_str from django.utils.six import StringIO @@ -342,16 +341,20 @@ class AppCommand(BaseCommand): args = '' def handle(self, *app_labels, **options): - from django.db import models + from django.core.apps import app_cache if not app_labels: raise CommandError('Enter at least one appname.') try: - app_list = [models.get_app(app_label) for app_label in app_labels] - except (ImproperlyConfigured, ImportError) as e: + app_configs = [app_cache.get_app_config(app_label) for app_label in app_labels] + except (LookupError, ImportError) as e: raise CommandError("%s. Are you sure your INSTALLED_APPS setting is correct?" % e) output = [] - for app in app_list: - app_output = self.handle_app(app, **options) + for app_config in app_configs: + if app_config.models_module is None: + raise CommandError( + "AppCommand cannot handle app %r because it doesn't have " + "a models module." % app_config.label) + app_output = self.handle_app(app_config.models_module, **options) if app_output: output.append(app_output) return '\n'.join(output) diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index 85817e9194896..fa657bcd72366 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -3,7 +3,6 @@ from collections import OrderedDict from optparse import make_option -from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand, CommandError from django.core import serializers from django.db import router, DEFAULT_DB_ALIAS @@ -38,7 +37,7 @@ class Command(BaseCommand): args = '[appname appname.ModelName ...]' def handle(self, *app_labels, **options): - from django.db.models import get_app, get_apps, get_model + from django.core.apps import app_cache format = options.get('format') indent = options.get('indent') @@ -64,21 +63,24 @@ def handle(self, *app_labels, **options): for exclude in excludes: if '.' in exclude: app_label, model_name = exclude.split('.', 1) - model_obj = get_model(app_label, model_name) + model_obj = app_cache.get_model(app_label, model_name) if not model_obj: raise CommandError('Unknown model in excludes: %s' % exclude) excluded_models.add(model_obj) else: try: - app_obj = get_app(exclude) - excluded_apps.add(app_obj) - except ImproperlyConfigured: + app_obj = app_cache.get_app_config(exclude).models_module + if app_obj is not None: + excluded_apps.add(app_obj) + except LookupError: raise CommandError('Unknown app in excludes: %s' % exclude) if len(app_labels) == 0: if primary_keys: raise CommandError("You can only use --pks option with one model") - app_list = OrderedDict((app, None) for app in get_apps() if app not in excluded_apps) + app_list = OrderedDict((app_config.models_module, None) + for app_config in app_cache.get_app_configs(only_with_models_module=True) + if app_config.models_module not in excluded_apps) else: if len(app_labels) > 1 and primary_keys: raise CommandError("You can only use --pks option with one model") @@ -87,12 +89,12 @@ def handle(self, *app_labels, **options): try: app_label, model_label = label.split('.') try: - app = get_app(app_label) - except ImproperlyConfigured: + app = app_cache.get_app_config(app_label).models_module + except LookupError: raise CommandError("Unknown application: %s" % app_label) - if app in excluded_apps: + if app is None or app in excluded_apps: continue - model = get_model(app_label, model_label) + model = app_cache.get_model(app_label, model_label) if model is None: raise CommandError("Unknown model: %s.%s" % (app_label, model_label)) @@ -107,10 +109,10 @@ def handle(self, *app_labels, **options): # This is just an app - no model qualifier app_label = label try: - app = get_app(app_label) - except ImproperlyConfigured: + app = app_cache.get_app_config(app_label).models_module + except LookupError: raise CommandError("Unknown application: %s" % app_label) - if app in excluded_apps: + if app is None or app in excluded_apps: continue app_list[app] = None @@ -160,13 +162,13 @@ 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.db.models import get_model, get_models + from django.core.apps import app_cache # Process the list of models, and get the list of dependencies model_dependencies = [] models = set() for app, model_list in app_list: if model_list is None: - model_list = get_models(app) + model_list = app_cache.get_models(app) for model in model_list: models.add(model) @@ -174,7 +176,7 @@ def sort_dependencies(app_list): if hasattr(model, 'natural_key'): deps = getattr(model.natural_key, 'dependencies', []) if deps: - deps = [get_model(*d.split('.')) for d in deps] + deps = [app_cache.get_model(*d.split('.')) for d in deps] else: deps = [] diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index 130338a55ac90..562147e403812 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -3,7 +3,8 @@ from optparse import make_option from django.conf import settings -from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS +from django.core.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 from django.core.management.color import no_style @@ -93,6 +94,6 @@ def emit_post_migrate(verbosity, interactive, database): # Emit the post migrate signal. This allows individual applications to # respond as if the database had been migrated from scratch. all_models = [] - for app in models.get_apps(): - all_models.extend(router.get_migratable_models(app, database, include_auto_created=True)) + for app_config in app_cache.get_app_configs(only_with_models_module=True): + all_models.extend(router.get_migratable_models(app_config.models_module, database, include_auto_created=True)) emit_post_migrate_signal(set(all_models), verbosity, interactive, database) diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 59c1343271b9e..ee88232d9f010 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -8,12 +8,12 @@ from optparse import make_option 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 from django.db import (connections, router, transaction, DEFAULT_DB_ALIAS, IntegrityError, DatabaseError) -from django.db.models import get_app_paths from django.utils import lru_cache from django.utils.encoding import force_text from django.utils.functional import cached_property @@ -230,8 +230,8 @@ def fixture_dirs(self): current directory. """ dirs = [] - for path in get_app_paths(): - d = os.path.join(path, 'fixtures') + for app_config in app_cache.get_app_configs(): + d = os.path.join(app_config.path, 'fixtures') if os.path.isdir(d): dirs.append(d) dirs.extend(list(settings.FIXTURE_DIRS)) diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index 239a0a416c5b8..3bd7ad42beb79 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -3,15 +3,14 @@ import operator from optparse import make_option +from django.core.apps import app_cache from django.core.management.base import BaseCommand, CommandError -from django.core.exceptions import ImproperlyConfigured from django.db import connections, DEFAULT_DB_ALIAS, migrations from django.db.migrations.loader import MigrationLoader from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.questioner import MigrationQuestioner, InteractiveMigrationQuestioner from django.db.migrations.state import ProjectState from django.db.migrations.writer import MigrationWriter -from django.db.models.loading import cache from django.utils.six.moves import reduce @@ -38,8 +37,8 @@ def handle(self, *app_labels, **options): bad_app_labels = set() for app_label in app_labels: try: - cache.get_app(app_label) - except ImproperlyConfigured: + app_cache.get_app_config(app_label) + except LookupError: bad_app_labels.add(app_label) if bad_app_labels: for app_label in bad_app_labels: @@ -73,7 +72,7 @@ def handle(self, *app_labels, **options): # Detect changes autodetector = MigrationAutodetector( loader.graph.project_state(), - ProjectState.from_app_cache(cache), + ProjectState.from_app_cache(app_cache), InteractiveMigrationQuestioner(specified_apps=app_labels), ) changes = autodetector.changes(graph=loader.graph, trim_to_apps=app_labels or None) diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index 093c8a79d0950..b607e8ee4383b 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -7,16 +7,16 @@ 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 from django.core.management.color import no_style from django.core.management.sql import custom_sql_for_model, emit_post_migrate_signal, emit_pre_migrate_signal -from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS +from django.db import connections, router, transaction, DEFAULT_DB_ALIAS from django.db.migrations.executor import MigrationExecutor from django.db.migrations.loader import MigrationLoader, AmbiguityError from django.db.migrations.state import ProjectState from django.db.migrations.autodetector import MigrationAutodetector -from django.db.models.loading import cache from django.utils.module_loading import module_has_submodule @@ -136,7 +136,7 @@ def handle(self, *args, **options): # If there's changes that aren't in migrations yet, tell them how to fix it. autodetector = MigrationAutodetector( executor.loader.graph.project_state(), - ProjectState.from_app_cache(cache), + ProjectState.from_app_cache(app_cache), ) changes = autodetector.changes(graph=executor.loader.graph) if changes: @@ -180,9 +180,10 @@ def sync_apps(self, connection, apps): # Build the manifest of apps and models that are to be synchronized all_models = [ - (app.__name__.split('.')[-2], - router.get_migratable_models(app, connection.alias, include_auto_created=True)) - for app in models.get_apps() if app.__name__.split('.')[-2] in apps + (app_config.label, + router.get_migratable_models(app_config.models_module, connection.alias, include_auto_created=True)) + for app_config in app_cache.get_app_configs(only_with_models_module=True) + if app_config.label in apps ] def model_installed(model): diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index 00a6602c0bb7c..5c189ac980747 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -66,8 +66,8 @@ def run_shell(self, shell=None): def handle_noargs(self, **options): # XXX: (Temporary) workaround for ticket #1796: force early loading of all # models from installed apps. - from django.db.models.loading import get_models - get_models() + from django.core.apps import app_cache + app_cache.get_models() use_plain = options.get('plain', False) no_startup = options.get('no_startup', False) diff --git a/django/core/management/commands/sqlsequencereset.py b/django/core/management/commands/sqlsequencereset.py index 8f4ea823c7c10..cc35030a91c4e 100644 --- a/django/core/management/commands/sqlsequencereset.py +++ b/django/core/management/commands/sqlsequencereset.py @@ -2,8 +2,9 @@ from optparse import make_option +from django.core.apps import app_cache from django.core.management.base import AppCommand -from django.db import connections, models, DEFAULT_DB_ALIAS +from django.db import connections, DEFAULT_DB_ALIAS class Command(AppCommand): @@ -20,4 +21,4 @@ class Command(AppCommand): def handle_app(self, app, **options): connection = connections[options.get('database')] - return '\n'.join(connection.ops.sequence_reset_sql(self.style, models.get_models(app, include_auto_created=True))) + return '\n'.join(connection.ops.sequence_reset_sql(self.style, app_cache.get_models(app, include_auto_created=True))) diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 6f60d73ae925f..0b6e38124ea50 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -6,6 +6,7 @@ import warnings 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 @@ -24,7 +25,7 @@ def sql_create(app, style, connection): # We trim models from the current app so that the sqlreset command does not # generate invalid SQL (leaving models out of known_models is harmless, so # we can be conservative). - app_models = models.get_models(app, include_auto_created=True) + app_models = app_cache.get_models(app, include_auto_created=True) final_output = [] tables = connection.introspection.table_names() known_models = set(model for model in connection.introspection.installed_models(tables) if model not in app_models) @@ -168,7 +169,7 @@ def _split_statements(content): def custom_sql_for_model(model, style, connection): opts = model._meta app_dirs = [] - app_dir = models.get_app_path(model._meta.app_label) + app_dir = app_cache.get_app_config(model._meta.app_label).path app_dirs.append(os.path.normpath(os.path.join(app_dir, 'sql'))) # Deprecated location -- remove in Django 1.9 @@ -206,23 +207,27 @@ def custom_sql_for_model(model, style, connection): def emit_pre_migrate_signal(create_models, verbosity, interactive, db): # Emit the pre_migrate signal for every application. - for app in models.get_apps(): - app_name = app.__name__.split('.')[-2] + for app_config in app_cache.get_app_configs(only_with_models_module=True): if verbosity >= 2: - print("Running pre-migrate handlers for application %s" % app_name) - models.signals.pre_migrate.send(sender=app, app=app, - create_models=create_models, - verbosity=verbosity, - interactive=interactive, - db=db) + print("Running pre-migrate handlers for application %s" % app_config.label) + models.signals.pre_migrate.send( + sender=app_config.models_module, + app=app_config.models_module, + create_models=create_models, + verbosity=verbosity, + interactive=interactive, + db=db) def emit_post_migrate_signal(created_models, verbosity, interactive, db): # Emit the post_migrate signal for every application. - for app in models.get_apps(): - app_name = app.__name__.split('.')[-2] + for app_config in app_cache.get_app_configs(only_with_models_module=True): if verbosity >= 2: - print("Running post-migrate handlers for application %s" % app_name) - models.signals.post_migrate.send(sender=app, app=app, - created_models=created_models, verbosity=verbosity, - interactive=interactive, db=db) + print("Running post-migrate handlers for application %s" % app_config.label) + models.signals.post_migrate.send( + sender=app_config.models_module, + app=app_config.models_module, + created_models=created_models, + verbosity=verbosity, + interactive=interactive, + db=db) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 6bdcf853d43db..1cedcb9925a04 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -26,16 +26,13 @@ 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.db import connection, models - from django.db.models.loading import get_app_errors from django.db.models.deletion import SET_NULL, SET_DEFAULT e = ModelErrorCollection(outfile) - for (app_name, error) in get_app_errors().items(): - e.add(app_name, error) - - for cls in models.get_models(app, include_swapped=True): + for cls in app_cache.get_models(app, include_swapped=True): opts = cls._meta # Check swappable attribute. @@ -45,7 +42,7 @@ def get_validation_errors(outfile, app=None): except ValueError: e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable) continue - if not models.get_model(app_label, model_name): + if not app_cache.get_model(app_label, model_name): e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped) # No need to perform any other validation checks on a swapped model. continue @@ -155,7 +152,7 @@ def get_validation_errors(outfile, app=None): # Check to see if the related field will clash with any existing # fields, m2m fields, m2m related objects or related objects if f.rel: - if f.rel.to not in models.get_models(): + if f.rel.to not in app_cache.get_models(): # If the related model is swapped, provide a hint; # otherwise, the model just hasn't been installed. if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped: @@ -210,7 +207,7 @@ def get_validation_errors(outfile, app=None): # Check to see if the related m2m field will clash with any # existing fields, m2m fields, m2m related objects or related # objects - if f.rel.to not in models.get_models(): + if f.rel.to not in app_cache.get_models(): # If the related model is swapped, provide a hint; # otherwise, the model just hasn't been installed. if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped: @@ -268,7 +265,7 @@ def get_validation_errors(outfile, app=None): ) else: seen_to = True - if f.rel.through not in models.get_models(include_auto_created=True): + if f.rel.through not in app_cache.get_models(include_auto_created=True): e.add(opts, "'%s' specifies an m2m relation through model " "%s, which has not been installed." % (f.name, f.rel.through)) signature = (f.rel.to, cls, f.rel.through) diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index b7d3e28a0d338..db4b79a0201b1 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -3,6 +3,7 @@ """ import warnings +from django.core.apps import app_cache from django.db import models from django.utils import six @@ -136,10 +137,9 @@ def __init__(self, stream_or_string, **options): self.stream = six.StringIO(stream_or_string) else: self.stream = stream_or_string - # hack to make sure that the models have all been loaded before - # deserialization starts (otherwise subclass calls to get_model() - # and friends might fail...) - models.get_apps() + # Make sure the app cache is loaded before deserialization starts + # (otherwise subclass calls to get_model() and friends might fail...) + app_cache.populate() def __iter__(self): return self diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 4ac7cc4cf1009..13d6f01a4e54d 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals 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 @@ -87,7 +88,8 @@ def Deserializer(object_list, **options): db = options.pop('using', DEFAULT_DB_ALIAS) ignore = options.pop('ignorenonexistent', False) - models.get_apps() + app_cache.populate() + for d in object_list: # Look up the model and starting build a dict of data for it. Model = _get_model(d["model"]) @@ -153,7 +155,7 @@ def _get_model(model_identifier): Helper to look up a model from an "app_label.model_name" string. """ try: - Model = models.get_model(*model_identifier.split(".")) + Model = app_cache.get_model(*model_identifier.split(".")) except TypeError: Model = None if Model is None: diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 6870ac7d445ac..90ad2cf398f9f 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals 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 @@ -276,7 +277,7 @@ def _get_model_from_node(self, node, attr): "<%s> node is missing the required '%s' attribute" % (node.nodeName, attr)) try: - Model = models.get_model(*model_identifier.split(".")) + Model = app_cache.get_model(*model_identifier.split(".")) except TypeError: Model = None if Model is None: diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 3faaeffb219e9..adf3b236ac883 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1268,10 +1268,11 @@ def django_table_names(self, only_existing=False): If only_existing is True, the resulting list will only include the tables that actually exist in the database. """ - from django.db import models, router + from django.core.apps import app_cache + from django.db import router tables = set() - for app in models.get_apps(): - for model in router.get_migratable_models(app, self.connection.alias): + for app_config in app_cache.get_app_configs(only_with_models_module=True): + for model in router.get_migratable_models(app_config.models_module, self.connection.alias): if not model._meta.managed: continue tables.add(model._meta.db_table) @@ -1288,10 +1289,11 @@ def django_table_names(self, only_existing=False): def installed_models(self, tables): "Returns a set of all models represented by the provided list of table names." - from django.db import models, router + from django.core.apps import app_cache + from django.db import router all_models = [] - for app in models.get_apps(): - all_models.extend(router.get_migratable_models(app, self.connection.alias)) + for app_config in app_cache.get_app_configs(only_with_models_module=True): + all_models.extend(router.get_migratable_models(app_config.models_module, self.connection.alias)) tables = list(map(self.table_name_converter, tables)) return set([ m for m in all_models @@ -1300,13 +1302,13 @@ def installed_models(self, tables): 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.db import models, router - apps = models.get_apps() sequence_list = [] - for app in apps: - for model in router.get_migratable_models(app, self.connection.alias): + for app_config in app_cache.get_app_configs(only_with_models_module=True): + for model in router.get_migratable_models(app_config.models_module, self.connection.alias): if not model._meta.managed: continue if model._meta.swapped: diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index f04095507f545..8314533bbaa19 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -1,6 +1,6 @@ +from django.core.apps.cache import BaseAppCache from django.db.backends.schema import BaseDatabaseSchemaEditor from django.db.models.fields.related import ManyToManyField -from django.db.models.loading import BaseAppCache class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py index f2510b83681d2..4c12e05add478 100644 --- a/django/db/migrations/loader.py +++ b/django/db/migrations/loader.py @@ -1,7 +1,8 @@ +from importlib import import_module import os import sys -from importlib import import_module -from django.db.models.loading import cache + +from django.core.apps import app_cache from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.graph import MigrationGraph from django.utils import six @@ -45,7 +46,7 @@ def migrations_module(cls, app_label): if app_label in settings.MIGRATION_MODULES: return settings.MIGRATION_MODULES[app_label] else: - return '%s.migrations' % cache.get_app_package(app_label) + return '%s.migrations' % app_cache.get_app_config(app_label).name def load_disk(self): """ @@ -54,10 +55,9 @@ def load_disk(self): self.disk_migrations = {} self.unmigrated_apps = set() self.migrated_apps = set() - for app in cache.get_apps(): + for app_config in app_cache.get_app_configs(only_with_models_module=True): # Get the migrations module directory - app_label = app.__name__.split(".")[-2] - module_name = self.migrations_module(app_label) + module_name = self.migrations_module(app_config.label) was_loaded = module_name in sys.modules try: module = import_module(module_name) @@ -65,7 +65,7 @@ def load_disk(self): # I hate doing this, but I don't want to squash other import errors. # Might be better to try a directory check directly. if "No module named" in str(e) and "migrations" in str(e): - self.unmigrated_apps.add(app_label) + self.unmigrated_apps.add(app_config.label) continue raise else: @@ -78,7 +78,7 @@ def load_disk(self): # Force a reload if it's already loaded (tests need this) if was_loaded: six.moves.reload_module(module) - self.migrated_apps.add(app_label) + self.migrated_apps.add(app_config.label) directory = os.path.dirname(module.__file__) # Scan for .py[c|o] files migration_names = set() @@ -99,14 +99,14 @@ def load_disk(self): break raise if not hasattr(migration_module, "Migration"): - raise BadMigrationError("Migration %s in app %s has no Migration class" % (migration_name, app_label)) + raise BadMigrationError("Migration %s in app %s has no Migration class" % (migration_name, app_config.label)) # Ignore South-style migrations if hasattr(migration_module.Migration, "forwards"): south_style_migrations = True break - self.disk_migrations[app_label, migration_name] = migration_module.Migration(migration_name, app_label) + self.disk_migrations[app_config.label, migration_name] = migration_module.Migration(migration_name, app_config.label) if south_style_migrations: - self.unmigrated_apps.add(app_label) + self.unmigrated_apps.add(app_config.label) def get_migration(self, app_label, name_prefix): "Gets the migration exactly named, or raises KeyError" diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py index 7e798d310525f..8a11559993dce 100644 --- a/django/db/migrations/questioner.py +++ b/django/db/migrations/questioner.py @@ -2,10 +2,9 @@ import os import sys -from django.db.models.loading import cache +from django.core.apps import app_cache from django.utils import datetime_safe from django.utils.six.moves import input -from django.core.exceptions import ImproperlyConfigured class MigrationQuestioner(object): @@ -29,10 +28,10 @@ def ask_initial(self, app_label): # Apps from the new app template will have these; the python # file check will ensure we skip South ones. try: - models_module = cache.get_app(app_label) - except ImproperlyConfigured: # It's a fake app + app_config = app_cache.get_app_config(app_label) + except LookupError: # It's a fake app. return self.defaults.get("ask_initial", False) - migrations_import_path = "%s.migrations" % models_module.__package__ + migrations_import_path = "%s.migrations" % app_config.name try: migrations_module = importlib.import_module(migrations_import_path) except ImportError: diff --git a/django/db/migrations/recorder.py b/django/db/migrations/recorder.py index be804e52aa58a..1c714ca272128 100644 --- a/django/db/migrations/recorder.py +++ b/django/db/migrations/recorder.py @@ -1,5 +1,5 @@ +from django.core.apps.cache import BaseAppCache from django.db import models -from django.db.models.loading import BaseAppCache from django.utils.timezone import now diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 769b0005f81e3..2102773b71893 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -1,5 +1,5 @@ +from django.core.apps.cache import BaseAppCache from django.db import models -from django.db.models.loading import BaseAppCache from django.db.models.options import DEFAULT_NAMES, normalize_unique_together from django.utils import six from django.utils.module_loading import import_by_path diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index 22b0977ba40d4..04e00501f9bb6 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -1,14 +1,16 @@ from __future__ import unicode_literals + import datetime -import types -import os from importlib import import_module -from django.utils import six +import os +import types + +from django.core.apps import app_cache from django.db import models -from django.db.models.loading import cache from django.db.migrations.loader import MigrationLoader from django.utils.encoding import force_text from django.utils.functional import Promise +from django.utils import six class MigrationWriter(object): @@ -67,14 +69,12 @@ def path(self): migrations_module = import_module(migrations_package_name) basedir = os.path.dirname(migrations_module.__file__) except ImportError: - app = cache.get_app(self.migration.app_label) - app_path = cache._get_app_path(app) - app_package_name = cache._get_app_package(app) + app_config = app_cache.get_app_config(self.migration.app_label) migrations_package_basename = migrations_package_name.split(".")[-1] # Alright, see if it's a direct submodule of the app - if '%s.%s' % (app_package_name, migrations_package_basename) == migrations_package_name: - basedir = os.path.join(app_path, migrations_package_basename) + if '%s.%s' % (app_config.name, migrations_package_basename) == migrations_package_name: + basedir = os.path.join(app_config.path, migrations_package_basename) else: raise ImportError("Cannot open migrations module %s for app %s" % (migrations_package_name, self.migration.app_label)) return os.path.join(basedir, self.filename) diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 12c31f89fd266..1459cb318d01c 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -1,9 +1,8 @@ from functools import wraps +import sys +import warnings from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured # NOQA -from django.db.models.loading import ( # NOQA - get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, - register_models, UnavailableApp) from django.db.models.query import Q, QuerySet, Prefetch # NOQA from django.db.models.expressions import F # NOQA from django.db.models.manager import Manager # NOQA @@ -38,3 +37,26 @@ def inner(*args, **kwargs): bits = func(*args, **kwargs) return reverse(bits[0], None, *bits[1:3]) return inner + + +# Deprecated aliases for functions were exposed in this module. + +this_module = sys.modules['django.db.models'] + +def make_alias(function_name): + # Close function_name. + def alias(*args, **kwargs): + warnings.warn( + "django.db.models.%s is deprecated." % function_name, + PendingDeprecationWarning, stacklevel=2) + # This raises a second warning. + from . import loading + return getattr(loading, function_name)(*args, **kwargs) + alias.__name__ = function_name + return alias + +for function_name in ('get_apps', 'get_app_path', 'get_app_paths', 'get_app', + 'get_models', 'get_model', 'register_models'): + setattr(this_module, function_name, make_alias(function_name)) + +del this_module, make_alias, function_name diff --git a/django/db/models/base.py b/django/db/models/base.py index dd4850c3d4929..56973d4e382c0 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -5,6 +5,8 @@ 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 import django.db.models.manager # NOQA: Imported to register signal handler. from django.conf import settings from django.core.exceptions import (ObjectDoesNotExist, @@ -19,7 +21,6 @@ from django.db.models.deletion import Collector from django.db.models.options import Options from django.db.models import signals -from django.db.models.loading import get_model, MODELS_MODULE_NAME from django.utils.translation import ugettext_lazy as _ from django.utils.functional import curry from django.utils.encoding import force_str, force_text @@ -273,7 +274,7 @@ def __new__(cls, name, bases, attrs): new_class._prepare() - new_class._meta.app_cache.register_models(new_class._meta.app_label, new_class) + new_class._meta.app_cache.register_model(new_class._meta.app_label, new_class) # Because of the way imports happen (recursively), we may or may not be # 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 @@ -1066,7 +1067,7 @@ def model_unpickle(model_id, attrs, factory): Used to unpickle Model subclasses with deferred fields. """ if isinstance(model_id, tuple): - model = get_model(*model_id) + model = app_cache.get_model(*model_id) else: # Backwards compat - the model was cached directly in earlier versions. model = model_id diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 33adaedc7edf8..b24323d1cfd45 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -9,8 +9,8 @@ from base64 import b64decode, b64encode from itertools import tee +from django.core.apps import app_cache from django.db import connection -from django.db.models.loading import get_model from django.db.models.query_utils import QueryWrapper from django.conf import settings from django import forms @@ -51,7 +51,7 @@ class NOT_PROVIDED: def _load_field(app_label, model_name, field_name): - return get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0] + return app_cache.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0] class FieldDoesNotExist(Exception): diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 21440216e0a36..5745dbed4d4f5 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -1,397 +1,35 @@ -"Utilities for loading models and the modules that contain them." +import warnings -from collections import OrderedDict -import copy -import imp -from importlib import import_module -import os -import sys +from django.core.apps import app_cache -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured -from django.utils.module_loading import module_has_submodule -from django.utils._os import upath -from django.utils import six +warnings.warn( + "The utilities in django.db.models.loading are deprecated " + "in favor of the new application loading system.", + PendingDeprecationWarning, stacklevel=2) __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', 'load_app', 'app_cache_ready') -MODELS_MODULE_NAME = 'models' - - -class ModelDict(OrderedDict): - """ - We need to special-case the deepcopy for this, as the keys are modules, - which can't be deep copied. - """ - def __deepcopy__(self, memo): - return self.__class__([(key, copy.deepcopy(value, memo)) - for key, value in self.items()]) - - -class UnavailableApp(Exception): - pass - - -def _initialize(): - """ - Returns a dictionary to be used as the initial value of the - [shared] state of the app cache. - """ - return dict( - # Keys of app_store are the model modules for each application. - app_store=ModelDict(), - - # Mapping of installed app_labels to model modules for that app. - app_labels={}, - - # Mapping of app_labels to a dictionary of model names to model code. - # May contain apps that are not installed. - app_models=ModelDict(), - - # Mapping of app_labels to errors raised when trying to import the app. - app_errors={}, - - # Pending lookups for lazy relations - pending_lookups={}, - - # List of app_labels that allows restricting the set of apps. - # Used by TransactionTestCase.available_apps for performance reasons. - available_apps=None, - - # -- Everything below here is only used when populating the cache -- - loads_installed=True, - loaded=False, - handled=set(), - postponed=[], - nesting_level=0, - _get_models_cache={}, - ) - - -class BaseAppCache(object): - """ - A cache that stores installed applications and their models. Used to - provide reverse-relations and for app introspection (e.g. admin). - - This provides the base (non-Borg) AppCache class - the AppCache - subclass adds borg-like behaviour for the few cases where it's needed. - """ - - def __init__(self): - self.__dict__ = _initialize() - # This stops _populate loading from INSTALLED_APPS and ignores the - # only_installed arguments to get_model[s] - self.loads_installed = False - - 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.loads_installed: - 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. - imp.acquire_lock() - try: - if self.loaded: - return - for app_name in settings.INSTALLED_APPS: - if app_name in self.handled: - continue - self.load_app(app_name, True) - if not self.nesting_level: - for app_name in self.postponed: - self.load_app(app_name) - self.loaded = True - finally: - imp.release_lock() - - def _label_for(self, app_mod): - """ - Return app_label for given models module. - - """ - return app_mod.__name__.split('.')[-2] - - def load_app(self, app_name, can_postpone=False): - """ - 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 = import_module('%s.%s' % (app_name, MODELS_MODULE_NAME)) - except ImportError: - self.nesting_level -= 1 - # If the app doesn't have a models module, we can just ignore the - # ImportError and return no models for it. - if not module_has_submodule(app_module, MODELS_MODULE_NAME): - return 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 None - else: - raise - - self.nesting_level -= 1 - if models not in self.app_store: - self.app_store[models] = len(self.app_store) - self.app_labels[self._label_for(models)] = models - return models - - def app_cache_ready(self): - """ - Returns true if the model cache is fully populated. - - Useful for code that wants to cache the results of get_models() for - themselves once it is safe to do so. - """ - return self.loaded - - def get_apps(self): - """ - Returns a list of all installed modules that contain models. - """ - self._populate() - - apps = self.app_store.items() - if self.available_apps is not None: - apps = [elt for elt in apps - if self._label_for(elt[0]) in self.available_apps] - - # Ensure the returned list is always in the same order (with new apps - # added at the end). This avoids unstable ordering on the admin app - # list page, for example. - apps = sorted(apps, key=lambda elt: elt[1]) - - return [elt[0] for elt in apps] - - def _get_app_package(self, app): - return '.'.join(app.__name__.split('.')[:-1]) - - def get_app_package(self, app_label): - return self._get_app_package(self.get_app(app_label)) - - def _get_app_path(self, app): - if hasattr(app, '__path__'): # models/__init__.py package - app_path = app.__path__[0] - else: # models.py module - app_path = app.__file__ - return os.path.dirname(upath(app_path)) - - def get_app_path(self, app_label): - return self._get_app_path(self.get_app(app_label)) - - def get_app_paths(self): - """ - Returns a list of paths to all installed apps. - - Useful for discovering files at conventional locations inside apps - (static files, templates, etc.) - """ - self._populate() - - app_paths = [] - for app in self.get_apps(): - app_paths.append(self._get_app_path(app)) - return app_paths - - def get_app(self, app_label, emptyOK=False): - """ - Returns the module containing the models for the given app_label. - - Returns None if the app has no models in it and emptyOK is True. - - Raises UnavailableApp when set_available_apps() in in effect and - doesn't include app_label. - """ - self._populate() - imp.acquire_lock() - try: - for app_name in settings.INSTALLED_APPS: - if app_label == app_name.split('.')[-1]: - mod = self.load_app(app_name, False) - if mod is None and not emptyOK: - raise ImproperlyConfigured("App with label %s is missing a models.py module." % app_label) - if self.available_apps is not None and app_label not in self.available_apps: - raise UnavailableApp("App with label %s isn't available." % app_label) - return mod - raise ImproperlyConfigured("App with label %s could not be found" % app_label) - finally: - imp.release_lock() - - def get_app_errors(self): - "Returns the map of known problems with the INSTALLED_APPS." - self._populate() - return self.app_errors - - def get_models(self, app_mod=None, - include_auto_created=False, include_deferred=False, - only_installed=True, include_swapped=False): - """ - Given a module containing models, returns a list of the models. - Otherwise returns a list of all installed models. - - By default, auto-created models (i.e., m2m models without an - explicit intermediate table) are not included. However, if you - specify include_auto_created=True, they will be. - - By default, models created to satisfy deferred attribute - queries are *not* included in the list of models. However, if - you specify include_deferred, they will be. - - By default, models that aren't part of installed apps will *not* - be included in the list of models. However, if you specify - only_installed=False, they will be. If you're using a non-default - AppCache, this argument does nothing - all models will be included. - - By default, models that have been swapped out will *not* be - included in the list of models. However, if you specify - include_swapped, they will be. - """ - if not self.loads_installed: - only_installed = False - cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped) - model_list = None - try: - model_list = self._get_models_cache[cache_key] - if self.available_apps is not None and only_installed: - model_list = [m for m in model_list if m._meta.app_label in self.available_apps] - - return model_list - except KeyError: - pass - self._populate() - if app_mod: - if app_mod in self.app_store: - app_list = [self.app_models.get(self._label_for(app_mod), ModelDict())] - else: - app_list = [] - else: - if only_installed: - app_list = [self.app_models.get(app_label, ModelDict()) - for app_label in six.iterkeys(self.app_labels)] - else: - app_list = six.itervalues(self.app_models) - model_list = [] - for app in app_list: - model_list.extend( - model for model in app.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)) - ) - 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 m._meta.app_label in self.available_apps] - return model_list - - def get_model(self, app_label, model_name, - seed_cache=True, only_installed=True): - """ - Returns the model matching the given app_label and case-insensitive - model_name. - - Returns None if no model is found. - - Raises UnavailableApp when set_available_apps() in in effect and - doesn't include app_label. - """ - if not self.loads_installed: - only_installed = False - if seed_cache: - self._populate() - if only_installed and app_label not in self.app_labels: - return None - if (self.available_apps is not None and only_installed - and app_label not in self.available_apps): - raise UnavailableApp("App with label %s isn't available." % app_label) - try: - return self.app_models[app_label][model_name.lower()] - except KeyError: - return None - - def register_models(self, app_label, *models): - """ - Register a set of models as belonging to an app. - """ - for model in models: - # Store as 'name: model' pair in a dictionary - # in the app_models dictionary - model_name = model._meta.model_name - model_dict = self.app_models.setdefault(app_label, ModelDict()) - if model_name in model_dict: - # 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__)) - # 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]: - continue - model_dict[model_name] = model - self._get_models_cache.clear() - - def set_available_apps(self, available): - if not set(available).issubset(set(settings.INSTALLED_APPS)): - extra = set(available) - set(settings.INSTALLED_APPS) - raise ValueError("Available apps isn't a subset of installed " - "apps, extra apps: " + ", ".join(extra)) - self.available_apps = set(app.rsplit('.', 1)[-1] for app in available) - - def unset_available_apps(self): - self.available_apps = None - - -class AppCache(BaseAppCache): - """ - A cache that stores installed applications and their models. Used to - provide reverse-relations and for app introspection (e.g. admin). - - Borg version of the BaseAppCache class. - """ - - __shared_state = _initialize() - - def __init__(self): - self.__dict__ = self.__shared_state - - -cache = AppCache() - - # These methods were always module level, so are kept that way for backwards # compatibility. -get_apps = cache.get_apps -get_app_package = cache.get_app_package -get_app_path = cache.get_app_path -get_app_paths = cache.get_app_paths -get_app = cache.get_app -get_app_errors = cache.get_app_errors -get_models = cache.get_models -get_model = cache.get_model -register_models = cache.register_models -load_app = cache.load_app -app_cache_ready = cache.app_cache_ready +get_apps = app_cache.get_apps +get_app_package = app_cache.get_app_package +get_app_path = app_cache.get_app_path +get_app_paths = app_cache.get_app_paths +get_app = app_cache.get_app +get_models = app_cache.get_models +get_model = app_cache.get_model +register_models = app_cache.register_models +load_app = app_cache.load_app +app_cache_ready = app_cache.app_cache_ready + + +# This method doesn't return anything interesting in Django 1.6. Maintain it +# just for backwards compatibility until this module is deprecated. +def get_app_errors(): + try: + return app_cache.app_errors + except AttributeError: + app_cache.populate() + app_cache.app_errors = {} + return app_cache.app_errors diff --git a/django/db/models/options.py b/django/db/models/options.py index b14e61573c54a..386e90b057eea 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -6,10 +6,10 @@ import warnings 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 -from django.db.models.loading import app_cache_ready, cache from django.utils import six from django.utils.functional import cached_property from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible @@ -89,7 +89,7 @@ def __init__(self, meta, app_label=None): self.related_fkey_lookups = [] # A custom AppCache to use, if you're making a separate model set. - self.app_cache = cache + self.app_cache = app_cache def contribute_to_class(self, cls, name): from django.db import connection @@ -432,7 +432,7 @@ def init_name_map(self): if hasattr(f, 'related'): cache[f.name] = cache[f.attname] = ( f.related, None if f.model == self.model else f.model, True, False) - if app_cache_ready(): + if app_cache.app_cache_ready(): self._name_map = cache return cache @@ -558,7 +558,7 @@ def _fill_related_many_to_many_cache(self): and not isinstance(f.rel.to, six.string_types) and self == f.rel.to._meta): cache[f.related] = None - if app_cache_ready(): + if app_cache.app_cache_ready(): self._related_many_to_many_cache = cache return cache diff --git a/django/db/models/signals.py b/django/db/models/signals.py index 6b011c20997ee..a6822309a3987 100644 --- a/django/db/models/signals.py +++ b/django/db/models/signals.py @@ -1,6 +1,6 @@ from collections import defaultdict -from django.db.models.loading import get_model +from django.core.apps import app_cache from django.dispatch import Signal from django.utils import six @@ -41,7 +41,7 @@ def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): "Specified sender must either be a model or a " "model name of the 'app_label.ModelName' form." ) - sender = get_model(app_label, object_name, only_installed=False) + sender = app_cache.get_model(app_label, object_name, only_installed=False) if sender is None: reference = (app_label, object_name) self.unresolved_references[reference].append( diff --git a/django/db/utils.py b/django/db/utils.py index 43abaf9b5a52e..4d53d252bfc13 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -282,6 +282,6 @@ def get_migratable_models(self, app, db, include_auto_created=False): """ Return app models allowed to be synchronized on provided db. """ - from .models import get_models - return [model for model in get_models(app, include_auto_created=include_auto_created) + from django.core.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/test/simple.py b/django/test/simple.py index 73deef917b04d..6129ce1305e8b 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -9,7 +9,7 @@ import unittest as real_unittest import warnings -from django.db.models import get_app, get_apps +from django.core.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 @@ -96,22 +96,13 @@ def __init__(self, *args, **kwargs): doctestOutputChecker = OutputChecker() -def get_tests(app_module): - parts = app_module.__name__.split('.') - prefix, last = parts[:-1], parts[-1] +def get_tests(app_config): try: - test_module = import_module('.'.join(prefix + [TEST_MODULE])) + test_module = import_module('%s.%s' % (app_config.name, TEST_MODULE)) except ImportError: # Couldn't import tests.py. Was it due to a missing file, or # due to an import error in a tests.py that actually exists? - # app_module either points to a models.py file, or models/__init__.py - # Tests are therefore either in same directory, or one level up - if last == 'models': - app_root = import_module('.'.join(prefix)) - else: - app_root = app_module - - if not module_has_submodule(app_root, TEST_MODULE): + if not module_has_submodule(app_config.app_module, TEST_MODULE): test_module = None else: # The module exists, so there must be an import error in the test @@ -126,7 +117,7 @@ def make_doctest(module): runner=DocTestRunner) -def build_suite(app_module): +def build_suite(app_config): """ Create a complete Django test suite for the provided application module. """ @@ -134,30 +125,32 @@ def build_suite(app_module): # Load unit and doctests in the models.py module. If module has # a suite() method, use it. Otherwise build the test suite ourselves. - if hasattr(app_module, 'suite'): - suite.addTest(app_module.suite()) - else: - suite.addTest(unittest.defaultTestLoader.loadTestsFromModule( - app_module)) - try: - suite.addTest(make_doctest(app_module)) - except ValueError: - # No doc tests in models.py - pass + models_module = app_config.models_module + if models_module: + if hasattr(models_module, 'suite'): + suite.addTest(models_module.suite()) + else: + suite.addTest(unittest.defaultTestLoader.loadTestsFromModule( + models_module)) + try: + suite.addTest(make_doctest(models_module)) + except ValueError: + # No doc tests in models.py + pass # Check to see if a separate 'tests' module exists parallel to the # models module - test_module = get_tests(app_module) - if test_module: + tests_module = get_tests(app_config) + if tests_module: # Load unit and doctests in the tests.py module. If module has # a suite() method, use it. Otherwise build the test suite ourselves. - if hasattr(test_module, 'suite'): - suite.addTest(test_module.suite()) + if hasattr(tests_module, 'suite'): + suite.addTest(tests_module.suite()) else: suite.addTest(unittest.defaultTestLoader.loadTestsFromModule( - test_module)) + tests_module)) try: - suite.addTest(make_doctest(test_module)) + suite.addTest(make_doctest(tests_module)) except ValueError: # No doc tests in tests.py pass @@ -167,26 +160,29 @@ def build_suite(app_module): def build_test(label): """ Construct a test case with the specified label. Label should be of the - form model.TestClass or model.TestClass.test_method. Returns an + form app_label.TestClass or app_label.TestClass.test_method. Returns an instantiated test or test suite corresponding to the label provided. - """ parts = label.split('.') if len(parts) < 2 or len(parts) > 3: raise ValueError("Test label '%s' should be of the form app.TestCase " "or app.TestCase.test_method" % label) - # - # First, look for TestCase instances with a name that matches - # - app_module = get_app(parts[0]) - test_module = get_tests(app_module) - TestClass = getattr(app_module, parts[1], None) + app_config = app_cache.get_app_config(parts[0]) + models_module = app_config.models_module + tests_module = get_tests(app_config) + + test_modules = [] + if models_module: + test_modules.append(models_module) + if tests_module: + test_modules.append(tests_module) - # Couldn't find the test class in models.py; look in tests.py - if TestClass is None: - if test_module: - TestClass = getattr(test_module, parts[1], None) + TestClass = None + for module in test_modules: + TestClass = getattr(models_module, parts[1], None) + if TestClass is not None: + break try: if issubclass(TestClass, (unittest.TestCase, real_unittest.TestCase)): @@ -208,7 +204,7 @@ def build_test(label): # If there isn't a TestCase, look for a doctest that matches # tests = [] - for module in app_module, test_module: + for module in test_modules: try: doctests = make_doctest(module) # Now iterate over the suite, looking for doctests whose name @@ -241,11 +237,11 @@ def build_suite(self, test_labels, extra_tests=None, **kwargs): if '.' in label: suite.addTest(build_test(label)) else: - app = get_app(label) - suite.addTest(build_suite(app)) + app_config = app_cache.get_app_config(label) + suite.addTest(build_suite(app_config)) else: - for app in get_apps(): - suite.addTest(build_suite(app)) + for app_config in app_cache.get_app_configs(): + suite.addTest(build_suite(app_config)) if extra_tests: for test in extra_tests: diff --git a/django/test/testcases.py b/django/test/testcases.py index 4dbff55204de1..13f56fa6cc135 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -17,6 +17,7 @@ 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 @@ -25,7 +26,6 @@ from django.core.servers.basehttp import WSGIRequestHandler, WSGIServer from django.core.urlresolvers import clear_url_caches, set_urlconf from django.db import connection, connections, DEFAULT_DB_ALIAS, transaction -from django.db.models.loading import cache from django.forms.fields import CharField from django.http import QueryDict from django.test.client import Client @@ -725,14 +725,14 @@ def _pre_setup(self): """ super(TransactionTestCase, self)._pre_setup() if self.available_apps is not None: - cache.set_available_apps(self.available_apps) + app_cache.set_available_apps(self.available_apps) for db_name in self._databases_names(include_mirrors=False): flush.Command.emit_post_migrate(verbosity=0, interactive=False, database=db_name) try: self._fixture_setup() except Exception: if self.available_apps is not None: - cache.unset_available_apps() + app_cache.unset_available_apps() raise def _databases_names(self, include_mirrors=True): @@ -786,7 +786,7 @@ def _post_teardown(self): for conn in connections.all(): conn.close() finally: - cache.unset_available_apps() + app_cache.unset_available_apps() def _fixture_teardown(self): # Allow TRUNCATE ... CASCADE and don't emit the post_migrate signal diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 2e53901536909..c3b0cda95b982 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -222,6 +222,10 @@ these changes. * ``django.core.cache.get_cache`` will be removed. Add suitable entries to :setting:`CACHES` and use :data:`django.core.cache.caches` instead. +* ``django.db.models.loading`` will be removed. Use the new application + loading APIs instead. Several undocumented methods of the ``AppCache`` class + will also be removed. + 2.0 --- diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 73a91b6e7b009..8f693bfd34336 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -379,14 +379,14 @@ def test_builtin_with_settings(self): args = ['sqlall', '--settings=test_project.settings', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'App with label admin_scripts could not be found') + self.assertOutput(err, "No 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, 'App with label admin_scripts could not be found') + self.assertOutput(err, "No 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 @@ def test_builtin_command(self): args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, 'App with label admin_scripts could not be found') + self.assertOutput(err, "No 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, 'App with label admin_scripts could not be found') + self.assertOutput(err, "No 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, 'App with label admin_scripts could not be found') + self.assertOutput(err, "No 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 @@ def test_builtin_command(self): args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, 'App with label admin_scripts could not be found.') + self.assertOutput(err, "No 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 @@ def test_app_command_invalid_appname(self): "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, "App with label NOT_AN_APP could not be found") + self.assertOutput(err, "No 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, "App with label NOT_AN_APP could not be found") + self.assertOutput(err, "No app with label 'NOT_AN_APP'.") def test_label_command(self): "User LabelCommands can execute when a label is provided" diff --git a/tests/app_cache/models.py b/tests/app_cache/models.py index 1b4d33c2f9928..99f9f57b67d8a 100644 --- a/tests/app_cache/models.py +++ b/tests/app_cache/models.py @@ -1,5 +1,5 @@ +from django.core.apps.cache import BaseAppCache from django.db import models -from django.db.models.loading import BaseAppCache # 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 b72b862de3c60..29d5315de02b7 100644 --- a/tests/app_cache/tests.py +++ b/tests/app_cache/tests.py @@ -1,7 +1,10 @@ from __future__ import absolute_import -from django.test import TestCase -from django.db.models.loading import cache, BaseAppCache + +from django.core.apps import app_cache +from django.core.apps.cache import BaseAppCache from django.db import models +from django.test import TestCase + from .models import TotallyNormal, SoAlternative, new_app_cache @@ -14,8 +17,8 @@ def test_models_py(self): """ Tests that the models in the models.py file were loaded correctly. """ - self.assertEqual(cache.get_model("app_cache", "TotallyNormal"), TotallyNormal) - self.assertEqual(cache.get_model("app_cache", "SoAlternative"), None) + self.assertEqual(app_cache.get_model("app_cache", "TotallyNormal"), TotallyNormal) + self.assertEqual(app_cache.get_model("app_cache", "SoAlternative"), None) self.assertEqual(new_app_cache.get_model("app_cache", "TotallyNormal"), None) self.assertEqual(new_app_cache.get_model("app_cache", "SoAlternative"), SoAlternative) @@ -24,7 +27,7 @@ def test_dynamic_load(self): """ Makes a new model at runtime and ensures it goes into the right place. """ - old_models = cache.get_models(cache.get_app("app_cache")) + old_models = app_cache.get_models(app_cache.get_app_config("app_cache").models_module) # Construct a new model in a new app cache body = {} new_app_cache = BaseAppCache() @@ -39,6 +42,6 @@ def test_dynamic_load(self): # Make sure it appeared in the right place! self.assertEqual( old_models, - cache.get_models(cache.get_app("app_cache")), + app_cache.get_models(app_cache.get_app_config("app_cache").models_module), ) self.assertEqual(new_app_cache.get_model("app_cache", "SouthPonies"), temp_model) diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index 20ec064d69e6a..3cc0ffb36805a 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -1,11 +1,11 @@ from __future__ import unicode_literals -import copy import os import sys from unittest import TestCase -from django.db.models.loading import cache, load_app, get_model, get_models, AppCache +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 @@ -16,53 +16,52 @@ def setUp(self): self.old_path = sys.path[:] self.egg_dir = '%s/eggs' % os.path.dirname(upath(__file__)) - # This test adds dummy applications to the app cache. These - # need to be removed in order to prevent bad interactions - # with the flush operation in other tests. - self.old_app_models = copy.deepcopy(cache.app_models) - self.old_app_store = copy.deepcopy(cache.app_store) + # 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['app_loading'].models.copy() def tearDown(self): + app_cache.app_configs['app_loading'].models = self._old_models + app_cache._get_models_cache = {} + sys.path = self.old_path - cache.app_models = self.old_app_models - cache.app_store = self.old_app_store def test_egg1(self): """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 = load_app('app_with_models') + models = app_cache.load_app('app_with_models') self.assertFalse(models is None) 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 = load_app('app_no_models') + models = app_cache.load_app('app_no_models') self.assertTrue(models is None) 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 = load_app('omelet.app_with_models') + models = app_cache.load_app('omelet.app_with_models') self.assertFalse(models is None) 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 = load_app('omelet.app_no_models') + models = app_cache.load_app('omelet.app_no_models') self.assertTrue(models is None) 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, load_app, 'broken_app') + self.assertRaises(ImportError, app_cache.load_app, 'broken_app') raised = None try: - load_app('broken_app') + app_cache.load_app('broken_app') except ImportError as e: raised = e @@ -83,8 +82,8 @@ def test_missing_app(self): a.loaded = False try: with override_settings(INSTALLED_APPS=('notexists',)): - self.assertRaises(ImportError, get_model, 'notexists', 'nomodel', seed_cache=True) - self.assertRaises(ImportError, get_model, 'notexists', 'nomodel', seed_cache=True) + self.assertRaises(ImportError, app_cache.get_model, 'notexists', 'nomodel', seed_cache=True) + self.assertRaises(ImportError, app_cache.get_model, 'notexists', 'nomodel', seed_cache=True) finally: a.loaded = True @@ -96,26 +95,26 @@ def setUp(self): def test_get_model_only_returns_installed_models(self): self.assertEqual( - get_model("not_installed", "NotInstalledModel"), None) + app_cache.get_model("not_installed", "NotInstalledModel"), None) def test_get_model_with_not_installed(self): self.assertEqual( - get_model( + app_cache.get_model( "not_installed", "NotInstalledModel", only_installed=False), self.not_installed_module.NotInstalledModel) def test_get_models_only_returns_installed_models(self): self.assertFalse( "NotInstalledModel" in - [m.__name__ for m in get_models()]) + [m.__name__ for m in app_cache.get_models()]) def test_get_models_with_app_label_only_returns_installed_models(self): - self.assertEqual(get_models(self.not_installed_module), []) + 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 get_models(only_installed=False)]) + m.__name__ for m in app_cache.get_models(only_installed=False)]) class NotInstalledModelsTest(TestCase): diff --git a/tests/commands_sql/tests.py b/tests/commands_sql/tests.py index 3a0b5527f4343..3088ea4d48975 100644 --- a/tests/commands_sql/tests.py +++ b/tests/commands_sql/tests.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals +from django.core.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) -from django.db import connections, DEFAULT_DB_ALIAS, models, router +from django.db import connections, DEFAULT_DB_ALIAS, router from django.test import TestCase from django.utils import six @@ -16,7 +17,7 @@ def count_ddl(self, output, cmd): return len([o for o in output if o.startswith(cmd)]) def test_sql_create(self): - app = models.get_app('commands_sql') + app = app_cache.get_app_config('commands_sql').models_module output = sql_create(app, no_style(), connections[DEFAULT_DB_ALIAS]) create_tables = [o for o in output if o.startswith('CREATE TABLE')] self.assertEqual(len(create_tables), 3) @@ -25,7 +26,7 @@ def test_sql_create(self): six.assertRegex(self, sql, r'^create table .commands_sql_book.*') def test_sql_delete(self): - app = models.get_app('commands_sql') + app = app_cache.get_app_config('commands_sql').models_module output = sql_delete(app, no_style(), connections[DEFAULT_DB_ALIAS]) drop_tables = [o for o in output if o.startswith('DROP TABLE')] self.assertEqual(len(drop_tables), 3) @@ -34,19 +35,19 @@ def test_sql_delete(self): six.assertRegex(self, sql, r'^drop table .commands_sql_comment.*') def test_sql_indexes(self): - app = models.get_app('commands_sql') + app = app_cache.get_app_config('commands_sql').models_module output = sql_indexes(app, no_style(), connections[DEFAULT_DB_ALIAS]) # PostgreSQL creates one additional index for CharField self.assertIn(self.count_ddl(output, 'CREATE INDEX'), [3, 4]) def test_sql_destroy_indexes(self): - app = models.get_app('commands_sql') + app = app_cache.get_app_config('commands_sql').models_module output = sql_destroy_indexes(app, no_style(), connections[DEFAULT_DB_ALIAS]) # PostgreSQL creates one additional index for CharField self.assertIn(self.count_ddl(output, 'DROP INDEX'), [3, 4]) def test_sql_all(self): - app = models.get_app('commands_sql') + app = app_cache.get_app_config('commands_sql').models_module output = sql_all(app, no_style(), connections[DEFAULT_DB_ALIAS]) self.assertEqual(self.count_ddl(output, 'CREATE TABLE'), 3) @@ -68,7 +69,7 @@ def tearDown(self): router.routers = self._old_routers def test_router_honored(self): - app = models.get_app('commands_sql') + app = app_cache.get_app_config('commands_sql').models_module for sql_command in (sql_all, sql_create, sql_delete, sql_indexes, sql_destroy_indexes): output = sql_command(app, no_style(), connections[DEFAULT_DB_ALIAS]) self.assertEqual(len(output), 0, diff --git a/tests/contenttypes_tests/tests.py b/tests/contenttypes_tests/tests.py index 9d1e1f77eae1b..ea61e5e89362b 100644 --- a/tests/contenttypes_tests/tests.py +++ b/tests/contenttypes_tests/tests.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals from django.contrib.contenttypes.models import ContentType +from django.core.apps.cache import BaseAppCache from django.db import models -from django.db.models.loading import BaseAppCache from django.test import TestCase from .models import Author, Article diff --git a/tests/defer_regress/tests.py b/tests/defer_regress/tests.py index c03388b50ec13..3e9651b2d283e 100644 --- a/tests/defer_regress/tests.py +++ b/tests/defer_regress/tests.py @@ -4,8 +4,8 @@ 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.db.models.loading import cache from django.test import TestCase from django.test.utils import override_settings @@ -103,7 +103,7 @@ def test_ticket_11936(self): klasses = set( map( attrgetter("__name__"), - cache.get_models(cache.get_app("defer_regress")) + app_cache.get_models(app_cache.get_app_config("defer_regress").models_module) ) ) self.assertIn("Child", klasses) @@ -111,13 +111,13 @@ def test_ticket_11936(self): self.assertNotIn("Child_Deferred_value", klasses) self.assertNotIn("Item_Deferred_name", klasses) self.assertFalse(any( - k._deferred for k in cache.get_models(cache.get_app("defer_regress")))) + k._deferred for k in app_cache.get_models(app_cache.get_app_config("defer_regress").models_module))) klasses_with_deferred = set( map( attrgetter("__name__"), - cache.get_models( - cache.get_app("defer_regress"), include_deferred=True + app_cache.get_models( + app_cache.get_app_config("defer_regress").models_module, include_deferred=True ), ) ) @@ -126,8 +126,8 @@ def test_ticket_11936(self): self.assertIn("Child_Deferred_value", klasses_with_deferred) self.assertIn("Item_Deferred_name", klasses_with_deferred) self.assertTrue(any( - k._deferred for k in cache.get_models( - cache.get_app("defer_regress"), include_deferred=True)) + k._deferred for k in app_cache.get_models( + app_cache.get_app_config("defer_regress").models_module, include_deferred=True)) ) @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer') diff --git a/tests/empty/no_models/tests.py b/tests/empty/no_models/tests.py deleted file mode 100644 index 8b8db1af39e3f..0000000000000 --- a/tests/empty/no_models/tests.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.test import TestCase - - -class NoModelTests(TestCase): - """ A placeholder test case. See empty.tests for more info. """ - pass diff --git a/tests/empty/tests.py b/tests/empty/tests.py index 007d04c363aa1..7cebb87c2a368 100644 --- a/tests/empty/tests.py +++ b/tests/empty/tests.py @@ -1,8 +1,4 @@ -from django.core.exceptions import ImproperlyConfigured -from django.db.models.loading import get_app from django.test import TestCase -from django.test.utils import override_settings -from django.utils import six from .models import Empty @@ -17,20 +13,3 @@ def test_empty(self): self.assertTrue(m.id is not None) existing = Empty(m.id) existing.save() - - -class NoModelTests(TestCase): - """ - Test for #7198 to ensure that the proper error message is raised - when attempting to load an app with no models.py file. - - Because the test runner won't currently load a test module with no - models.py file, this TestCase instead lives in this module. - - It seemed like an appropriate home for it. - """ - @override_settings(INSTALLED_APPS=("empty.no_models",)) - def test_no_models(self): - with six.assertRaisesRegex(self, ImproperlyConfigured, - 'App with label no_models is missing a models.py module.'): - get_app('no_models') diff --git a/tests/invalid_models/tests.py b/tests/invalid_models/tests.py index 9c9db91da9dda..712484c6112b4 100644 --- a/tests/invalid_models/tests.py +++ b/tests/invalid_models/tests.py @@ -1,9 +1,8 @@ -import copy import sys import unittest +from django.core.apps import app_cache from django.core.management.validation import get_validation_errors -from django.db.models.loading import cache, load_app from django.test.utils import override_settings from django.utils.six import StringIO @@ -19,16 +18,13 @@ def setUp(self): self.stdout = StringIO() sys.stdout = self.stdout - # This test adds dummy applications to the app cache. These - # need to be removed in order to prevent bad interactions - # with the flush operation in other tests. - self.old_app_models = copy.deepcopy(cache.app_models) - self.old_app_store = copy.deepcopy(cache.app_store) + # 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): - cache.app_models = self.old_app_models - cache.app_store = self.old_app_store - cache._get_models_cache = {} + app_cache.app_configs['invalid_models'].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 @@ -42,7 +38,7 @@ def tearDown(self): ) def test_invalid_models(self): try: - module = load_app("invalid_models.invalid_models") + module = app_cache.load_app("invalid_models.invalid_models") except Exception: self.fail('Unable to load invalid model module') diff --git a/tests/managers_regress/tests.py b/tests/managers_regress/tests.py index 3798b91ef5028..f2897862ec530 100644 --- a/tests/managers_regress/tests.py +++ b/tests/managers_regress/tests.py @@ -1,8 +1,7 @@ from __future__ import unicode_literals -import copy +from django.core.apps import app_cache from django.db import models -from django.db.models.loading import cache from django.template import Context, Template from django.test import TestCase from django.test.utils import override_settings @@ -110,13 +109,11 @@ def test_explicit_abstract_manager(self): @override_settings(TEST_SWAPPABLE_MODEL='managers_regress.Parent') def test_swappable_manager(self): - try: - # This test adds dummy models to the app cache. These - # need to be removed in order to prevent bad interactions - # with the flush operation in other tests. - old_app_models = copy.deepcopy(cache.app_models) - old_app_store = copy.deepcopy(cache.app_store) + # The models need to be removed after the test in order to prevent bad + # interactions with the flush operation in other tests. + _old_models = app_cache.app_configs['managers_regress'].models.copy() + try: class SwappableModel(models.Model): class Meta: swappable = 'TEST_SWAPPABLE_MODEL' @@ -130,18 +127,16 @@ class Meta: self.assertEqual(str(e), "Manager isn't available; SwappableModel has been swapped for 'managers_regress.Parent'") finally: - cache.app_models = old_app_models - cache.app_store = old_app_store + app_cache.app_configs['managers_regress'].models = _old_models + app_cache._get_models_cache = {} @override_settings(TEST_SWAPPABLE_MODEL='managers_regress.Parent') def test_custom_swappable_manager(self): - try: - # This test adds dummy models to the app cache. These - # need to be removed in order to prevent bad interactions - # with the flush operation in other tests. - old_app_models = copy.deepcopy(cache.app_models) - old_app_store = copy.deepcopy(cache.app_store) + # The models need to be removed after the test in order to prevent bad + # interactions with the flush operation in other tests. + _old_models = app_cache.app_configs['managers_regress'].models.copy() + try: class SwappableModel(models.Model): stuff = models.Manager() @@ -159,18 +154,16 @@ class Meta: self.assertEqual(str(e), "Manager isn't available; SwappableModel has been swapped for 'managers_regress.Parent'") finally: - cache.app_models = old_app_models - cache.app_store = old_app_store + app_cache.app_configs['managers_regress'].models = _old_models + app_cache._get_models_cache = {} @override_settings(TEST_SWAPPABLE_MODEL='managers_regress.Parent') def test_explicit_swappable_manager(self): - try: - # This test adds dummy models to the app cache. These - # need to be removed in order to prevent bad interactions - # with the flush operation in other tests. - old_app_models = copy.deepcopy(cache.app_models) - old_app_store = copy.deepcopy(cache.app_store) + # The models need to be removed after the test in order to prevent bad + # interactions with the flush operation in other tests. + _old_models = app_cache.app_configs['managers_regress'].models.copy() + try: class SwappableModel(models.Model): objects = models.Manager() @@ -188,8 +181,8 @@ class Meta: self.assertEqual(str(e), "Manager isn't available; SwappableModel has been swapped for 'managers_regress.Parent'") finally: - cache.app_models = old_app_models - cache.app_store = old_app_store + app_cache.app_configs['managers_regress'].models = _old_models + app_cache._get_models_cache = {} def test_regress_3871(self): related = RelatedModel.objects.create() diff --git a/tests/migrations/models.py b/tests/migrations/models.py index 3bb50289bed8d..03390b3a06671 100644 --- a/tests/migrations/models.py +++ b/tests/migrations/models.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.core.apps.cache import BaseAppCache from django.db import models -from django.db.models.loading import BaseAppCache from django.utils.encoding import python_2_unicode_compatible diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 48fb68b03dfab..59cd56e78bf33 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -2,12 +2,11 @@ from __future__ import unicode_literals import codecs -import copy import os import shutil +from django.core.apps import app_cache from django.core.management import call_command, CommandError -from django.db.models.loading import cache from django.test.utils import override_settings from django.utils import six from django.utils._os import upath @@ -132,13 +131,11 @@ def setUp(self): self.test_dir = os.path.abspath(os.path.dirname(upath(__file__))) self.migration_dir = os.path.join(self.test_dir, 'migrations_%d' % self.creation_counter) self.migration_pkg = "migrations.migrations_%d" % self.creation_counter - self._old_app_models = copy.deepcopy(cache.app_models) - self._old_app_store = copy.deepcopy(cache.app_store) + self._old_models = app_cache.app_configs['migrations'].models.copy() def tearDown(self): - cache.app_models = self._old_app_models - cache.app_store = self._old_app_store - cache._get_models_cache = {} + app_cache.app_configs['migrations'].models = self._old_models + app_cache._get_models_cache = {} os.chdir(self.test_dir) try: @@ -154,7 +151,7 @@ def _rmrf(self, dname): def test_files_content(self): self.assertTableNotExists("migrations_unicodemodel") - cache.register_models('migrations', UnicodeModel) + app_cache.register_model('migrations', UnicodeModel) with override_settings(MIGRATION_MODULES={"migrations": self.migration_pkg}): call_command("makemigrations", "migrations", verbosity=0) @@ -190,7 +187,7 @@ def test_files_content(self): def test_failing_migration(self): #21280 - If a migration fails to serialize, it shouldn't generate an empty file. - cache.register_models('migrations', UnserializableModel) + app_cache.register_model('migrations', UnserializableModel) with six.assertRaisesRegex(self, ValueError, r'Cannot serialize'): with override_settings(MIGRATION_MODULES={"migrations": self.migration_pkg}): diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 20aff59de2313..0ce030a66c0bd 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -203,8 +203,8 @@ def test_add_field_m2m(self): self.assertColumnNotExists("test_adflmm_pony", "stables") # Make sure the M2M field actually works with atomic(): - app_cache = new_state.render() - Pony = app_cache.get_model("test_adflmm", "Pony") + new_app_cache = new_state.render() + Pony = new_app_cache.get_model("test_adflmm", "Pony") p = Pony.objects.create(pink=False, weight=4.55) p.stables.create() self.assertEqual(p.stables.count(), 1) diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index 9cbbbf294d7cc..206bf78b352b3 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -1,7 +1,7 @@ -from django.test import TestCase +from django.core.apps.cache import BaseAppCache from django.db import models -from django.db.models.loading import BaseAppCache from django.db.migrations.state import ProjectState, ModelState, InvalidBasesError +from django.test import TestCase class StateTests(TestCase): diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 0d64d403503fa..76fe5b5a9e7d6 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -2,14 +2,13 @@ from __future__ import unicode_literals -import copy import datetime import os +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.db.models.loading import cache from django.test import TestCase, override_settings from django.utils import six from django.utils.deconstruct import deconstructible @@ -116,8 +115,6 @@ def test_simple_migration(self): self.assertIn("Migration", result) def test_migration_path(self): - _old_app_store = copy.deepcopy(cache.app_store) - test_apps = [ 'migrations.migrations_test_apps.normal', 'migrations.migrations_test_apps.with_package_model', @@ -125,13 +122,10 @@ def test_migration_path(self): base_dir = os.path.dirname(os.path.dirname(__file__)) - try: - with override_settings(INSTALLED_APPS=test_apps): - for app in test_apps: - 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) - finally: - cache.app_store = _old_app_store + 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) diff --git a/tests/no_models/__init__.py b/tests/no_models/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/no_models/tests.py b/tests/no_models/tests.py new file mode 100644 index 0000000000000..34ef7244465b0 --- /dev/null +++ b/tests/no_models/tests.py @@ -0,0 +1,10 @@ +from django.core.apps import app_cache +from django.test import TestCase + + +class NoModelTests(TestCase): + + def test_no_models(self): + """Test that it's possible to load an app with no models.py file.""" + app_config = app_cache.get_app_config('no_models') + self.assertIsNone(app_config.models_module) diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 9941506303b4b..861ab4af170cc 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -4,8 +4,8 @@ import sys from django.conf import settings +from django.core.apps import app_cache from django.core.management import call_command -from django.db.models.loading import cache, load_app from django.test import TestCase, TransactionTestCase from django.test.utils import override_settings from django.utils._os import upath @@ -28,23 +28,19 @@ 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: - load_app(app) + app_cache.load_app(app) def tearDown(self): sys.path = self.old_sys_path - del cache.app_store[cache.app_labels['app1']] - del cache.app_store[cache.app_labels['app2']] - del cache.app_labels['app1'] - del cache.app_labels['app2'] - del cache.app_models['app1'] - del cache.app_models['app2'] + del app_cache.app_configs['app1'] + del app_cache.app_configs['app2'] def test_table_exists(self): try: - cache.set_available_apps(settings.INSTALLED_APPS) + app_cache.set_available_apps(settings.INSTALLED_APPS) call_command('migrate', verbosity=0) finally: - cache.unset_available_apps() + app_cache.unset_available_apps() from .app1.models import ProxyModel from .app2.models import NiceModel self.assertEqual(NiceModel.objects.all().count(), 0) diff --git a/tests/proxy_models/tests.py b/tests/proxy_models/tests.py index 8be7929ac48d9..3389f3597fd1d 100644 --- a/tests/proxy_models/tests.py +++ b/tests/proxy_models/tests.py @@ -1,13 +1,12 @@ from __future__ import unicode_literals -import copy 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 -from django.db.models.loading import cache from django.test import TestCase from django.test.utils import override_settings @@ -155,13 +154,11 @@ class Meta: @override_settings(TEST_SWAPPABLE_MODEL='proxy_models.AlternateModel') def test_swappable(self): - try: - # This test adds dummy applications to the app cache. These - # need to be removed in order to prevent bad interactions - # with the flush operation in other tests. - old_app_models = copy.deepcopy(cache.app_models) - old_app_store = copy.deepcopy(cache.app_store) + # The models need to be removed after the test in order to prevent bad + # interactions with the flush operation in other tests. + _old_models = app_cache.app_configs['proxy_models'].models.copy() + try: class SwappableModel(models.Model): class Meta: @@ -177,8 +174,8 @@ class ProxyModel(SwappableModel): class Meta: proxy = True finally: - cache.app_models = old_app_models - cache.app_store = old_app_store + app_cache.app_configs['proxy_models'].models = _old_models + app_cache._get_models_cache = {} def test_myperson_manager(self): Person.objects.create(name="fred") diff --git a/tests/runtests.py b/tests/runtests.py index f37c0e9dda03d..d5b94064d5fd2 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -80,14 +80,14 @@ def get_test_modules(): def get_installed(): - from django.db.models.loading import get_apps - return [app.__name__.rsplit('.', 1)[0] for app in get_apps()] + from django.core.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.conf import settings - from django.db.models.loading import get_apps, load_app + 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__)) @@ -128,7 +128,7 @@ def no_available_apps(self): # 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) - get_apps() + app_cache.populate() # Load all the test model apps. test_modules = get_test_modules() @@ -164,7 +164,7 @@ def no_available_apps(self): if module_found_in_labels: if verbosity >= 2: print("Importing application %s" % module_name) - mod = load_app(module_label) + mod = app_cache.load_app(module_label) if mod: if module_label not in settings.INSTALLED_APPS: settings.INSTALLED_APPS.append(module_label) diff --git a/tests/schema/models.py b/tests/schema/models.py index 06ba8fb760c83..e1369bff05178 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -1,5 +1,5 @@ +from django.core.apps.cache import BaseAppCache from django.db import models -from django.db.models.loading import BaseAppCache # Because we want to test creation and deletion of these as separate things, # these models are all inserted into a separate AppCache so the main test diff --git a/tests/swappable_models/tests.py b/tests/swappable_models/tests.py index 85484615a3ec0..fec9db5af8c10 100644 --- a/tests/swappable_models/tests.py +++ b/tests/swappable_models/tests.py @@ -4,8 +4,8 @@ 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.db.models.loading import cache from django.test import TestCase from django.test.utils import override_settings @@ -23,12 +23,12 @@ class SwappableModelTests(TestCase): def setUp(self): # This test modifies the installed apps, so we need to make sure # we're not dealing with a cached app list. - cache._get_models_cache.clear() + app_cache._get_models_cache.clear() def tearDown(self): # By fiddling with swappable models, we alter the installed models # cache, so flush it to make sure there are no side effects. - cache._get_models_cache.clear() + app_cache._get_models_cache.clear() @override_settings(TEST_ARTICLE_MODEL='swappable_models.AlternateArticle') def test_generated_data(self): diff --git a/tests/tablespaces/tests.py b/tests/tablespaces/tests.py index 6a81643a0c019..4a62ad5a45590 100644 --- a/tests/tablespaces/tests.py +++ b/tests/tablespaces/tests.py @@ -1,10 +1,8 @@ from __future__ import unicode_literals -import copy - from django.conf import settings +from django.core.apps import app_cache from django.db import connection -from django.db.models.loading import cache from django.core.management.color import no_style from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature @@ -28,8 +26,7 @@ class TablespacesTests(TestCase): def setUp(self): # The unmanaged models need to be removed after the test in order to # prevent bad interactions with the flush operation in other tests. - self.old_app_models = copy.deepcopy(cache.app_models) - self.old_app_store = copy.deepcopy(cache.app_store) + self._old_models = app_cache.app_configs['tablespaces'].models.copy() for model in Article, Authors, Reviewers, Scientist: model._meta.managed = True @@ -38,9 +35,8 @@ def tearDown(self): for model in Article, Authors, Reviewers, Scientist: model._meta.managed = False - cache.app_models = self.old_app_models - cache.app_store = self.old_app_store - cache._get_models_cache = {} + app_cache.app_configs['tablespaces'].models = self._old_models + app_cache._get_models_cache = {} def assertNumContains(self, haystack, needle, count): real_count = haystack.count(needle) diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 8b9dbe3faec20..04fa3b8784da6 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -5,6 +5,7 @@ from importlib import import_module from optparse import make_option +import types import unittest from django.core.exceptions import ImproperlyConfigured @@ -18,10 +19,6 @@ from .models import Person -TEST_APP_OK = 'test_runner.valid_app.models' -TEST_APP_ERROR = 'test_runner_invalid_app.models' - - class DependencyOrderingTests(unittest.TestCase): def test_simple_dependencies(self): @@ -228,16 +225,24 @@ 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.test.simple import get_tests - module = import_module(TEST_APP_OK) - tests = get_tests(module) - self.assertIsInstance(tests, type(module)) + app_config = AppConfig( + 'test_runner.valid_app', + import_module('test_runner.valid_app'), + import_module('test_runner.valid_app.models')) + tests = get_tests(app_config) + self.assertIsInstance(tests, types.ModuleType) 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.test.simple import get_tests - module = import_module(TEST_APP_ERROR) - self.assertRaises(ImportError, get_tests, module) + 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) class Sqlite3InMemoryTestDbs(TestCase): diff --git a/tests/test_suite_override/tests.py b/tests/test_suite_override/tests.py index e69dab12bf421..b464659275b27 100644 --- a/tests/test_suite_override/tests.py +++ b/tests/test_suite_override/tests.py @@ -1,6 +1,6 @@ import unittest -from django.db.models import get_app +from django.core.apps import app_cache from django.test.utils import IgnoreAllDeprecationWarningsMixin @@ -20,8 +20,8 @@ def test_suite_override(self): """ from django.test.simple import build_suite - app = get_app("test_suite_override") - suite = build_suite(app) + app_config = app_cache.get_app_config("test_suite_override") + suite = build_suite(app_config) self.assertEqual(suite.countTestCases(), 1) diff --git a/tests/validation/test_unique.py b/tests/validation/test_unique.py index b3d389e4ecfa5..83863b19c6fd8 100644 --- a/tests/validation/test_unique.py +++ b/tests/validation/test_unique.py @@ -3,9 +3,9 @@ import datetime import unittest +from django.core.apps.cache import BaseAppCache from django.core.exceptions import ValidationError from django.db import models -from django.db.models.loading import BaseAppCache from django.test import TestCase from .models import (CustomPKModel, UniqueTogetherModel, UniqueFieldsModel,