mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Fixed #31180 -- Configured applications automatically.
This commit is contained in:
parent
6ec5eb5d74
commit
3f2821af6b
@ -1,9 +1,13 @@
|
|||||||
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
import warnings
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.utils.module_loading import module_has_submodule
|
from django.utils.deprecation import RemovedInDjango41Warning
|
||||||
|
from django.utils.module_loading import import_string, module_has_submodule
|
||||||
|
|
||||||
|
APPS_MODULE_NAME = 'apps'
|
||||||
MODELS_MODULE_NAME = 'models'
|
MODELS_MODULE_NAME = 'models'
|
||||||
|
|
||||||
|
|
||||||
@ -83,73 +87,139 @@ class AppConfig:
|
|||||||
"""
|
"""
|
||||||
Factory that creates an app config from an entry in INSTALLED_APPS.
|
Factory that creates an app config from an entry in INSTALLED_APPS.
|
||||||
"""
|
"""
|
||||||
|
# create() eventually returns app_config_class(app_name, app_module).
|
||||||
|
app_config_class = None
|
||||||
|
app_name = None
|
||||||
|
app_module = None
|
||||||
|
|
||||||
|
# If import_module succeeds, entry points to the app module.
|
||||||
try:
|
try:
|
||||||
# If import_module succeeds, entry is a path to an app module,
|
app_module = import_module(entry)
|
||||||
# which may specify an app config class with default_app_config.
|
except Exception:
|
||||||
# Otherwise, entry is a path to an app config class or an error.
|
pass
|
||||||
module = import_module(entry)
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# Track that importing as an app module failed. If importing as an
|
|
||||||
# app config class fails too, we'll trigger the ImportError again.
|
|
||||||
module = None
|
|
||||||
|
|
||||||
mod_path, _, cls_name = entry.rpartition('.')
|
|
||||||
|
|
||||||
# Raise the original exception when entry cannot be a path to an
|
|
||||||
# app config class.
|
|
||||||
if not mod_path:
|
|
||||||
raise
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
# If app_module has an apps submodule that defines a single
|
||||||
# If this works, the app module specifies an app config class.
|
# AppConfig subclass, use it automatically.
|
||||||
entry = module.default_app_config
|
# To prevent this, an AppConfig subclass can declare a class
|
||||||
except AttributeError:
|
# variable default = False.
|
||||||
# Otherwise, it simply uses the default app config class.
|
# If the apps module defines more than one AppConfig subclass,
|
||||||
return cls(entry, module)
|
# the default one can declare default = True.
|
||||||
else:
|
if module_has_submodule(app_module, APPS_MODULE_NAME):
|
||||||
mod_path, _, cls_name = entry.rpartition('.')
|
mod_path = '%s.%s' % (entry, APPS_MODULE_NAME)
|
||||||
|
mod = import_module(mod_path)
|
||||||
# If we're reaching this point, we must attempt to load the app config
|
# Check if there's exactly one AppConfig candidate,
|
||||||
# class located at <mod_path>.<cls_name>
|
# excluding those that explicitly define default = False.
|
||||||
mod = import_module(mod_path)
|
app_configs = [
|
||||||
try:
|
(name, candidate)
|
||||||
cls = getattr(mod, cls_name)
|
for name, candidate in inspect.getmembers(mod, inspect.isclass)
|
||||||
except AttributeError:
|
if (
|
||||||
if module is None:
|
issubclass(candidate, cls) and
|
||||||
# If importing as an app module failed, check if the module
|
candidate is not cls and
|
||||||
# contains any valid AppConfigs and show them as choices.
|
getattr(candidate, 'default', True)
|
||||||
# Otherwise, that error probably contains the most informative
|
|
||||||
# traceback, so trigger it again.
|
|
||||||
candidates = sorted(
|
|
||||||
repr(name) for name, candidate in mod.__dict__.items()
|
|
||||||
if isinstance(candidate, type) and
|
|
||||||
issubclass(candidate, AppConfig) and
|
|
||||||
candidate is not AppConfig
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
"'%s' does not contain a class '%s'. Choices are: %s."
|
|
||||||
% (mod_path, cls_name, ', '.join(candidates))
|
|
||||||
)
|
)
|
||||||
import_module(entry)
|
]
|
||||||
|
if len(app_configs) == 1:
|
||||||
|
app_config_class = app_configs[0][1]
|
||||||
|
app_config_name = '%s.%s' % (mod_path, app_configs[0][0])
|
||||||
|
else:
|
||||||
|
# Check if there's exactly one AppConfig subclass,
|
||||||
|
# among those that explicitly define default = True.
|
||||||
|
app_configs = [
|
||||||
|
(name, candidate)
|
||||||
|
for name, candidate in app_configs
|
||||||
|
if getattr(candidate, 'default', False)
|
||||||
|
]
|
||||||
|
if len(app_configs) > 1:
|
||||||
|
candidates = [repr(name) for name, _ in app_configs]
|
||||||
|
raise RuntimeError(
|
||||||
|
'%r declares more than one default AppConfig: '
|
||||||
|
'%s.' % (mod_path, ', '.join(candidates))
|
||||||
|
)
|
||||||
|
elif len(app_configs) == 1:
|
||||||
|
app_config_class = app_configs[0][1]
|
||||||
|
app_config_name = '%s.%s' % (mod_path, app_configs[0][0])
|
||||||
|
|
||||||
|
# If app_module specifies a default_app_config, follow the link.
|
||||||
|
# default_app_config is deprecated, but still takes over the
|
||||||
|
# automatic detection for backwards compatibility during the
|
||||||
|
# deprecation period.
|
||||||
|
try:
|
||||||
|
new_entry = app_module.default_app_config
|
||||||
|
except AttributeError:
|
||||||
|
# Use the default app config class if we didn't find anything.
|
||||||
|
if app_config_class is None:
|
||||||
|
app_config_class = cls
|
||||||
|
app_name = entry
|
||||||
else:
|
else:
|
||||||
raise
|
message = (
|
||||||
|
'%r defines default_app_config = %r. ' % (entry, new_entry)
|
||||||
|
)
|
||||||
|
if new_entry == app_config_name:
|
||||||
|
message += (
|
||||||
|
'Django now detects this configuration automatically. '
|
||||||
|
'You can remove default_app_config.'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message += (
|
||||||
|
"However, Django's automatic detection picked another "
|
||||||
|
"configuration, %r. You should move the default "
|
||||||
|
"config class to the apps submodule of your "
|
||||||
|
"application and, if this module defines several "
|
||||||
|
"config classes, mark the default one with default = "
|
||||||
|
"True." % app_config_name
|
||||||
|
)
|
||||||
|
warnings.warn(message, RemovedInDjango41Warning, stacklevel=2)
|
||||||
|
entry = new_entry
|
||||||
|
app_config_class = None
|
||||||
|
|
||||||
|
# If import_string succeeds, entry is an app config class.
|
||||||
|
if app_config_class is None:
|
||||||
|
try:
|
||||||
|
app_config_class = import_string(entry)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# If both import_module and import_string failed, it means that entry
|
||||||
|
# doesn't have a valid value.
|
||||||
|
if app_module is None and app_config_class is None:
|
||||||
|
# If the last component of entry starts with an uppercase letter,
|
||||||
|
# then it was likely intended to be an app config class; if not,
|
||||||
|
# an app module. Provide a nice error message in both cases.
|
||||||
|
mod_path, _, cls_name = entry.rpartition('.')
|
||||||
|
if mod_path and cls_name[0].isupper():
|
||||||
|
# We could simply re-trigger the string import exception, but
|
||||||
|
# we're going the extra mile and providing a better error
|
||||||
|
# message for typos in INSTALLED_APPS.
|
||||||
|
# This may raise ImportError, which is the best exception
|
||||||
|
# possible if the module at mod_path cannot be imported.
|
||||||
|
mod = import_module(mod_path)
|
||||||
|
candidates = [
|
||||||
|
repr(name)
|
||||||
|
for name, candidate in inspect.getmembers(mod, inspect.isclass)
|
||||||
|
if issubclass(candidate, cls) and candidate is not cls
|
||||||
|
]
|
||||||
|
msg = "Module '%s' does not contain a '%s' class." % (mod_path, cls_name)
|
||||||
|
if candidates:
|
||||||
|
msg += ' Choices are: %s.' % ', '.join(candidates)
|
||||||
|
raise ImportError(msg)
|
||||||
|
else:
|
||||||
|
# Re-trigger the module import exception.
|
||||||
|
import_module(entry)
|
||||||
|
|
||||||
# Check for obvious errors. (This check prevents duck typing, but
|
# Check for obvious errors. (This check prevents duck typing, but
|
||||||
# it could be removed if it became a problem in practice.)
|
# it could be removed if it became a problem in practice.)
|
||||||
if not issubclass(cls, AppConfig):
|
if not issubclass(app_config_class, AppConfig):
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"'%s' isn't a subclass of AppConfig." % entry)
|
"'%s' isn't a subclass of AppConfig." % entry)
|
||||||
|
|
||||||
# Obtain app name here rather than in AppClass.__init__ to keep
|
# Obtain app name here rather than in AppClass.__init__ to keep
|
||||||
# all error checking for entries in INSTALLED_APPS in one place.
|
# all error checking for entries in INSTALLED_APPS in one place.
|
||||||
try:
|
if app_name is None:
|
||||||
app_name = cls.name
|
try:
|
||||||
except AttributeError:
|
app_name = app_config_class.name
|
||||||
raise ImproperlyConfigured(
|
except AttributeError:
|
||||||
"'%s' must supply a name attribute." % entry)
|
raise ImproperlyConfigured(
|
||||||
|
"'%s' must supply a name attribute." % entry
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure app_name points to a valid module.
|
# Ensure app_name points to a valid module.
|
||||||
try:
|
try:
|
||||||
@ -157,12 +227,14 @@ class AppConfig:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"Cannot import '%s'. Check that '%s.%s.name' is correct." % (
|
"Cannot import '%s'. Check that '%s.%s.name' is correct." % (
|
||||||
app_name, mod_path, cls_name,
|
app_name,
|
||||||
|
app_config_class.__module__,
|
||||||
|
app_config_class.__qualname__,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Entry is a path to an app config class.
|
# Entry is a path to an app config class.
|
||||||
return cls(app_name, app_module)
|
return app_config_class(app_name, app_module)
|
||||||
|
|
||||||
def get_model(self, model_name, require_ready=True):
|
def get_model(self, model_name, require_ready=True):
|
||||||
"""
|
"""
|
||||||
|
@ -22,6 +22,3 @@ __all__ = [
|
|||||||
|
|
||||||
def autodiscover():
|
def autodiscover():
|
||||||
autodiscover_modules('admin', register_to=site)
|
autodiscover_modules('admin', register_to=site)
|
||||||
|
|
||||||
|
|
||||||
default_app_config = 'django.contrib.admin.apps.AdminConfig'
|
|
||||||
|
@ -19,6 +19,8 @@ class SimpleAdminConfig(AppConfig):
|
|||||||
class AdminConfig(SimpleAdminConfig):
|
class AdminConfig(SimpleAdminConfig):
|
||||||
"""The default AppConfig for admin which does autodiscovery."""
|
"""The default AppConfig for admin which does autodiscovery."""
|
||||||
|
|
||||||
|
default = True
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
super().ready()
|
super().ready()
|
||||||
self.module.autodiscover()
|
self.module.autodiscover()
|
||||||
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.admindocs.apps.AdminDocsConfig'
|
|
@ -217,6 +217,3 @@ def update_session_auth_hash(request, user):
|
|||||||
request.session.cycle_key()
|
request.session.cycle_key()
|
||||||
if hasattr(user, 'get_session_auth_hash') and request.user == user:
|
if hasattr(user, 'get_session_auth_hash') and request.user == user:
|
||||||
request.session[HASH_SESSION_KEY] = user.get_session_auth_hash()
|
request.session[HASH_SESSION_KEY] = user.get_session_auth_hash()
|
||||||
|
|
||||||
|
|
||||||
default_app_config = 'django.contrib.auth.apps.AuthConfig'
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.contenttypes.apps.ContentTypesConfig'
|
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.flatpages.apps.FlatPagesConfig'
|
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.gis.apps.GISConfig'
|
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.humanize.apps.HumanizeConfig'
|
|
@ -1,4 +1,2 @@
|
|||||||
from django.contrib.messages.api import * # NOQA
|
from django.contrib.messages.api import * # NOQA
|
||||||
from django.contrib.messages.constants import * # NOQA
|
from django.contrib.messages.constants import * # NOQA
|
||||||
|
|
||||||
default_app_config = 'django.contrib.messages.apps.MessagesConfig'
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.postgres.apps.PostgresConfig'
|
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.redirects.apps.RedirectsConfig'
|
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.sessions.apps.SessionsConfig'
|
|
@ -158,6 +158,3 @@ class GenericSitemap(Sitemap):
|
|||||||
if self.date_field is not None:
|
if self.date_field is not None:
|
||||||
return getattr(item, self.date_field)
|
return getattr(item, self.date_field)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
default_app_config = 'django.contrib.sitemaps.apps.SiteMapsConfig'
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.sites.apps.SitesConfig'
|
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.staticfiles.apps.StaticFilesConfig'
|
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'django.contrib.syndication.apps.SyndicationConfig'
|
|
@ -24,6 +24,8 @@ details on these changes.
|
|||||||
* The ``whitelist`` argument and ``domain_whitelist`` attribute of
|
* The ``whitelist`` argument and ``domain_whitelist`` attribute of
|
||||||
``django.core.validators.EmailValidator`` will be removed.
|
``django.core.validators.EmailValidator`` will be removed.
|
||||||
|
|
||||||
|
* The ``default_app_config`` module variable will be removed.
|
||||||
|
|
||||||
.. _deprecation-removed-in-4.0:
|
.. _deprecation-removed-in-4.0:
|
||||||
|
|
||||||
4.0
|
4.0
|
||||||
|
@ -56,25 +56,28 @@ application and have models, etc. (which would require adding it to
|
|||||||
Configuring applications
|
Configuring applications
|
||||||
========================
|
========================
|
||||||
|
|
||||||
To configure an application, subclass :class:`~django.apps.AppConfig` and put
|
To configure an application, create an ``apps.py`` module inside the
|
||||||
the dotted path to that subclass in :setting:`INSTALLED_APPS`.
|
application, then define a subclass of :class:`AppConfig` there.
|
||||||
|
|
||||||
When :setting:`INSTALLED_APPS` contains the dotted path to an application
|
When :setting:`INSTALLED_APPS` contains the dotted path to an application
|
||||||
module, Django checks for a ``default_app_config`` variable in that module.
|
module, by default, if Django finds exactly one :class:`AppConfig` subclass in
|
||||||
|
the ``apps.py`` submodule, it uses that configuration for the application. This
|
||||||
|
behavior may be disabled by setting :attr:`AppConfig.default` to ``False``.
|
||||||
|
|
||||||
If it's defined, it's the dotted path to the :class:`~django.apps.AppConfig`
|
If the ``apps.py`` module contains more than one :class:`AppConfig` subclass,
|
||||||
subclass for that application.
|
Django will look for a single one where :attr:`AppConfig.default` is ``True``.
|
||||||
|
|
||||||
If there is no ``default_app_config``, Django uses the base
|
If no :class:`AppConfig` subclass is found, the base :class:`AppConfig` class
|
||||||
:class:`~django.apps.AppConfig` class.
|
will be used.
|
||||||
|
|
||||||
``default_app_config`` allows applications that predate Django 1.7 such as
|
Alternatively, :setting:`INSTALLED_APPS` may contain the dotted path to a
|
||||||
``django.contrib.admin`` to opt-in to :class:`~django.apps.AppConfig` features
|
configuration class to specify it explicitly::
|
||||||
without requiring users to update their :setting:`INSTALLED_APPS`.
|
|
||||||
|
|
||||||
New applications should avoid ``default_app_config``. Instead they should
|
INSTALLED_APPS = [
|
||||||
require the dotted path to the appropriate :class:`~django.apps.AppConfig`
|
...
|
||||||
subclass to be configured explicitly in :setting:`INSTALLED_APPS`.
|
'polls.apps.PollsAppConfig',
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
For application authors
|
For application authors
|
||||||
-----------------------
|
-----------------------
|
||||||
@ -90,32 +93,24 @@ would provide a proper name for the admin::
|
|||||||
name = 'rock_n_roll'
|
name = 'rock_n_roll'
|
||||||
verbose_name = "Rock ’n’ roll"
|
verbose_name = "Rock ’n’ roll"
|
||||||
|
|
||||||
You can make your application load this :class:`~django.apps.AppConfig`
|
``RockNRollConfig`` will be loaded automatically when :setting:`INSTALLED_APPS`
|
||||||
subclass by default as follows::
|
contains ``'rock_n_roll'``. If you need to prevent this, set
|
||||||
|
:attr:`~AppConfig.default` to ``False`` in the class definition.
|
||||||
|
|
||||||
# rock_n_roll/__init__.py
|
You can provide several :class:`AppConfig` subclasses with different behaviors.
|
||||||
|
To tell Django which one to use by default, set :attr:`~AppConfig.default` to
|
||||||
|
``True`` in its definition. If your users want to pick a non-default
|
||||||
|
configuration, they must replace ``'rock_n_roll'`` with the dotted path to that
|
||||||
|
specific class in their :setting:`INSTALLED_APPS` setting.
|
||||||
|
|
||||||
default_app_config = 'rock_n_roll.apps.RockNRollConfig'
|
The :attr:`AppConfig.name` attribute tells Django which application this
|
||||||
|
configuration applies to. You can define any other attribute documented in the
|
||||||
|
:class:`~django.apps.AppConfig` API reference.
|
||||||
|
|
||||||
That will cause ``RockNRollConfig`` to be used when :setting:`INSTALLED_APPS`
|
:class:`AppConfig` subclasses may be defined anywhere. The ``apps.py``
|
||||||
contains ``'rock_n_roll'``. This allows you to make use of
|
convention merely allows Django to load them automatically when
|
||||||
:class:`~django.apps.AppConfig` features without requiring your users to update
|
:setting:`INSTALLED_APPS` contains the path to an application module rather
|
||||||
their :setting:`INSTALLED_APPS` setting. Besides this use case, it's best to
|
than the path to a configuration class.
|
||||||
avoid using ``default_app_config`` and instead specify the app config class in
|
|
||||||
:setting:`INSTALLED_APPS` as described next.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
You must include the :attr:`~django.apps.AppConfig.name` attribute for Django
|
|
||||||
to determine which application this configuration applies to. You can define
|
|
||||||
any attributes documented in the :class:`~django.apps.AppConfig` API
|
|
||||||
reference.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@ -126,6 +121,11 @@ reference.
|
|||||||
|
|
||||||
from django.apps import apps as django_apps
|
from django.apps import apps as django_apps
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
In previous versions, a ``default_app_config`` variable in the application
|
||||||
|
module was used to identify the default application configuration class.
|
||||||
|
|
||||||
For application users
|
For application users
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
@ -147,8 +147,13 @@ configuration::
|
|||||||
# ...
|
# ...
|
||||||
]
|
]
|
||||||
|
|
||||||
Again, defining project-specific configuration classes in a submodule called
|
This example shows project-specific configuration classes located in a
|
||||||
``apps`` is a convention, not a requirement.
|
submodule called ``apps.py``. This is a convention, not a requirement.
|
||||||
|
:class:`AppConfig` subclasses may be defined anywhere.
|
||||||
|
|
||||||
|
In this situation, :setting:`INSTALLED_APPS` must contain the dotted path to
|
||||||
|
the configuration class because it lives outside of an application and thus
|
||||||
|
cannot be automatically detected.
|
||||||
|
|
||||||
Application configuration
|
Application configuration
|
||||||
=========================
|
=========================
|
||||||
@ -198,6 +203,22 @@ Configurable attributes
|
|||||||
required; for instance if the app package is a `namespace package`_ with
|
required; for instance if the app package is a `namespace package`_ with
|
||||||
multiple paths.
|
multiple paths.
|
||||||
|
|
||||||
|
.. attribute:: AppConfig.default
|
||||||
|
|
||||||
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
Set this attribute to ``False`` to prevent Django from selecting a
|
||||||
|
configuration class automatically. This is useful when ``apps.py`` defines
|
||||||
|
only one :class:`AppConfig` subclass but you don't want Django to use it by
|
||||||
|
default.
|
||||||
|
|
||||||
|
Set this attribute to ``True`` to tell Django to select a configuration
|
||||||
|
class automatically. This is useful when ``apps.py`` defines more than one
|
||||||
|
:class:`AppConfig` subclass and you want Django to use one of them by
|
||||||
|
default.
|
||||||
|
|
||||||
|
By default, this attribute isn't set.
|
||||||
|
|
||||||
Read-only attributes
|
Read-only attributes
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@ -412,7 +433,8 @@ processes all applications in the order of :setting:`INSTALLED_APPS`.
|
|||||||
|
|
||||||
If it's an application configuration class, Django imports the root package
|
If it's an application configuration class, Django imports the root package
|
||||||
of the application, defined by its :attr:`~AppConfig.name` attribute. If
|
of the application, defined by its :attr:`~AppConfig.name` attribute. If
|
||||||
it's a Python package, Django creates a default application configuration.
|
it's a Python package, Django looks for an application configuration in an
|
||||||
|
``apps.py`` submodule, or else creates a default application configuration.
|
||||||
|
|
||||||
*At this stage, your code shouldn't import any models!*
|
*At this stage, your code shouldn't import any models!*
|
||||||
|
|
||||||
|
@ -31,6 +31,28 @@ officially support the latest release of each series.
|
|||||||
What's new in Django 3.2
|
What's new in Django 3.2
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
Automatic :class:`~django.apps.AppConfig` discovery
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
Most pluggable applications define an :class:`~django.apps.AppConfig` subclass
|
||||||
|
in an ``apps.py`` submodule. Many define a ``default_app_config`` variable
|
||||||
|
pointing to this class in their ``__init__.py``.
|
||||||
|
|
||||||
|
When the ``apps.py`` submodule exists and defines a single
|
||||||
|
:class:`~django.apps.AppConfig` subclass, Django now uses that configuration
|
||||||
|
automatically, so you can remove ``default_app_config``.
|
||||||
|
|
||||||
|
``default_app_config`` made it possible to declare only the application's path
|
||||||
|
in :setting:`INSTALLED_APPS` (e.g. ``'django.contrib.admin'``) rather than the
|
||||||
|
app config's path (e.g. ``'django.contrib.admin.apps.AdminConfig'``). It was
|
||||||
|
introduced for backwards-compatibility with the former style, with the intent
|
||||||
|
to switch the ecosystem to the latter, but the switch didn't happen.
|
||||||
|
|
||||||
|
With automatic ``AppConfig`` discovery, ``default_app_config`` is no longer
|
||||||
|
needed. As a consequence, it's deprecated.
|
||||||
|
|
||||||
|
See :ref:`configuring-applications-ref` for full details.
|
||||||
|
|
||||||
Minor features
|
Minor features
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
@ -387,6 +409,12 @@ Miscellaneous
|
|||||||
:attr:`~django.db.models.Model._default_manager` to check that related
|
:attr:`~django.db.models.Model._default_manager` to check that related
|
||||||
instances exist.
|
instances exist.
|
||||||
|
|
||||||
|
* When an application defines an :class:`~django.apps.AppConfig` subclass in
|
||||||
|
an ``apps.py`` submodule, Django now uses this configuration automatically,
|
||||||
|
even if it isn't enabled with ``default_app_config``. Set ``default = False``
|
||||||
|
in the :class:`~django.apps.AppConfig` subclass if you need to prevent this
|
||||||
|
behavior. See :ref:`whats-new-3.2` for more details.
|
||||||
|
|
||||||
.. _deprecated-features-3.2:
|
.. _deprecated-features-3.2:
|
||||||
|
|
||||||
Features deprecated in 3.2
|
Features deprecated in 3.2
|
||||||
@ -408,3 +436,7 @@ Miscellaneous
|
|||||||
``allowlist`` instead of ``whitelist``, and ``domain_allowlist`` instead of
|
``allowlist`` instead of ``whitelist``, and ``domain_allowlist`` instead of
|
||||||
``domain_whitelist``. You may need to rename ``whitelist`` in existing
|
``domain_whitelist``. You may need to rename ``whitelist`` in existing
|
||||||
migrations.
|
migrations.
|
||||||
|
|
||||||
|
* The ``default_app_config`` application configuration variable is deprecated,
|
||||||
|
due to the now automatic ``AppConfig`` discovery. See :ref:`whats-new-3.2`
|
||||||
|
for more details.
|
||||||
|
@ -1 +0,0 @@
|
|||||||
default_app_config = 'apps.default_config_app.apps.CustomConfig'
|
|
@ -1,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class CustomConfig(AppConfig):
|
|
||||||
name = 'apps.default_config_app'
|
|
1
tests/apps/explicit_default_config_app/__init__.py
Normal file
1
tests/apps/explicit_default_config_app/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'apps.explicit_default_config_app.apps.ExplicitDefaultConfig'
|
5
tests/apps/explicit_default_config_app/apps.py
Normal file
5
tests/apps/explicit_default_config_app/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ExplicitDefaultConfig(AppConfig):
|
||||||
|
name = 'apps.explicit_default_config_app'
|
@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'apps.explicit_default_config_mismatch_app.not_apps.ExplicitDefaultConfigMismatch'
|
5
tests/apps/explicit_default_config_mismatch_app/apps.py
Normal file
5
tests/apps/explicit_default_config_mismatch_app/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ImplicitDefaultConfigMismatch(AppConfig):
|
||||||
|
name = 'apps.explicit_default_config_mismatch_app'
|
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ExplicitDefaultConfigMismatch(AppConfig):
|
||||||
|
name = 'apps.explicit_default_config_mismatch_app'
|
@ -4,5 +4,6 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class NSAppConfig(AppConfig):
|
class NSAppConfig(AppConfig):
|
||||||
|
default = False
|
||||||
name = 'nsapp'
|
name = 'nsapp'
|
||||||
path = os.path.dirname(__file__)
|
path = os.path.dirname(__file__)
|
||||||
|
0
tests/apps/no_config_app/__init__.py
Normal file
0
tests/apps/no_config_app/__init__.py
Normal file
0
tests/apps/one_config_app/__init__.py
Normal file
0
tests/apps/one_config_app/__init__.py
Normal file
5
tests/apps/one_config_app/apps.py
Normal file
5
tests/apps/one_config_app/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class OneConfig(AppConfig):
|
||||||
|
name = 'apps.one_config_app'
|
@ -5,11 +5,17 @@ from django.apps.registry import Apps
|
|||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
|
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import SimpleTestCase, ignore_warnings, override_settings
|
||||||
from django.test.utils import extend_sys_path, isolate_apps
|
from django.test.utils import extend_sys_path, isolate_apps
|
||||||
|
from django.utils.deprecation import RemovedInDjango41Warning
|
||||||
|
|
||||||
from .default_config_app.apps import CustomConfig
|
from .explicit_default_config_app.apps import ExplicitDefaultConfig
|
||||||
|
from .explicit_default_config_mismatch_app.not_apps import (
|
||||||
|
ExplicitDefaultConfigMismatch,
|
||||||
|
)
|
||||||
from .models import SoAlternative, TotallyNormal, new_apps
|
from .models import SoAlternative, TotallyNormal, new_apps
|
||||||
|
from .one_config_app.apps import OneConfig
|
||||||
|
from .two_configs_one_default_app.apps import TwoConfig
|
||||||
|
|
||||||
# Small list with a variety of cases for tests that iterate on installed apps.
|
# Small list with a variety of cases for tests that iterate on installed apps.
|
||||||
# Intentionally not in alphabetical order to check if the order is preserved.
|
# Intentionally not in alphabetical order to check if the order is preserved.
|
||||||
@ -84,25 +90,56 @@ class AppsTests(SimpleTestCase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def test_no_such_app_config(self):
|
def test_no_such_app_config(self):
|
||||||
msg = "No module named 'apps.NoSuchConfig'"
|
msg = "Module 'apps' does not contain a 'NoSuchConfig' class."
|
||||||
with self.assertRaisesMessage(ImportError, msg):
|
with self.assertRaisesMessage(ImportError, msg):
|
||||||
with self.settings(INSTALLED_APPS=['apps.NoSuchConfig']):
|
with self.settings(INSTALLED_APPS=['apps.NoSuchConfig']):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_no_such_app_config_with_choices(self):
|
def test_no_such_app_config_with_choices(self):
|
||||||
msg = (
|
msg = (
|
||||||
"'apps.apps' does not contain a class 'NoSuchConfig'. Choices are: "
|
"Module 'apps.apps' does not contain a 'NoSuchConfig' class. "
|
||||||
"'BadConfig', 'MyAdmin', 'MyAuth', 'NoSuchApp', 'PlainAppsConfig', "
|
"Choices are: 'BadConfig', 'MyAdmin', 'MyAuth', 'NoSuchApp', "
|
||||||
"'RelabeledAppsConfig'."
|
"'PlainAppsConfig', 'RelabeledAppsConfig'."
|
||||||
)
|
)
|
||||||
with self.assertRaisesMessage(ImproperlyConfigured, msg):
|
with self.assertRaisesMessage(ImportError, msg):
|
||||||
with self.settings(INSTALLED_APPS=['apps.apps.NoSuchConfig']):
|
with self.settings(INSTALLED_APPS=['apps.apps.NoSuchConfig']):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_default_app_config(self):
|
def test_no_config_app(self):
|
||||||
with self.settings(INSTALLED_APPS=['apps.default_config_app']):
|
"""Load an app that doesn't provide an AppConfig class."""
|
||||||
config = apps.get_app_config('default_config_app')
|
with self.settings(INSTALLED_APPS=['apps.no_config_app']):
|
||||||
self.assertIsInstance(config, CustomConfig)
|
config = apps.get_app_config('no_config_app')
|
||||||
|
self.assertIsInstance(config, AppConfig)
|
||||||
|
|
||||||
|
def test_one_config_app(self):
|
||||||
|
"""Load an app that provides an AppConfig class."""
|
||||||
|
with self.settings(INSTALLED_APPS=['apps.one_config_app']):
|
||||||
|
config = apps.get_app_config('one_config_app')
|
||||||
|
self.assertIsInstance(config, OneConfig)
|
||||||
|
|
||||||
|
def test_two_configs_app(self):
|
||||||
|
"""Load an app that provides two AppConfig classes."""
|
||||||
|
with self.settings(INSTALLED_APPS=['apps.two_configs_app']):
|
||||||
|
config = apps.get_app_config('two_configs_app')
|
||||||
|
self.assertIsInstance(config, AppConfig)
|
||||||
|
|
||||||
|
def test_two_default_configs_app(self):
|
||||||
|
"""Load an app that provides two default AppConfig classes."""
|
||||||
|
msg = (
|
||||||
|
"'apps.two_default_configs_app.apps' declares more than one "
|
||||||
|
"default AppConfig: 'TwoConfig', 'TwoConfigBis'."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(RuntimeError, msg):
|
||||||
|
with self.settings(INSTALLED_APPS=['apps.two_default_configs_app']):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_two_configs_one_default_app(self):
|
||||||
|
"""
|
||||||
|
Load an app that provides two AppConfig classes, one being the default.
|
||||||
|
"""
|
||||||
|
with self.settings(INSTALLED_APPS=['apps.two_configs_one_default_app']):
|
||||||
|
config = apps.get_app_config('two_configs_one_default_app')
|
||||||
|
self.assertIsInstance(config, TwoConfig)
|
||||||
|
|
||||||
@override_settings(INSTALLED_APPS=SOME_INSTALLED_APPS)
|
@override_settings(INSTALLED_APPS=SOME_INSTALLED_APPS)
|
||||||
def test_get_app_configs(self):
|
def test_get_app_configs(self):
|
||||||
@ -438,3 +475,48 @@ class NamespacePackageAppTests(SimpleTestCase):
|
|||||||
with self.settings(INSTALLED_APPS=['nsapp.apps.NSAppConfig']):
|
with self.settings(INSTALLED_APPS=['nsapp.apps.NSAppConfig']):
|
||||||
app_config = apps.get_app_config('nsapp')
|
app_config = apps.get_app_config('nsapp')
|
||||||
self.assertEqual(app_config.path, self.app_path)
|
self.assertEqual(app_config.path, self.app_path)
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecationTests(SimpleTestCase):
|
||||||
|
@ignore_warnings(category=RemovedInDjango41Warning)
|
||||||
|
def test_explicit_default_app_config(self):
|
||||||
|
with self.settings(INSTALLED_APPS=['apps.explicit_default_config_app']):
|
||||||
|
config = apps.get_app_config('explicit_default_config_app')
|
||||||
|
self.assertIsInstance(config, ExplicitDefaultConfig)
|
||||||
|
|
||||||
|
def test_explicit_default_app_config_warning(self):
|
||||||
|
"""
|
||||||
|
Load an app that specifies a default AppConfig class matching the
|
||||||
|
autodetected one.
|
||||||
|
"""
|
||||||
|
msg = (
|
||||||
|
"'apps.explicit_default_config_app' defines default_app_config = "
|
||||||
|
"'apps.explicit_default_config_app.apps.ExplicitDefaultConfig'. "
|
||||||
|
"Django now detects this configuration automatically. You can "
|
||||||
|
"remove default_app_config."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(RemovedInDjango41Warning, msg):
|
||||||
|
with self.settings(INSTALLED_APPS=['apps.explicit_default_config_app']):
|
||||||
|
config = apps.get_app_config('explicit_default_config_app')
|
||||||
|
self.assertIsInstance(config, ExplicitDefaultConfig)
|
||||||
|
|
||||||
|
def test_explicit_default_app_config_mismatch(self):
|
||||||
|
"""
|
||||||
|
Load an app that specifies a default AppConfig class not matching the
|
||||||
|
autodetected one.
|
||||||
|
"""
|
||||||
|
msg = (
|
||||||
|
"'apps.explicit_default_config_mismatch_app' defines "
|
||||||
|
"default_app_config = 'apps.explicit_default_config_mismatch_app."
|
||||||
|
"not_apps.ExplicitDefaultConfigMismatch'. However, Django's "
|
||||||
|
"automatic detection picked another configuration, 'apps."
|
||||||
|
"explicit_default_config_mismatch_app.apps."
|
||||||
|
"ImplicitDefaultConfigMismatch'. You should move the default "
|
||||||
|
"config class to the apps submodule of your application and, if "
|
||||||
|
"this module defines several config classes, mark the default one "
|
||||||
|
"with default = True."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(RemovedInDjango41Warning, msg):
|
||||||
|
with self.settings(INSTALLED_APPS=['apps.explicit_default_config_mismatch_app']):
|
||||||
|
config = apps.get_app_config('explicit_default_config_mismatch_app')
|
||||||
|
self.assertIsInstance(config, ExplicitDefaultConfigMismatch)
|
||||||
|
0
tests/apps/two_configs_app/__init__.py
Normal file
0
tests/apps/two_configs_app/__init__.py
Normal file
9
tests/apps/two_configs_app/apps.py
Normal file
9
tests/apps/two_configs_app/apps.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TwoConfig(AppConfig):
|
||||||
|
name = 'apps.two_configs_app'
|
||||||
|
|
||||||
|
|
||||||
|
class TwoConfigBis(AppConfig):
|
||||||
|
name = 'apps.two_configs_app'
|
0
tests/apps/two_configs_one_default_app/__init__.py
Normal file
0
tests/apps/two_configs_one_default_app/__init__.py
Normal file
10
tests/apps/two_configs_one_default_app/apps.py
Normal file
10
tests/apps/two_configs_one_default_app/apps.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TwoConfig(AppConfig):
|
||||||
|
default = True
|
||||||
|
name = 'apps.two_configs_one_default_app'
|
||||||
|
|
||||||
|
|
||||||
|
class TwoConfigAlt(AppConfig):
|
||||||
|
name = 'apps.two_configs_one_default_app'
|
0
tests/apps/two_default_configs_app/__init__.py
Normal file
0
tests/apps/two_default_configs_app/__init__.py
Normal file
11
tests/apps/two_default_configs_app/apps.py
Normal file
11
tests/apps/two_default_configs_app/apps.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TwoConfig(AppConfig):
|
||||||
|
default = True
|
||||||
|
name = 'apps.two_default_configs_app'
|
||||||
|
|
||||||
|
|
||||||
|
class TwoConfigBis(AppConfig):
|
||||||
|
default = True
|
||||||
|
name = 'apps.two_default_configs_app'
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class LoadingAppConfig(AppConfig):
|
class LoadingAppConfig(AppConfig):
|
||||||
name = 'loading_app'
|
name = 'i18n.loading_app'
|
||||||
|
Loading…
Reference in New Issue
Block a user