From dda6759e0e7f4ff4e1a856c1d907cea619a1dfda Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 12 Jul 2014 15:33:21 +0200 Subject: [PATCH] [1.7.x] Checked more precisely whether the app registry is ready. Accounted for the three stages of population: app configs, models, ready() methods of app configs. Backport of a764a9cc from master --- django/apps/config.py | 15 +++++++++--- django/apps/registry.py | 51 ++++++++++++++++++++++++--------------- docs/ref/applications.txt | 10 +++++--- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/django/apps/config.py b/django/apps/config.py index dd1018b50a..9cfe4858aa 100644 --- a/django/apps/config.py +++ b/django/apps/config.py @@ -1,7 +1,7 @@ from importlib import import_module import os -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured from django.utils.module_loading import module_has_submodule from django.utils._os import upath @@ -139,15 +139,21 @@ class AppConfig(object): # Entry is a path to an app config class. return cls(app_name, app_module) + def check_models_ready(self): + """ + Raises an exception if models haven't been imported yet. + """ + if self.models is None: + raise AppRegistryNotReady( + "Models for app '%s' haven't been imported yet." % self.label) + def get_model(self, model_name): """ Returns the model with the given case-insensitive model_name. Raises LookupError if no model exists with this name. """ - if self.models is None: - raise LookupError( - "App '%s' doesn't have any models." % self.label) + self.check_models_ready() try: return self.models[model_name.lower()] except KeyError: @@ -169,6 +175,7 @@ class AppConfig(object): Set the corresponding keyword argument to True to include such models. Keyword arguments aren't documented; they're a private API. """ + self.check_models_ready() for model in self.models.values(): if model._deferred and not include_deferred: continue diff --git a/django/apps/registry.py b/django/apps/registry.py index 492a97f603..57bdc3edb2 100644 --- a/django/apps/registry.py +++ b/django/apps/registry.py @@ -43,7 +43,7 @@ class Apps(object): self.stored_app_configs = [] # Whether the registry is populated. - self.ready = False + self.apps_ready = self.models_ready = self.ready = False # Lock for thread-safe population. self._lock = threading.Lock() @@ -100,29 +100,41 @@ class Apps(object): "Application names aren't unique, " "duplicates: %s" % ", ".join(duplicates)) + self.apps_ready = True + # Load models. for app_config in self.app_configs.values(): all_models = self.all_models[app_config.label] app_config.import_models(all_models) self.clear_cache() - self.ready = True + + self.models_ready = True for app_config in self.get_app_configs(): app_config.ready() - def check_ready(self): + self.ready = True + + def check_apps_ready(self): """ - Raises an exception if the registry isn't ready. + Raises an exception if all apps haven't been imported yet. """ - if not self.ready: - raise AppRegistryNotReady() + if not self.apps_ready: + raise AppRegistryNotReady("Apps aren't loaded yet.") + + def check_models_ready(self): + """ + Raises an exception if all models haven't been imported yet. + """ + if not self.models_ready: + raise AppRegistryNotReady("Models aren't loaded yet.") def get_app_configs(self): """ Imports applications and returns an iterable of app configs. """ - self.check_ready() + self.check_apps_ready() return self.app_configs.values() def get_app_config(self, app_label): @@ -131,7 +143,7 @@ class Apps(object): Raises LookupError if no application exists with this label. """ - self.check_ready() + self.check_apps_ready() try: return self.app_configs[app_label] except KeyError: @@ -153,7 +165,7 @@ class Apps(object): Set the corresponding keyword argument to True to include such models. """ - self.check_ready() + self.check_models_ready() if app_mod: warnings.warn( "The app_mod argument of get_models is deprecated.", @@ -184,7 +196,7 @@ class Apps(object): model exists with this name in the application. Raises ValueError if called with a single argument that doesn't contain exactly one dot. """ - self.check_ready() + self.check_models_ready() if model_name is None: app_label, model_name = app_label.split('.') return self.get_app_config(app_label).get_model(model_name.lower()) @@ -207,10 +219,8 @@ class Apps(object): Checks whether an application with this name exists in the registry. app_name is the full name of the app eg. 'django.contrib.admin'. - - It's safe to call this method at import time, even while the registry - is being populated. It returns False for apps that aren't loaded yet. """ + self.check_apps_ready() return any(ac.name == app_name for ac in self.app_configs.values()) def get_containing_app_config(self, object_name): @@ -221,10 +231,10 @@ class Apps(object): Returns the app config for the inner application in case of nesting. Returns None if the object isn't in any registered app config. - - It's safe to call this method at import time, even while the registry - is being populated. """ + # In Django 1.7 and 1.8, it's allowed to call this method at import + # time, even while the registry is being populated. In Django 1.9 and + # later, that should be forbidden with `self.check_apps_ready()`. candidates = [] for app_config in self.app_configs.values(): if object_name.startswith(app_config.name): @@ -297,10 +307,11 @@ class Apps(object): imports safely (eg. that could lead to registering listeners twice), models are registered when they're imported and never removed. """ - self.check_ready() + if not self.ready: + raise AppRegistryNotReady("App registry isn't ready yet.") self.stored_app_configs.append(self.app_configs) self.app_configs = OrderedDict() - self.ready = False + self.apps_ready = self.models_ready = self.ready = False self.clear_cache() self.populate(installed) @@ -309,7 +320,7 @@ class Apps(object): Cancels a previous call to set_installed_apps(). """ self.app_configs = self.stored_app_configs.pop() - self.ready = True + self.apps_ready = self.models_ready = self.ready = True self.clear_cache() def clear_cache(self): @@ -402,7 +413,7 @@ class Apps(object): warnings.warn( "[a.path for a in get_app_configs()] supersedes get_app_paths().", RemovedInDjango19Warning, stacklevel=2) - self.check_ready() + self.check_apps_ready() app_paths = [] for app in self.get_apps(): app_paths.append(self._get_app_path(app)) diff --git a/docs/ref/applications.txt b/docs/ref/applications.txt index f944a6fcac..6469b76630 100644 --- a/docs/ref/applications.txt +++ b/docs/ref/applications.txt @@ -301,10 +301,6 @@ Application registry Checks whether an application with the given name exists in the registry. ``app_name`` is the full name of the app, e.g. ``'django.contrib.admin'``. - Unlike :meth:`~django.apps.apps.get_app_config`, this method can be called - safely at import time. If the registry is still being populated, it may - return ``False``, even though the app will become available later. - .. method:: apps.get_model(app_label, model_name) Returns the :class:`~django.db.models.Model` with the given ``app_label`` @@ -365,6 +361,9 @@ processes all applications in the order of :setting:`INSTALLED_APPS`. the order of :setting:`INSTALLED_APPS`, it's strongly recommended not import any models at this stage. + Once this stage completes, APIs that operate of application configurations + such as :meth:`~apps.get_app_config()` become usable. + #. Then Django attempts to import the ``models`` submodule of each application, if there is one. @@ -372,6 +371,9 @@ processes all applications in the order of :setting:`INSTALLED_APPS`. ``models/__init__.py``. Otherwise, the application registry may not be fully populated at this point, which could cause the ORM to malfunction. + Once this stage completes, APIs that operate on models such as + :meth:`~apps.get_model()` become usable. + #. Finally Django runs the :meth:`~AppConfig.ready()` method of each application configuration.