1
0
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:
Aymeric Augustin 2020-07-21 10:35:12 +02:00 committed by GitHub
parent 6ec5eb5d74
commit 3f2821af6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 374 additions and 137 deletions

View File

@ -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):
""" """

View File

@ -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'

View File

@ -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()

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.admindocs.apps.AdminDocsConfig'

View File

@ -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'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.contenttypes.apps.ContentTypesConfig'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.flatpages.apps.FlatPagesConfig'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.gis.apps.GISConfig'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.humanize.apps.HumanizeConfig'

View File

@ -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'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.postgres.apps.PostgresConfig'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.redirects.apps.RedirectsConfig'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.sessions.apps.SessionsConfig'

View File

@ -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'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.sites.apps.SitesConfig'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.staticfiles.apps.StaticFilesConfig'

View File

@ -1 +0,0 @@
default_app_config = 'django.contrib.syndication.apps.SyndicationConfig'

View File

@ -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

View File

@ -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!*

View File

@ -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.

View File

@ -1 +0,0 @@
default_app_config = 'apps.default_config_app.apps.CustomConfig'

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class CustomConfig(AppConfig):
name = 'apps.default_config_app'

View File

@ -0,0 +1 @@
default_app_config = 'apps.explicit_default_config_app.apps.ExplicitDefaultConfig'

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ExplicitDefaultConfig(AppConfig):
name = 'apps.explicit_default_config_app'

View File

@ -0,0 +1 @@
default_app_config = 'apps.explicit_default_config_mismatch_app.not_apps.ExplicitDefaultConfigMismatch'

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ImplicitDefaultConfigMismatch(AppConfig):
name = 'apps.explicit_default_config_mismatch_app'

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ExplicitDefaultConfigMismatch(AppConfig):
name = 'apps.explicit_default_config_mismatch_app'

View File

@ -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__)

View File

View File

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class OneConfig(AppConfig):
name = 'apps.one_config_app'

View File

@ -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)

View File

View 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'

View 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'

View 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'

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class LoadingAppConfig(AppConfig): class LoadingAppConfig(AppConfig):
name = 'loading_app' name = 'i18n.loading_app'