Stopped populating the app registry as a side effect.

Since it triggers imports, it shouldn't be done lightly.

This commit adds a public API for doing it explicitly, django.setup(),
and does it automatically when using manage.py and wsgi.py.
This commit is contained in:
Aymeric Augustin 2013-12-30 15:42:15 +01:00
parent 7ed20e0153
commit 80d74097b4
13 changed files with 65 additions and 52 deletions

View File

@ -6,3 +6,12 @@ def get_version(*args, **kwargs):
# Only import if it's actually called. # Only import if it's actually called.
from django.utils.version import get_version from django.utils.version import get_version
return get_version(*args, **kwargs) return get_version(*args, **kwargs)
def setup():
# Configure the settings (this happens as a side effect of accessing
# INSTALLED_APPS or any other setting) and populate the app registry.
from django.apps import apps
from django.conf import settings
apps.populate_apps(settings.INSTALLED_APPS)
apps.populate_models()

View File

@ -114,8 +114,6 @@ class AppConfig(object):
Returns the model with the given case-insensitive model_name. Returns the model with the given case-insensitive model_name.
Raises LookupError if no model exists with this name. Raises LookupError if no model exists with this name.
This method assumes that apps.populate_models() has run.
""" """
if self.models is None: if self.models is None:
raise LookupError( raise LookupError(
@ -140,8 +138,6 @@ class AppConfig(object):
Set the corresponding keyword argument to True to include such models. Set the corresponding keyword argument to True to include such models.
Keyword arguments aren't documented; they're a private API. Keyword arguments aren't documented; they're a private API.
This method assumes that apps.populate_models() has run.
""" """
for model in self.models.values(): for model in self.models.values():
if model._deferred and not include_deferred: if model._deferred and not include_deferred:
@ -156,7 +152,8 @@ class AppConfig(object):
# Dictionary of models for this app, primarily maintained in the # Dictionary of models for this app, primarily maintained in the
# 'all_models' attribute of the Apps this AppConfig is attached to. # 'all_models' attribute of the Apps this AppConfig is attached to.
# Injected as a parameter because it gets populated when models are # Injected as a parameter because it gets populated when models are
# imported, which may happen before populate_models() runs. # imported, which might happen before populate_models() runs (or at
# least used to).
self.models = all_models self.models = all_models
if module_has_submodule(self.module, MODELS_MODULE_NAME): if module_has_submodule(self.module, MODELS_MODULE_NAME):

View File

@ -3,7 +3,6 @@ import os
import sys import sys
import warnings import warnings
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils import lru_cache from django.utils import lru_cache
from django.utils.module_loading import import_lock from django.utils.module_loading import import_lock
@ -79,8 +78,6 @@ class Apps(object):
# Application modules aren't expected to import anything, and # Application modules aren't expected to import anything, and
# especially not other application modules, even indirectly. # especially not other application modules, even indirectly.
# Therefore we simply import them sequentially. # Therefore we simply import them sequentially.
if installed_apps is None:
installed_apps = settings.INSTALLED_APPS
for entry in installed_apps: for entry in installed_apps:
if isinstance(entry, AppConfig): if isinstance(entry, AppConfig):
app_config = entry app_config = entry
@ -108,7 +105,9 @@ class Apps(object):
if self._models_loaded: if self._models_loaded:
return return
self.populate_apps() if not self._apps_loaded:
raise RuntimeError(
"populate_models() must run after populate_apps()")
# Models modules are likely to import other models modules, for # Models modules are likely to import other models modules, for
# example to reference related objects. As a consequence: # example to reference related objects. As a consequence:
@ -144,6 +143,15 @@ class Apps(object):
for app_config in self.get_app_configs(): for app_config in self.get_app_configs():
app_config.setup() app_config.setup()
def check_ready(self):
"""
Raises an exception if the registry isn't ready.
"""
if not self._models_loaded:
raise RuntimeError(
"App registry isn't populated yet. "
"Have you called django.setup()?")
@property @property
def ready(self): def ready(self):
""" """
@ -161,11 +169,7 @@ class Apps(object):
If only_with_models_module in True (non-default), imports models and If only_with_models_module in True (non-default), imports models and
considers only applications containing a models module. considers only applications containing a models module.
""" """
if only_with_models_module: self.check_ready()
self.populate_models()
else:
self.populate_apps()
for app_config in self.app_configs.values(): for app_config in self.app_configs.values():
if only_with_models_module and app_config.models_module is None: if only_with_models_module and app_config.models_module is None:
continue continue
@ -180,11 +184,7 @@ class Apps(object):
If only_with_models_module in True (non-default), imports models and If only_with_models_module in True (non-default), imports models and
considers only applications containing a models module. considers only applications containing a models module.
""" """
if only_with_models_module: self.check_ready()
self.populate_models()
else:
self.populate_apps()
app_config = self.app_configs.get(app_label) app_config = self.app_configs.get(app_label)
if app_config is None: if app_config is None:
raise LookupError("No installed app with label '%s'." % app_label) raise LookupError("No installed app with label '%s'." % app_label)
@ -208,8 +208,7 @@ class Apps(object):
Set the corresponding keyword argument to True to include such models. Set the corresponding keyword argument to True to include such models.
""" """
self.populate_models() self.check_ready()
if app_mod: if app_mod:
warnings.warn( warnings.warn(
"The app_mod argument of get_models is deprecated.", "The app_mod argument of get_models is deprecated.",
@ -236,7 +235,7 @@ class Apps(object):
Raises LookupError if no application exists with this label, or no Raises LookupError if no application exists with this label, or no
model exists with this name in the application. model exists with this name in the application.
""" """
self.populate_models() self.check_ready()
return self.get_app_config(app_label).get_model(model_name.lower()) return self.get_app_config(app_label).get_model(model_name.lower())
def register_model(self, app_label, model): def register_model(self, app_label, model):
@ -328,7 +327,8 @@ class Apps(object):
imports safely (eg. that could lead to registering listeners twice), imports safely (eg. that could lead to registering listeners twice),
models are registered when they're imported and never removed. models are registered when they're imported and never removed.
""" """
self.stored_app_configs.append((self.app_configs, self._apps_loaded, self._models_loaded)) self.check_ready()
self.stored_app_configs.append(self.app_configs)
self.app_configs = OrderedDict() self.app_configs = OrderedDict()
self.clear_cache() self.clear_cache()
self._apps_loaded = False self._apps_loaded = False
@ -340,7 +340,9 @@ class Apps(object):
""" """
Cancels a previous call to set_installed_apps(). Cancels a previous call to set_installed_apps().
""" """
self.app_configs, self._apps_loaded, self._models_loaded = self.stored_app_configs.pop() self.app_configs = self.stored_app_configs.pop()
self._apps_loaded = True
self._models_loaded = True
self.clear_cache() self.clear_cache()
def clear_cache(self): def clear_cache(self):
@ -429,9 +431,7 @@ class Apps(object):
warnings.warn( warnings.warn(
"[a.path for a in get_app_configs()] supersedes get_app_paths().", "[a.path for a in get_app_configs()] supersedes get_app_paths().",
PendingDeprecationWarning, stacklevel=2) PendingDeprecationWarning, stacklevel=2)
self.check_ready()
self.populate_models()
app_paths = [] app_paths = []
for app in self.get_apps(): for app in self.get_apps():
app_paths.append(self._get_app_path(app)) app_paths.append(self._get_app_path(app))

View File

@ -160,7 +160,6 @@ class AdminSite(object):
The default implementation checks that admin and contenttypes apps are The default implementation checks that admin and contenttypes apps are
installed, as well as the auth context processor. installed, as well as the auth context processor.
""" """
apps.populate_apps()
if not apps.has_app('django.contrib.admin'): if not apps.has_app('django.contrib.admin'):
raise ImproperlyConfigured("Put 'django.contrib.admin' in your " raise ImproperlyConfigured("Put 'django.contrib.admin' in your "
"INSTALLED_APPS setting in order to use the admin application.") "INSTALLED_APPS setting in order to use the admin application.")

View File

@ -1,4 +1,3 @@
from django.apps import apps
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.db.models.fields import FieldDoesNotExist from django.db.models.fields import FieldDoesNotExist
@ -15,10 +14,6 @@ __all__ = ['BaseValidator', 'InlineValidator']
class BaseValidator(object): class BaseValidator(object):
def __init__(self):
# Before we can introspect models, they need the app registry to be
# fully loaded so that inter-relations are set up correctly.
apps.populate_models()
def validate(self, cls, model): def validate(self, cls, model):
for m in dir(self): for m in dir(self):

View File

@ -3,7 +3,6 @@ Module for abstract serializer/unserializer base classes.
""" """
import warnings import warnings
from django.apps import apps
from django.db import models from django.db import models
from django.utils import six from django.utils import six
@ -137,9 +136,6 @@ class Deserializer(six.Iterator):
self.stream = six.StringIO(stream_or_string) self.stream = six.StringIO(stream_or_string)
else: else:
self.stream = stream_or_string self.stream = stream_or_string
# Make sure the app registy is loaded before deserialization starts
# (otherwise subclass calls to get_model() and friends might fail...)
apps.populate_models()
def __iter__(self): def __iter__(self):
return self return self

View File

@ -88,8 +88,6 @@ def Deserializer(object_list, **options):
db = options.pop('using', DEFAULT_DB_ALIAS) db = options.pop('using', DEFAULT_DB_ALIAS)
ignore = options.pop('ignorenonexistent', False) ignore = options.pop('ignorenonexistent', False)
apps.populate_models()
for d in object_list: for d in object_list:
# Look up the model and starting build a dict of data for it. # Look up the model and starting build a dict of data for it.
Model = _get_model(d["model"]) Model = _get_model(d["model"])

View File

@ -1,5 +1,4 @@
from django.apps import apps import django
from django.conf import settings
from django.core.handlers.wsgi import WSGIHandler from django.core.handlers.wsgi import WSGIHandler
@ -12,9 +11,5 @@ def get_wsgi_application():
case the internal WSGI implementation changes or moves in the future. case the internal WSGI implementation changes or moves in the future.
""" """
# Configure the settings (this happens automatically on the first access). django.setup()
# Populate the app registry.
apps.populate_apps(settings.INSTALLED_APPS)
apps.populate_models()
return WSGIHandler() return WSGIHandler()

View File

@ -30,6 +30,5 @@ def get_app_errors():
try: try:
return apps.app_errors return apps.app_errors
except AttributeError: except AttributeError:
apps.populate_models()
apps.app_errors = {} apps.app_errors = {}
return apps.app_errors return apps.app_errors

View File

@ -602,9 +602,19 @@ the Python import path to your :file:`mysite/settings.py` file.
.. admonition:: Bypassing manage.py .. admonition:: Bypassing manage.py
If you'd rather not use :file:`manage.py`, no problem. Just set the If you'd rather not use :file:`manage.py`, no problem. Just set the
``DJANGO_SETTINGS_MODULE`` environment variable to ``mysite.settings`` and :envvar:`DJANGO_SETTINGS_MODULE` environment variable to
run ``python`` from the same directory :file:`manage.py` is in (or ensure ``mysite.settings``, start a plain Python shell, and set up Django::
that directory is on the Python path, so that ``import mysite`` works).
>>> import django
>>> django.setup()
If this raises an :exc:`~exceptions.AttributeError`, you're probably using
a version of Django that doesn't match this tutorial version. You'll want
to either switch to the older tutorial or the newer Django version.
You must run ``python`` from the same directory :file:`manage.py` is in,
or ensure that directory is on the Python path, so that ``import mysite``
works.
For more information on all of this, see the :doc:`django-admin.py For more information on all of this, see the :doc:`django-admin.py
documentation </ref/django-admin>`. documentation </ref/django-admin>`.

View File

@ -14,6 +14,12 @@ two things for you before delegating to ``django-admin.py``:
* It sets the :envvar:`DJANGO_SETTINGS_MODULE` environment variable so that * It sets the :envvar:`DJANGO_SETTINGS_MODULE` environment variable so that
it points to your project's ``settings.py`` file. it points to your project's ``settings.py`` file.
* It calls ``django.setup()`` to initialize various internals of Django.
.. versionadded:: 1.7
``django.setup()`` didn't exist in previous versions of Django.
The ``django-admin.py`` script should be on your system path if you installed The ``django-admin.py`` script should be on your system path if you installed
Django via its ``setup.py`` utility. If it's not on your path, you can find it Django via its ``setup.py`` utility. If it's not on your path, you can find it
in ``site-packages/django/bin`` within your Python installation. Consider in ``site-packages/django/bin`` within your Python installation. Consider

View File

@ -613,6 +613,15 @@ Since :setting:`INSTALLED_APPS` now supports application configuration classes
in addition to application modules, you should review code that accesses this in addition to application modules, you should review code that accesses this
setting directly and use the app registry (:attr:`django.apps.apps`) instead. setting directly and use the app registry (:attr:`django.apps.apps`) instead.
If you're using Django in a plain Python script (not a management command) and
rely on the :envvar:`DJANGO_SETTINGS_MODULE` environment variable, you must
now explicitly initialize Django at the beginning of your script with::
>>> import django
>>> django.setup()
Otherwise, you will most likely encounter a :exc:`~exceptions.RuntimeError`.
The "app registry" that manages the list of installed applications doesn't The "app registry" that manages the list of installed applications doesn't
have the same features as the old "app cache". Even though the "app cache" was have the same features as the old "app cache". Even though the "app cache" was
a private API, obsolete methods and arguments will be removed after a standard a private API, obsolete methods and arguments will be removed after a standard

View File

@ -9,6 +9,7 @@ import sys
import tempfile import tempfile
import warnings import warnings
import django
from django import contrib from django import contrib
from django.utils._os import upath from django.utils._os import upath
from django.utils import six from django.utils import six
@ -85,7 +86,6 @@ def get_installed():
def setup(verbosity, test_labels): def setup(verbosity, test_labels):
import django
from django.apps import apps, AppConfig from django.apps import apps, AppConfig
from django.conf import settings from django.conf import settings
from django.test import TransactionTestCase, TestCase from django.test import TransactionTestCase, TestCase
@ -128,7 +128,7 @@ def setup(verbosity, test_labels):
# Load all the ALWAYS_INSTALLED_APPS. # Load all the ALWAYS_INSTALLED_APPS.
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.filterwarnings('ignore', 'django.contrib.comments is deprecated and will be removed before Django 1.8.', DeprecationWarning) warnings.filterwarnings('ignore', 'django.contrib.comments is deprecated and will be removed before Django 1.8.', DeprecationWarning)
apps.populate_models() django.setup()
# Load all the test model apps. # Load all the test model apps.
test_modules = get_test_modules() test_modules = get_test_modules()