Commit 5891990b authored by Aymeric Augustin's avatar Aymeric Augustin
Browse files

Refactored INSTALLED_APPS overrides.

* Introduced [un]set_installed_apps to handle changes to the
  INSTALLED_APPS setting.
* Refactored [un]set_available_apps to share its implementation
  with [un]set_installed_apps.
* Implemented a receiver to clear some app-related caches.
* Removed test_missing_app as it is basically impossible to reproduce
  this situation with public methods of the new app cache.
parent 8cff95e9
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
from .base import AppConfig     # NOQA
from .cache import app_cache, UnavailableApp    # NOQA
from .cache import app_cache    # NOQA
+44 −44
Original line number Diff line number Diff line
@@ -14,10 +14,6 @@ from django.utils._os import upath
from .base import AppConfig


class UnavailableApp(Exception):
    pass


class AppCache(object):
    """
    A cache that stores installed applications and their models. Used to
@@ -43,9 +39,9 @@ class AppCache(object):
        # Mapping of labels to AppConfig instances for installed apps.
        self.app_configs = OrderedDict()

        # Set of app names. Allows restricting the set of installed apps.
        # Used by TransactionTestCase.available_apps for performance reasons.
        self.available_apps = None
        # Stack of app_configs. Used to store the current state in
        # set_available_apps and set_installed_apps.
        self.stored_app_configs = []

        # Internal flags used when populating the master cache.
        self._apps_loaded = not self.master
@@ -157,8 +153,6 @@ class AppCache(object):
        for app_config in self.app_configs.values():
            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_with_models_module=False):
@@ -167,9 +161,6 @@ class AppCache(object):

        Raises LookupError if no application exists with this label.

        Raises UnavailableApp when set_available_apps() disables the
        application with this label.

        If only_with_models_module in True (non-default), imports models and
        considers only applications containing a models module.
        """
@@ -183,8 +174,6 @@ class AppCache(object):
            raise LookupError("No installed app with label %r." % app_label)
        if only_with_models_module and app_config.models_module is None:
            raise LookupError("App with label %r doesn't have a models module." % app_label)
        if self.available_apps is not None and app_config.name not in self.available_apps:
            raise UnavailableApp("App with label %r isn't available." % app_label)
        return app_config

    def get_models(self, app_mod=None,
@@ -216,13 +205,7 @@ class AppCache(object):
        cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped)
        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
            return self._get_models_cache[cache_key]
        except KeyError:
            pass
        self.populate_models()
@@ -249,11 +232,6 @@ class AppCache(object):
                    (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, only_installed=True):
@@ -262,9 +240,6 @@ class AppCache(object):
        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.master:
            only_installed = False
@@ -273,9 +248,6 @@ class AppCache(object):
            app_config = self.app_configs.get(app_label)
            if app_config is None:
                return None
            if (self.available_apps is not None
                    and app_config.name not in self.available_apps):
                raise UnavailableApp("App with label %s isn't available." % app_label)
        return self.all_models[app_label].get(model_name.lower())

    def register_model(self, app_label, model):
@@ -326,22 +298,57 @@ class AppCache(object):
        available must be an iterable of application names.

        Primarily used for performance optimization in TransactionTestCase.

        This method is safe is the sense that it doesn't trigger any imports.
        """
        if self.available_apps is not None:
            raise RuntimeError("set_available_apps() may be called only once "
                "in a row; make sure it's paired with unset_available_apps()")
        available = set(available)
        installed = set(app_config.name for app_config in self.get_app_configs())
        if not available.issubset(installed):
            raise ValueError("Available apps isn't a subset of installed "
                "apps, extra apps: %s" % ", ".join(available - installed))
        self.available_apps = available

        self.stored_app_configs.append(self.app_configs)
        self.app_configs = OrderedDict(
            (label, app_config)
            for label, app_config in self.app_configs.items()
            if app_config.name in available)

    def unset_available_apps(self):
        """
        Cancels a previous call to set_available_apps().
        """
        self.available_apps = None
        self.app_configs = self.stored_app_configs.pop()

    def set_installed_apps(self, installed):
        """
        Enables a different set of installed_apps for get_app_config[s].

        installed must be an iterable in the same format as INSTALLED_APPS.

        Primarily used as a receiver of the setting_changed signal in tests.

        This method may trigger new imports, which may add new models to the
        registry of all imported models. They will stay in the registry even
        after unset_installed_apps(). Since it isn't possible to replay
        imports safely (eg. that could lead to registering listeners twice),
        models are registered when they're imported and never removed.
        """
        self.stored_app_configs.append(self.app_configs)
        self.app_configs = OrderedDict()
        try:
            self._apps_loaded = False
            self.populate_apps()
            self._models_loaded = False
            self.populate_models()
        except Exception:
            self.unset_installed_apps()
            raise

    def unset_installed_apps(self):
        """
        Cancels a previous call to set_installed_apps().
        """
        self.app_configs = self.stored_app_configs.pop()

    ### DANGEROUS METHODS ### (only used to preserve existing tests)

@@ -353,15 +360,11 @@ class AppCache(object):
        else:
            app_config.import_models(self.all_models[app_config.label])
            self.app_configs[app_config.label] = app_config
            if self.available_apps is not None:
                self.available_apps.add(app_config.name)
            return app_config

    def _end_with_app(self, app_config):
        if app_config is not None:
            del self.app_configs[app_config.label]
            if self.available_apps is not None:
                self.available_apps.discard(app_config.name)

    @contextmanager
    def _with_app(self, app_name):
@@ -420,9 +423,6 @@ class AppCache(object):
    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).",
+5 −8
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import getpass
import unicodedata

from django.apps import app_cache, UnavailableApp
from django.apps import app_cache
from django.contrib.auth import (models as auth_app, get_permission_codename,
    get_user_model)
from django.core import exceptions
@@ -61,9 +61,7 @@ def _check_permission_clashing(custom, builtin, ctype):


def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs):
    try:
        app_cache.get_model('auth', 'Permission')
    except UnavailableApp:
    if app_cache.get_model('auth', 'Permission') is None:
        return

    if not router.allow_migrate(db, auth_app.Permission):
@@ -119,12 +117,11 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw


def create_superuser(app, created_models, verbosity, db, **kwargs):
    try:
        app_cache.get_model('auth', 'Permission')
        UserModel = get_user_model()
    except UnavailableApp:
    if app_cache.get_model('auth', 'Permission') is None:
        return

    UserModel = get_user_model()

    from django.core.management import call_command

    if UserModel in created_models and kwargs.get('interactive', True):
+2 −4
Original line number Diff line number Diff line
from django.apps import app_cache, UnavailableApp
from django.apps import app_cache
from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS, router
from django.db.models import signals
@@ -12,9 +12,7 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, *
    Creates content types for models in the given app, removing any model
    entries that no longer have a matching model class.
    """
    try:
        app_cache.get_model('contenttypes', 'ContentType')
    except UnavailableApp:
    if app_cache.get_model('contenttypes', 'ContentType') is None:
        return

    if not router.allow_migrate(db, ContentType):
+9 −1
Original line number Diff line number Diff line
@@ -17,7 +17,7 @@ setting_changed = Signal(providing_args=["setting", "value", "enter"])
# except for cases where the receiver is related to a contrib app.

# Settings that may not work well when using 'override_settings' (#19031)
COMPLEX_OVERRIDE_SETTINGS = set(['DATABASES', 'INSTALLED_APPS'])
COMPLEX_OVERRIDE_SETTINGS = set(['DATABASES'])


@receiver(setting_changed)
@@ -27,6 +27,14 @@ def clear_cache_handlers(**kwargs):
        caches._caches = threading.local()


@receiver(setting_changed)
def update_installed_apps(**kwargs):
    if kwargs['setting'] == 'INSTALLED_APPS':
        # Rebuild any AppDirectoriesFinder instance.
        from django.contrib.staticfiles.finders import get_finder
        get_finder.cache_clear()


@receiver(setting_changed)
def update_connections_time_zone(**kwargs):
    if kwargs['setting'] == 'TIME_ZONE':
Loading