diff options
| author | Aymeric Augustin <aymeric.augustin@m4x.org> | 2020-07-21 10:35:12 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-07-21 10:35:12 +0200 |
| commit | 3f2821af6bc48fa8e7970c1ce27bc54c3172545e (patch) | |
| tree | 807b209dd7bf77cde680f54a18395672c1cb607f /django/apps | |
| parent | 6ec5eb5d74fd18a91256010706ab8b5b583526a9 (diff) | |
Fixed #31180 -- Configured applications automatically.
Diffstat (limited to 'django/apps')
| -rw-r--r-- | django/apps/config.py | 180 |
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): """ |
