summaryrefslogtreecommitdiff
path: root/django/apps/config.py
diff options
context:
space:
mode:
authorAymeric Augustin <aymeric.augustin@m4x.org>2020-07-21 10:35:12 +0200
committerGitHub <noreply@github.com>2020-07-21 10:35:12 +0200
commit3f2821af6bc48fa8e7970c1ce27bc54c3172545e (patch)
tree807b209dd7bf77cde680f54a18395672c1cb607f /django/apps/config.py
parent6ec5eb5d74fd18a91256010706ab8b5b583526a9 (diff)
Fixed #31180 -- Configured applications automatically.
Diffstat (limited to 'django/apps/config.py')
-rw-r--r--django/apps/config.py180
1 files changed, 126 insertions, 54 deletions
diff --git a/django/apps/config.py b/django/apps/config.py
index 2728503d62..8c276d5d34 100644
--- a/django/apps/config.py
+++ b/django/apps/config.py
@@ -1,9 +1,13 @@
+import inspect
import os
+import warnings
from importlib import import_module
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'
@@ -83,73 +87,139 @@ class AppConfig:
"""
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,
- # 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:
- # 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
+ # 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:
+ app_module = import_module(entry)
+ except Exception:
+ pass
else:
+ # If app_module has an apps submodule that defines a single
+ # AppConfig subclass, use it automatically.
+ # To prevent this, an AppConfig subclass can declare a class
+ # variable default = False.
+ # If the apps module defines more than one AppConfig subclass,
+ # the default one can declare default = True.
+ if module_has_submodule(app_module, APPS_MODULE_NAME):
+ mod_path = '%s.%s' % (entry, APPS_MODULE_NAME)
+ mod = import_module(mod_path)
+ # Check if there's exactly one AppConfig candidate,
+ # excluding those that explicitly define default = False.
+ app_configs = [
+ (name, candidate)
+ for name, candidate in inspect.getmembers(mod, inspect.isclass)
+ if (
+ issubclass(candidate, cls) and
+ candidate is not cls and
+ getattr(candidate, 'default', True)
+ )
+ ]
+ 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:
- # If this works, the app module specifies an app config class.
- entry = module.default_app_config
+ new_entry = app_module.default_app_config
except AttributeError:
- # Otherwise, it simply uses the default app config class.
- return cls(entry, module)
+ # 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:
- mod_path, _, cls_name = entry.rpartition('.')
-
- # If we're reaching this point, we must attempt to load the app config
- # class located at <mod_path>.<cls_name>
- mod = import_module(mod_path)
- try:
- cls = getattr(mod, cls_name)
- except AttributeError:
- if module is None:
- # If importing as an app module failed, check if the module
- # contains any valid AppConfigs and show them as choices.
- # 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
+ message = (
+ '%r defines default_app_config = %r. ' % (entry, new_entry)
)
- if candidates:
- raise ImproperlyConfigured(
- "'%s' does not contain a class '%s'. Choices are: %s."
- % (mod_path, cls_name, ', '.join(candidates))
+ if new_entry == app_config_name:
+ message += (
+ 'Django now detects this configuration automatically. '
+ 'You can remove default_app_config.'
)
- import_module(entry)
+ 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:
- raise
+ # Re-trigger the module import exception.
+ import_module(entry)
# 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):
+ if not issubclass(app_config_class, 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)
+ if app_name is None:
+ try:
+ app_name = app_config_class.name
+ except AttributeError:
+ raise ImproperlyConfigured(
+ "'%s' must supply a name attribute." % entry
+ )
# Ensure app_name points to a valid module.
try:
@@ -157,12 +227,14 @@ class AppConfig:
except ImportError:
raise ImproperlyConfigured(
"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.
- return cls(app_name, app_module)
+ return app_config_class(app_name, app_module)
def get_model(self, model_name, require_ready=True):
"""