From 80d74097b4bd7186ad99b6d41d0ed90347a39b21 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 30 Dec 2013 15:42:15 +0100 Subject: [PATCH] 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. --- django/__init__.py | 9 ++++++ django/apps/base.py | 7 ++--- django/apps/registry.py | 44 +++++++++++++++--------------- django/contrib/admin/sites.py | 1 - django/contrib/admin/validation.py | 5 ---- django/core/serializers/base.py | 4 --- django/core/serializers/python.py | 2 -- django/core/wsgi.py | 9 ++---- django/db/models/loading.py | 1 - docs/intro/tutorial01.txt | 16 +++++++++-- docs/ref/django-admin.txt | 6 ++++ docs/releases/1.7.txt | 9 ++++++ tests/runtests.py | 4 +-- 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 `. 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()