diff --git a/django/apps/__init__.py b/django/apps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/apps/cache.py b/django/apps/cache.py new file mode 100644 index 0000000000..a5b645646f --- /dev/null +++ b/django/apps/cache.py @@ -0,0 +1,396 @@ +"Utilities for loading models and the modules that contain them." + +from collections import OrderedDict +import copy +import imp +from importlib import import_module +import os +import sys + +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 + +__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 diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index bd51f39977..698947d57c 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from datetime import date +from django.apps.cache import get_app from django.contrib.auth import models, management from django.contrib.auth.management import create_permissions from django.contrib.auth.management.commands import changepassword @@ -12,7 +13,6 @@ 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 diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index 239a0a416c..7c4bb4f7cf 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -3,6 +3,7 @@ import os import operator from optparse import make_option +from django.apps.cache import cache from django.core.management.base import BaseCommand, CommandError from django.core.exceptions import ImproperlyConfigured from django.db import connections, DEFAULT_DB_ALIAS, migrations @@ -11,7 +12,6 @@ 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 diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index 093c8a79d0..1966223042 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -6,6 +6,7 @@ from importlib import import_module import itertools import traceback +from django.apps.cache import cache from django.conf import settings from django.core.management import call_command from django.core.management.base import BaseCommand, CommandError @@ -16,7 +17,6 @@ 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 diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index 00a6602c0b..0d84c9b2ec 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -66,7 +66,7 @@ class Command(NoArgsCommand): def handle_noargs(self, **options): # XXX: (Temporary) workaround for ticket #1796: force early loading of all # models from installed apps. - from django.db.models.loading import get_models + from django.apps.cache import get_models get_models() use_plain = options.get('plain', False) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 6bdcf853d4..459923a088 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -26,8 +26,8 @@ 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.apps.cache import get_app_errors 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) diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index f04095507f..69e3b61b87 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -1,6 +1,6 @@ +from django.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 f2510b8368..101588c4a1 100644 --- a/django/db/migrations/loader.py +++ b/django/db/migrations/loader.py @@ -1,7 +1,7 @@ import os import sys from importlib import import_module -from django.db.models.loading import cache +from django.apps.cache import cache from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.graph import MigrationGraph from django.utils import six diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py index 7e798d3105..7874f23ed0 100644 --- a/django/db/migrations/questioner.py +++ b/django/db/migrations/questioner.py @@ -2,7 +2,7 @@ import importlib import os import sys -from django.db.models.loading import cache +from django.apps.cache import cache from django.utils import datetime_safe from django.utils.six.moves import input from django.core.exceptions import ImproperlyConfigured diff --git a/django/db/migrations/recorder.py b/django/db/migrations/recorder.py index be804e52aa..bf1cd225c2 100644 --- a/django/db/migrations/recorder.py +++ b/django/db/migrations/recorder.py @@ -1,5 +1,5 @@ +from django.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 769b0005f8..6aa44ba108 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -1,5 +1,5 @@ +from django.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 22b0977ba4..150455b09c 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -3,12 +3,12 @@ import datetime import types import os from importlib import import_module -from django.utils import six +from django.apps.cache import 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): diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 12c31f89fd..ad76347494 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -1,9 +1,9 @@ from functools import wraps -from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured # NOQA -from django.db.models.loading import ( # NOQA +from django.apps.cache import ( # NOQA get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp) +from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured # NOQA 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 diff --git a/django/db/models/base.py b/django/db/models/base.py index dd4850c3d4..f22506aa92 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -5,6 +5,7 @@ import sys from functools import update_wrapper from django.utils.six.moves import zip +from django.apps.cache import get_model, 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 +20,6 @@ from django.db.models.query_utils import DeferredAttribute, deferred_class_facto 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 diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 33adaedc7e..d69a4346e2 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -9,8 +9,8 @@ import warnings from base64 import b64decode, b64encode from itertools import tee +from django.apps.cache import get_model 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 diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 21440216e0..795de130b5 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -1,387 +1,15 @@ -"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.apps.cache import 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 diff --git a/django/db/models/options.py b/django/db/models/options.py index b14e61573c..5d99066343 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -5,11 +5,11 @@ import re from bisect import bisect import warnings +from django.apps.cache import app_cache_ready, cache from django.conf import settings 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 diff --git a/django/db/models/signals.py b/django/db/models/signals.py index 6b011c2099..2543cf5f4a 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.apps.cache import get_model from django.dispatch import Signal from django.utils import six diff --git a/django/test/testcases.py b/django/test/testcases.py index 4dbff55204..52900ec478 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -15,6 +15,7 @@ import unittest from unittest import skipIf # NOQA: Imported here for backward compatibility from unittest.util import safe_repr +from django.apps.cache import cache from django.conf import settings from django.core import mail from django.core.exceptions import ValidationError, ImproperlyConfigured @@ -25,7 +26,6 @@ from django.core.management.commands import flush 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 diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 2e53901536..d548f275fe 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -222,6 +222,9 @@ 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. + 2.0 --- diff --git a/tests/app_cache/models.py b/tests/app_cache/models.py index 1b4d33c2f9..cc092390ab 100644 --- a/tests/app_cache/models.py +++ b/tests/app_cache/models.py @@ -1,5 +1,5 @@ +from django.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 b72b862de3..564f9f6f48 100644 --- a/tests/app_cache/tests.py +++ b/tests/app_cache/tests.py @@ -1,7 +1,9 @@ from __future__ import absolute_import -from django.test import TestCase -from django.db.models.loading import cache, BaseAppCache + +from django.apps.cache import cache, BaseAppCache from django.db import models +from django.test import TestCase + from .models import TotallyNormal, SoAlternative, new_app_cache diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index 20ec064d69..4795ca4139 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -5,7 +5,7 @@ import os import sys from unittest import TestCase -from django.db.models.loading import cache, load_app, get_model, get_models, AppCache +from django.apps.cache import cache, load_app, get_model, get_models, AppCache from django.test.utils import override_settings from django.utils._os import upath diff --git a/tests/contenttypes_tests/tests.py b/tests/contenttypes_tests/tests.py index 9d1e1f77ea..a56d196933 100644 --- a/tests/contenttypes_tests/tests.py +++ b/tests/contenttypes_tests/tests.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals +from django.apps.cache import BaseAppCache from django.contrib.contenttypes.models import ContentType 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 c03388b50e..0107e8167c 100644 --- a/tests/defer_regress/tests.py +++ b/tests/defer_regress/tests.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals from operator import attrgetter +from django.apps.cache import cache from django.contrib.contenttypes.models import ContentType from django.contrib.sessions.backends.db import SessionStore 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 diff --git a/tests/empty/tests.py b/tests/empty/tests.py index 007d04c363..2a9f568aea 100644 --- a/tests/empty/tests.py +++ b/tests/empty/tests.py @@ -1,5 +1,5 @@ +from django.apps.cache import get_app 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 diff --git a/tests/invalid_models/tests.py b/tests/invalid_models/tests.py index 9c9db91da9..2f7815f96d 100644 --- a/tests/invalid_models/tests.py +++ b/tests/invalid_models/tests.py @@ -2,8 +2,8 @@ import copy import sys import unittest +from django.apps.cache import cache, load_app 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 diff --git a/tests/managers_regress/tests.py b/tests/managers_regress/tests.py index 3798b91ef5..11ad52ce44 100644 --- a/tests/managers_regress/tests.py +++ b/tests/managers_regress/tests.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals import copy +from django.apps.cache import 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 diff --git a/tests/migrations/models.py b/tests/migrations/models.py index 3bb50289be..9726e4457a 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.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 48fb68b03d..fa8a212533 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -6,8 +6,8 @@ import copy import os import shutil +from django.apps.cache import 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 diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index 9cbbbf294d..13e575862d 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -1,7 +1,7 @@ -from django.test import TestCase +from django.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 0d64d40350..11109c8186 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -6,10 +6,10 @@ import copy import datetime import os +from django.apps.cache import 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 diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 9941506303..af22e7caed 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -3,9 +3,9 @@ from __future__ import unicode_literals import os import sys +from django.apps.cache import cache, load_app from django.conf import settings 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 diff --git a/tests/proxy_models/tests.py b/tests/proxy_models/tests.py index 8be7929ac4..a900366744 100644 --- a/tests/proxy_models/tests.py +++ b/tests/proxy_models/tests.py @@ -1,13 +1,13 @@ from __future__ import unicode_literals import copy +from django.apps.cache import cache from django.contrib import admin from django.contrib.contenttypes.models import ContentType from django.core import management 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 diff --git a/tests/runtests.py b/tests/runtests.py index f37c0e9dda..8bcb06e522 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 + from django.apps.cache import get_apps return [app.__name__.rsplit('.', 1)[0] for app in get_apps()] def setup(verbosity, test_labels): import django + from django.apps.cache import get_apps, load_app from django.conf import settings - from django.db.models.loading import get_apps, load_app from django.test import TransactionTestCase, TestCase print("Testing against Django installed in '%s'" % os.path.dirname(django.__file__)) diff --git a/tests/schema/models.py b/tests/schema/models.py index 06ba8fb760..2df97b935c 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -1,5 +1,5 @@ +from django.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 85484615a3..ae42366745 100644 --- a/tests/swappable_models/tests.py +++ b/tests/swappable_models/tests.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals from django.utils.six import StringIO +from django.apps.cache import cache from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.core import management -from django.db.models.loading import cache from django.test import TestCase from django.test.utils import override_settings diff --git a/tests/tablespaces/tests.py b/tests/tablespaces/tests.py index 6a81643a0c..0ee1b0e742 100644 --- a/tests/tablespaces/tests.py +++ b/tests/tablespaces/tests.py @@ -2,9 +2,9 @@ from __future__ import unicode_literals import copy +from django.apps.cache import cache from django.conf import settings 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 diff --git a/tests/validation/test_unique.py b/tests/validation/test_unique.py index b3d389e4ec..995949fed7 100644 --- a/tests/validation/test_unique.py +++ b/tests/validation/test_unique.py @@ -3,9 +3,9 @@ from __future__ import unicode_literals import datetime import unittest +from django.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,