summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAymeric Augustin <aymeric.augustin@m4x.org>2013-12-30 15:42:15 +0100
committerAymeric Augustin <aymeric.augustin@m4x.org>2013-12-30 22:11:17 +0100
commit80d74097b4bd7186ad99b6d41d0ed90347a39b21 (patch)
tree7b00c5113c311aaaeba4393701151e916ae7f9f0
parent7ed20e015335076fc98ad805eaf241f8a0d872d5 (diff)
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.
-rw-r--r--django/__init__.py9
-rw-r--r--django/apps/base.py7
-rw-r--r--django/apps/registry.py44
-rw-r--r--django/contrib/admin/sites.py1
-rw-r--r--django/contrib/admin/validation.py5
-rw-r--r--django/core/serializers/base.py4
-rw-r--r--django/core/serializers/python.py2
-rw-r--r--django/core/wsgi.py9
-rw-r--r--django/db/models/loading.py1
-rw-r--r--docs/intro/tutorial01.txt16
-rw-r--r--docs/ref/django-admin.txt6
-rw-r--r--docs/releases/1.7.txt9
-rwxr-xr-xtests/runtests.py4
13 files changed, 65 insertions, 52 deletions
diff --git a/django/__init__.py b/django/__init__.py
index 5b4034bfed..61413abe8e 100644
--- a/django/__init__.py
+++ b/django/__init__.py
@@ -6,3 +6,12 @@ def get_version(*args, **kwargs):
# Only import if it's actually called.
from django.utils.version import get_version
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()
diff --git a/django/apps/base.py b/django/apps/base.py
index eaf8993624..ae2abaae84 100644
--- a/django/apps/base.py
+++ b/django/apps/base.py
@@ -114,8 +114,6 @@ class AppConfig(object):
Returns the model with the given case-insensitive model_name.
Raises LookupError if no model exists with this name.
-
- This method assumes that apps.populate_models() has run.
"""
if self.models is None:
raise LookupError(
@@ -140,8 +138,6 @@ class AppConfig(object):
Set the corresponding keyword argument to True to include such models.
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():
if model._deferred and not include_deferred:
@@ -156,7 +152,8 @@ class AppConfig(object):
# Dictionary of models for this app, primarily maintained in the
# 'all_models' attribute of the Apps this AppConfig is attached to.
# 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
if module_has_submodule(self.module, MODELS_MODULE_NAME):
diff --git a/django/apps/registry.py b/django/apps/registry.py
index 35c4ac3cea..0e39068e1f 100644
--- a/django/apps/registry.py
+++ b/django/apps/registry.py
@@ -3,7 +3,6 @@ import os
import sys
import warnings
-from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils import lru_cache
from django.utils.module_loading import import_lock
@@ -79,8 +78,6 @@ class Apps(object):
# Application modules aren't expected to import anything, and
# especially not other application modules, even indirectly.
# Therefore we simply import them sequentially.
- if installed_apps is None:
- installed_apps = settings.INSTALLED_APPS
for entry in installed_apps:
if isinstance(entry, AppConfig):
app_config = entry
@@ -108,7 +105,9 @@ class Apps(object):
if self._models_loaded:
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
# example to reference related objects. As a consequence:
@@ -144,6 +143,15 @@ class Apps(object):
for app_config in self.get_app_configs():
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
def ready(self):
"""
@@ -161,11 +169,7 @@ class Apps(object):
If only_with_models_module in True (non-default), imports models and
considers only applications containing a models module.
"""
- if only_with_models_module:
- self.populate_models()
- else:
- self.populate_apps()
-
+ self.check_ready()
for app_config in self.app_configs.values():
if only_with_models_module and app_config.models_module is None:
continue
@@ -180,11 +184,7 @@ class Apps(object):
If only_with_models_module in True (non-default), imports models and
considers only applications containing a models module.
"""
- if only_with_models_module:
- self.populate_models()
- else:
- self.populate_apps()
-
+ self.check_ready()
app_config = self.app_configs.get(app_label)
if app_config is None:
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.
"""
- self.populate_models()
-
+ self.check_ready()
if app_mod:
warnings.warn(
"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
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())
def register_model(self, app_label, model):
@@ -328,7 +327,8 @@ class Apps(object):
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._apps_loaded, self._models_loaded))
+ self.check_ready()
+ self.stored_app_configs.append(self.app_configs)
self.app_configs = OrderedDict()
self.clear_cache()
self._apps_loaded = False
@@ -340,7 +340,9 @@ class Apps(object):
"""
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()
def clear_cache(self):
@@ -429,9 +431,7 @@ class Apps(object):
warnings.warn(
"[a.path for a in get_app_configs()] supersedes get_app_paths().",
PendingDeprecationWarning, stacklevel=2)
-
- self.populate_models()
-
+ self.check_ready()
app_paths = []
for app in self.get_apps():
app_paths.append(self._get_app_path(app))
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
index 0edeca210e..0626c5aa45 100644
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -160,7 +160,6 @@ class AdminSite(object):
The default implementation checks that admin and contenttypes apps are
installed, as well as the auth context processor.
"""
- apps.populate_apps()
if not apps.has_app('django.contrib.admin'):
raise ImproperlyConfigured("Put 'django.contrib.admin' in your "
"INSTALLED_APPS setting in order to use the admin application.")
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 23dba7c7bd..784bee6f8b 100644
--- a/django/contrib/admin/validation.py
+++ b/django/contrib/admin/validation.py
@@ -1,4 +1,3 @@
-from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.fields import FieldDoesNotExist
@@ -15,10 +14,6 @@ __all__ = ['BaseValidator', 'InlineValidator']
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):
for m in dir(self):
diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py
index 70bc651f82..e5c295131e 100644
--- a/django/core/serializers/base.py
+++ b/django/core/serializers/base.py
@@ -3,7 +3,6 @@ Module for abstract serializer/unserializer base classes.
"""
import warnings
-from django.apps import apps
from django.db import models
from django.utils import six
@@ -137,9 +136,6 @@ class Deserializer(six.Iterator):
self.stream = six.StringIO(stream_or_string)
else:
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):
return self
diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py
index 9a59f61d70..3d14a1b509 100644
--- a/django/core/serializers/python.py
+++ b/django/core/serializers/python.py
@@ -88,8 +88,6 @@ def Deserializer(object_list, **options):
db = options.pop('using', DEFAULT_DB_ALIAS)
ignore = options.pop('ignorenonexistent', False)
- apps.populate_models()
-
for d in object_list:
# Look up the model and starting build a dict of data for it.
Model = _get_model(d["model"])
diff --git a/django/core/wsgi.py b/django/core/wsgi.py
index d2dadee623..4ff41318d3 100644
--- a/django/core/wsgi.py
+++ b/django/core/wsgi.py
@@ -1,5 +1,4 @@
-from django.apps import apps
-from django.conf import settings
+import django
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.
"""
- # Configure the settings (this happens automatically on the first access).
- # Populate the app registry.
- apps.populate_apps(settings.INSTALLED_APPS)
- apps.populate_models()
-
+ django.setup()
return WSGIHandler()
diff --git a/django/db/models/loading.py b/django/db/models/loading.py
index 42fcce8551..6c4ee6bf68 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -30,6 +30,5 @@ def get_app_errors():
try:
return apps.app_errors
except AttributeError:
- apps.populate_models()
apps.app_errors = {}
return apps.app_errors
diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt
index 4382f21a0f..0bd3d0abaf 100644
--- a/docs/intro/tutorial01.txt
+++ b/docs/intro/tutorial01.txt
@@ -602,9 +602,19 @@ the Python import path to your :file:`mysite/settings.py` file.
.. admonition:: Bypassing manage.py
If you'd rather not use :file:`manage.py`, no problem. Just set the
- ``DJANGO_SETTINGS_MODULE`` environment variable to ``mysite.settings`` and
- 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).
+ :envvar:`DJANGO_SETTINGS_MODULE` environment variable to
+ ``mysite.settings``, start a plain Python shell, and set up Django::
+
+ >>> 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
documentation </ref/django-admin>`.
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 97923011f0..06f589ee63 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -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 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
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
diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt
index b4ee180c73..87fa419188 100644
--- a/docs/releases/1.7.txt
+++ b/docs/releases/1.7.txt
@@ -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
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
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
diff --git a/tests/runtests.py b/tests/runtests.py
index 4c917fbbed..e33533a083 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -9,6 +9,7 @@ import sys
import tempfile
import warnings
+import django
from django import contrib
from django.utils._os import upath
from django.utils import six
@@ -85,7 +86,6 @@ def get_installed():
def setup(verbosity, test_labels):
- import django
from django.apps import apps, AppConfig
from django.conf import settings
from django.test import TransactionTestCase, TestCase
@@ -128,7 +128,7 @@ def setup(verbosity, test_labels):
# 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)
- apps.populate_models()
+ django.setup()
# Load all the test model apps.
test_modules = get_test_modules()