Kaydet (Commit) 2ff93e02 authored tarafından Aymeric Augustin's avatar Aymeric Augustin

Fixed #21829 -- Added default AppConfigs.

Thanks Russell for the report, Marc for the initial patch, Carl for the
final review, and everyone who contributed to the design discussion.
üst 29ddae74
......@@ -61,13 +61,12 @@ class AppConfig(object):
Factory that creates an app config from an entry in INSTALLED_APPS.
"""
try:
# If import_module succeeds, entry is a path to an app module.
# If import_module succeeds, entry is a path to an app module,
# which may specify an app config class with default_app_config.
# Otherwise, entry is a path to an app config class or an error.
module = import_module(entry)
except ImportError:
# Avoid django.utils.module_loading.import_by_path because it
# masks errors -- it reraises ImportError as ImproperlyConfigured.
mod_path, _, cls_name = entry.rpartition('.')
# Raise the original exception when entry cannot be a path to an
......@@ -75,39 +74,51 @@ class AppConfig(object):
if not mod_path:
raise
mod = import_module(mod_path)
try:
cls = getattr(mod, cls_name)
except AttributeError:
# Emulate the error that "from <mod_path> import <cls_name>"
# would raise when <mod_path> exists but not <cls_name>, with
# more context (Python just says "cannot import name ...").
raise ImportError(
"cannot import name '%s' from '%s'" % (cls_name, mod_path))
# Check for obvious errors. (This check prevents duck typing, but
# it could be removed if it became a problem in practice.)
if not issubclass(cls, AppConfig):
raise ImproperlyConfigured(
"'%s' isn't a subclass of AppConfig." % entry)
# Obtain app name here rather than in AppClass.__init__ to keep
# all error checking for entries in INSTALLED_APPS in one place.
else:
try:
app_name = cls.name
# If this works, the app module specifies an app config class.
entry = module.default_app_config
except AttributeError:
raise ImproperlyConfigured(
"'%s' must supply a name attribute." % entry)
# Otherwise, it simply uses the default app config class.
return cls(entry, module)
else:
mod_path, _, cls_name = entry.rpartition('.')
# If we're reaching this point, we must load the app config class
# located at <mod_path>.<cls_name>.
# Ensure app_name points to a valid module.
app_module = import_module(app_name)
# Avoid django.utils.module_loading.import_by_path because it
# masks errors -- it reraises ImportError as ImproperlyConfigured.
mod = import_module(mod_path)
try:
cls = getattr(mod, cls_name)
except AttributeError:
# Emulate the error that "from <mod_path> import <cls_name>"
# would raise when <mod_path> exists but not <cls_name>, with
# more context (Python just says "cannot import name ...").
raise ImportError(
"cannot import name '%s' from '%s'" % (cls_name, mod_path))
# Check for obvious errors. (This check prevents duck typing, but
# it could be removed if it became a problem in practice.)
if not issubclass(cls, AppConfig):
raise ImproperlyConfigured(
"'%s' isn't a subclass of AppConfig." % entry)
# Obtain app name here rather than in AppClass.__init__ to keep
# all error checking for entries in INSTALLED_APPS in one place.
try:
app_name = cls.name
except AttributeError:
raise ImproperlyConfigured(
"'%s' must supply a name attribute." % entry)
# Entry is a path to an app config class.
return cls(app_name, app_module)
# Ensure app_name points to a valid module.
app_module = import_module(app_name)
# Entry is a path to an app config class.
return cls(app_name, app_module)
else:
# Entry is a path to an app module.
return cls(entry, module)
def get_model(self, model_name):
"""
......
......@@ -30,7 +30,7 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin.apps.AdminConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
......
# ACTION_CHECKBOX_NAME is unused, but should stay since its import from here
# has been referenced in documentation.
from django.contrib.admin.checks import check_admin_app
from django.contrib.admin.decorators import register
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
......@@ -9,7 +8,6 @@ from django.contrib.admin.filters import (ListFilter, SimpleListFilter,
FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter,
ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter)
from django.contrib.admin.sites import AdminSite, site
from django.core import checks
from django.utils.module_loading import autodiscover_modules
__all__ = [
......@@ -24,4 +22,5 @@ __all__ = [
def autodiscover():
autodiscover_modules('admin', register_to=site)
checks.register('admin')(check_admin_app)
default_app_config = 'django.contrib.admin.apps.AdminConfig'
from django.apps import AppConfig
from django.core import checks
from django.contrib.admin.checks import check_admin_app
from django.utils.translation import ugettext_lazy as _
class AdminConfig(AppConfig):
class SimpleAdminConfig(AppConfig):
"""Simple AppConfig which does not do automatic discovery."""
name = 'django.contrib.admin'
verbose_name = _("administration")
def ready(self):
checks.register('admin')(check_admin_app)
class AdminConfig(SimpleAdminConfig):
"""The default AppConfig for admin which does autodiscovery."""
def ready(self):
super(AdminConfig, self).ready()
self.module.autodiscover()
......@@ -161,7 +161,7 @@ class AdminSite(object):
installed, as well as the auth context processor.
"""
if not apps.is_installed('django.contrib.admin'):
raise ImproperlyConfigured("Put 'django.contrib.admin.apps.AdminConfig' in "
raise ImproperlyConfigured("Put 'django.contrib.admin' in "
"your INSTALLED_APPS setting in order to use the admin application.")
if not apps.is_installed('django.contrib.contenttypes'):
raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in "
......
default_app_config = 'django.contrib.admindocs.apps.AdminDocsConfig'
......@@ -2,8 +2,6 @@ import inspect
import re
from django.conf import settings
from django.contrib.auth.checks import check_user_model
from django.core import checks
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.utils.module_loading import import_by_path
from django.middleware.csrf import rotate_token
......@@ -15,10 +13,6 @@ BACKEND_SESSION_KEY = '_auth_user_backend'
REDIRECT_FIELD_NAME = 'next'
# Register the user model checks
checks.register('models')(check_user_model)
def load_backend(path):
return import_by_path(path)()
......@@ -164,3 +158,6 @@ def get_permission_codename(action, opts):
Returns the codename of the permission for the specified action.
"""
return '%s_%s' % (action, opts.model_name)
default_app_config = 'django.contrib.auth.apps.AuthConfig'
from django.apps import AppConfig
from django.core import checks
from django.contrib.auth.checks import check_user_model
from django.utils.translation import ugettext_lazy as _
......@@ -6,3 +8,6 @@ from django.utils.translation import ugettext_lazy as _
class AuthConfig(AppConfig):
name = 'django.contrib.auth'
verbose_name = _("authentication and authorization")
def ready(self):
checks.register('models')(check_user_model)
from importlib import import_module
import warnings
from django.apps import apps
from django.apps import apps as django_apps
from django.conf import settings
from django.core import urlresolvers
from django.core.exceptions import ImproperlyConfigured
......@@ -16,7 +16,7 @@ def get_comment_app():
Get the comment app (i.e. "django.contrib.comments") as defined in the settings
"""
try:
app_config = apps.get_app_config(get_comment_app_name().rpartition(".")[2])
app_config = django_apps.get_app_config(get_comment_app_name().rpartition(".")[2])
except LookupError:
raise ImproperlyConfigured("The COMMENTS_APP (%r) "
"must be in INSTALLED_APPS" % settings.COMMENTS_APP)
......@@ -85,3 +85,6 @@ def get_approve_url(comment):
else:
return urlresolvers.reverse("django.contrib.comments.views.moderation.approve",
args=(comment.id,))
default_app_config = 'django.contrib.comments.apps.CommentsConfig'
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib.contenttypes.checks import check_generic_foreign_keys
from django.core import checks
checks.register('models')(check_generic_foreign_keys)
default_app_config = 'django.contrib.contenttypes.apps.ContentTypesConfig'
from django.apps import AppConfig
from django.contrib.contenttypes.checks import check_generic_foreign_keys
from django.core import checks
from django.utils.translation import ugettext_lazy as _
class ContentTypesConfig(AppConfig):
name = 'django.contrib.contenttypes'
verbose_name = _("content types")
def ready(self):
checks.register('models')(check_generic_foreign_keys)
default_app_config = 'django.contrib.flatpages.apps.FlatPagesConfig'
default_app_config = 'django.contrib.formtools.apps.FormToolsConfig'
......@@ -4,3 +4,6 @@ if six.PY3:
memoryview = memoryview
else:
memoryview = buffer
default_app_config = 'django.contrib.gis.apps.GISConfig'
default_app_config = 'django.contrib.humanize.apps.HumanizeConfig'
from django.contrib.messages.api import * # NOQA
from django.contrib.messages.constants import * # NOQA
default_app_config = 'django.contrib.messages.apps.MessagesConfig'
default_app_config = 'django.contrib.redirects.apps.RedirectsConfig'
default_app_config = 'django.contrib.sessions.apps.SessionsConfig'
......@@ -133,3 +133,6 @@ class GenericSitemap(Sitemap):
if self.date_field is not None:
return getattr(item, self.date_field)
return None
default_app_config = 'django.contrib.sitemaps.apps.SiteMapsConfig'
default_app_config = 'django.contrib.sites.apps.SitesConfig'
default_app_config = 'django.contrib.staticfiles.apps.StaticFilesConfig'
default_app_config = 'django.contrib.syndication.apps.SyndicationConfig'
default_app_config = 'django.contrib.webdesign.apps.WebDesignConfig'
......@@ -29,7 +29,8 @@ class CheckRegistry(object):
def inner(check):
check.tags = tags
self.registered_checks.append(check)
if check not in self.registered_checks:
self.registered_checks.append(check)
return check
return inner
......
......@@ -435,7 +435,7 @@ look like this:
:filename: mysite/settings.py
INSTALLED_APPS = (
'django.contrib.admin.apps.AdminConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
......@@ -444,13 +444,6 @@ look like this:
'polls',
)
.. admonition:: Doesn't match what you see?
If you're seeing ``'django.contrib.admin'`` instead of
``'django.contrib.admin.apps.AdminConfig'``, 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.
Now Django knows to include the ``polls`` app. Let's run another command:
.. code-block:: bash
......
......@@ -49,9 +49,15 @@ Configuring applications
To configure an application, subclass :class:`~django.apps.AppConfig` and put
the dotted path to that subclass in :setting:`INSTALLED_APPS`.
Django uses the default :class:`~django.apps.AppConfig` class when
:setting:`INSTALLED_APPS` simply contains the dotted path to an application
module.
When :setting:`INSTALLED_APPS` simply contains the dotted path to an
application module, Django checks for a ``default_app_config`` variable in
that module.
If it's defined, it's the dotted path to the :class:`~django.apps.AppConfig`
subclass for that application.
If there is no ``default_app_config``, Django uses the base
:class:`~django.apps.AppConfig` class.
For application authors
-----------------------
......@@ -67,8 +73,23 @@ would provide a proper name for the admin::
name = 'rock_n_roll'
verbose_name = "Rock ’n’ roll"
You would then tell your users to add ``'rock_n_roll.apps.RockNRollConfig'``
to their :setting:`INSTALLED_APPS`.
You can make your application load this :class:`~django.apps.AppConfig`
subclass by default as follows::
# rock_n_roll/__init__.py
default_app_config = 'rock_n_roll.apps.RockNRollConfig'
That will cause ``RockNRollConfig`` to be used when :setting:`INSTALLED_APPS`
just contains ``'rock_n_roll'``. This allows you to make use of
:class:`~django.apps.AppConfig` features without requiring your users to
update their :setting:`INSTALLED_APPS` setting.
Of course, you can also tell your users to put
``'rock_n_roll.apps.RockNRollConfig'`` in their :setting:`INSTALLED_APPS`
setting. You can even provide several different
:class:`~django.apps.AppConfig` subclasses with different behaviors and allow
your users to choose one via their :setting:`INSTALLED_APPS` setting.
The recommended convention is to put the configuration class in a submodule of
the application called ``apps``. However, this isn't enforced by Django.
......@@ -87,7 +108,7 @@ configuration::
# anthology/apps.py
from rock_n_roll.app import RockNRollConfig
from rock_n_roll.apps import RockNRollConfig
class GypsyJazzConfig(RockNRollConfig):
verbose_name = "Gypsy jazz"
......@@ -213,7 +234,7 @@ Application registry
.. method:: apps.is_installed(app_name)
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'.
``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
......
......@@ -23,12 +23,7 @@ The admin is enabled in the default project template used by
For reference, here are the requirements:
1. Add ``'django.contrib.admin.apps.AdminConfig'`` to your
:setting:`INSTALLED_APPS` setting.
.. versionchanged:: 1.7
:setting:`INSTALLED_APPS` used to contain ``'django.contrib.admin'``.
1. Add ``'django.contrib.admin'`` to your :setting:`INSTALLED_APPS` setting.
2. The admin has four dependencies - :mod:`django.contrib.auth`,
:mod:`django.contrib.contenttypes`,
......@@ -136,16 +131,23 @@ The register decorator
Discovery of admin files
------------------------
The admin needs to be instructed to look for ``admin.py`` files in your project.
The easiest solution is to put ``'django.contrib.admin.apps.AdminConfig'`` in
your :setting:`INSTALLED_APPS` setting.
When you put ``'django.contrib.admin'`` in your :setting:`INSTALLED_APPS`
setting, Django automatically looks for an ``admin`` module in each
application and imports it.
.. class:: apps.AdminConfig
.. versionadded:: 1.7
This class calls :func:`~django.contrib.admin.autodiscover()` when Django
starts.
This is the default :class:`~django.apps.AppConfig` class for the admin.
It calls :func:`~django.contrib.admin.autodiscover()` when Django starts.
.. class:: apps.SimpleAdminConfig
.. versionadded:: 1.7
This class works like :class:`~django.contrib.admin.apps.AdminConfig`,
except it doesn't call :func:`~django.contrib.admin.autodiscover()`.
.. function:: autodiscover
......@@ -155,13 +157,23 @@ your :setting:`INSTALLED_APPS` setting.
.. versionchanged:: 1.7
Previous versions of Django recommended calling this function directly
in the URLconf. :class:`~django.contrib.admin.apps.AdminConfig`
replaces that mechanism and is more robust.
in the URLconf. As of Django 1.7 this isn't needed anymore.
:class:`~django.contrib.admin.apps.AdminConfig` takes care of running
the auto-discovery automatically.
If you are using a custom ``AdminSite``, it is common to import all of the
``ModelAdmin`` subclasses into your code and register them to the custom
``AdminSite``. In that case, simply put ``'django.contrib.admin'`` in your
:setting:`INSTALLED_APPS` setting, as you don't need autodiscovery.
``AdminSite``. In that case, in order to disable auto-discovery, you should
put ``'django.contrib.admin.apps.SimpleAdminConfig'`` instead of
``'django.contrib.admin'`` in your :setting:`INSTALLED_APPS` setting.
.. versionchanged:: 1.7
In previous versions, the admin needed to be instructed to look for
``admin.py`` files with :func:`~django.contrib.admin.autodiscover()`.
As of Django 1.7, auto-discovery is enabled by default and must be
explicitly disabled when it's undesirable.
``ModelAdmin`` options
----------------------
......@@ -2426,11 +2438,12 @@ In this example, we register the ``AdminSite`` instance
(r'^myadmin/', include(admin_site.urls)),
)
Note that you don't need autodiscovery of ``admin`` modules when using your
Note that you may not want autodiscovery of ``admin`` modules when using your
own ``AdminSite`` instance since you will likely be importing all the per-app
``admin`` modules in your ``myproject.admin`` module. This means you likely do
not need ``'django.contrib.admin.app.AdminConfig'`` in your
:setting:`INSTALLED_APPS` and can just use ``'django.contrib.admin'``.
``admin`` modules in your ``myproject.admin`` module. This means you need to
put ``'django.contrib.admin.app.SimpleAdminConfig'`` instead of
``'django.contrib.admin'`` in your :setting:`INSTALLED_APPS` setting.
Multiple admin sites in the same URLconf
----------------------------------------
......
......@@ -115,7 +115,7 @@ In addition, modify the :setting:`INSTALLED_APPS` setting to include
and ``world`` (your newly created application)::
INSTALLED_APPS = (
'django.contrib.admin.apps.AdminConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
......
......@@ -89,17 +89,14 @@ Improvements thus far include:
* The name of applications can be customized in the admin with the
:attr:`~django.apps.AppConfig.verbose_name` of application configurations.
* The admin automatically calls :func:`~django.contrib.admin.autodiscover()`
when Django starts. You can consequently remove this line from your
URLconf.
* Django imports all application configurations and models as soon as it
starts, through a deterministic and straightforward process. This should
make it easier to diagnose import issues such as import loops.
* The admin has an :class:`~django.contrib.admin.apps.AdminConfig` application
configuration class. When Django starts, this class takes care of calling
:func:`~django.contrib.admin.autodiscover()`. To use it, simply replace
``'django.contrib.admin'`` in :setting:`INSTALLED_APPS` with
``'django.contrib.admin.apps.AdminConfig'`` and remove
``admin.autodiscover()`` from your URLconf.
New method on Field subclasses
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
......@@ -698,6 +695,12 @@ regressions cannot be ruled out. You may encounter the following exceptions:
results. The code will be executed when you first need its results. This
concept is known as "lazy evaluation".
* ``django.contrib.admin`` will now automatically perform autodiscovery of
``admin`` modules in installed applications. To prevent it, change your
:setting:`INSTALLED_APPS` to contain
``'django.contrib.admin.apps.SimpleAdminConfig'`` instead of
``'django.contrib.admin'``.
Standalone scripts
^^^^^^^^^^^^^^^^^^
......
......@@ -67,9 +67,8 @@ Creating superusers
-------------------
:djadmin:`manage.py migrate <migrate>` prompts you to create a superuser the
first time you run it with ``'django.contrib.auth'`` in your
:setting:`INSTALLED_APPS`. If you need to create a superuser at a later date,
you can use a command line utility::
first time you run it with ``'django.contrib.auth'`` installed. If you need to
create a superuser at a later date, you can use a command line utility::
$ python manage.py createsuperuser --username=joe --email=joe@example.com
......
......@@ -1113,7 +1113,7 @@ class ManageCheck(AdminScriptTestCase):
apps=[
'admin_scripts.complex_app',
'admin_scripts.simple_app',
'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
],
......
default_app_config = 'apps.default_config_app.apps.CustomConfig'
from django.apps import AppConfig
class CustomConfig(AppConfig):
name = 'apps.default_config_app'
......@@ -7,6 +7,7 @@ from django.db import models
from django.test import TestCase, override_settings
from django.utils import six
from .default_config_app.apps import CustomConfig
from .models import TotallyNormal, SoAlternative, new_apps
......@@ -82,6 +83,11 @@ class AppsTests(TestCase):
with self.settings(INSTALLED_APPS=['apps.apps.NoSuchConfig']):
pass
def test_default_app_config(self):
with self.settings(INSTALLED_APPS=['apps.default_config_app']):
config = apps.get_app_config('default_config_app')
self.assertIsInstance(config, CustomConfig)
@override_settings(INSTALLED_APPS=SOME_INSTALLED_APPS)
def test_get_app_configs(self):
"""
......
......@@ -1049,7 +1049,7 @@ class AppResolutionOrderI18NTests(ResolutionOrderI18NTests):
# Doesn't work because it's added later in the list.
self.assertUgettext('Date/time', 'Datum/Zeit')
with self.modify_settings(INSTALLED_APPS={'remove': 'django.contrib.admin'}):
with self.modify_settings(INSTALLED_APPS={'remove': 'django.contrib.admin.apps.SimpleAdminConfig'}):
self.flush_caches()
activate('de')
......
......@@ -44,7 +44,7 @@ ALWAYS_INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.comments',
'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.admindocs',
'django.contrib.staticfiles',
'django.contrib.humanize',
......@@ -168,12 +168,11 @@ def setup(verbosity, test_labels):
for label in test_labels_set)
installed_app_names = set(get_installed())
if module_found_in_labels:
if module_found_in_labels and module_label not in installed_app_names:
if verbosity >= 2:
print("Importing application %s" % module_name)
# HACK.
if module_label not in installed_app_names:
settings.INSTALLED_APPS.append(module_label)
settings.INSTALLED_APPS.append(module_label)
app_config = AppConfig.create(module_label)
apps.app_configs[app_config.label] = app_config
app_config.import_models(apps.all_models[app_config.label])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment