|
| 1 | +import inspect |
1 | 2 | import os
|
| 3 | +import warnings |
2 | 4 | from importlib import import_module
|
3 | 5 |
|
4 | 6 | from django.core.exceptions import ImproperlyConfigured
|
5 |
| -from django.utils.module_loading import module_has_submodule |
| 7 | +from django.utils.deprecation import RemovedInDjango41Warning |
| 8 | +from django.utils.module_loading import import_string, module_has_submodule |
6 | 9 |
|
| 10 | +APPS_MODULE_NAME = 'apps' |
7 | 11 | MODELS_MODULE_NAME = 'models'
|
8 | 12 |
|
9 | 13 |
|
@@ -83,86 +87,154 @@ def create(cls, entry):
|
83 | 87 | """
|
84 | 88 | Factory that creates an app config from an entry in INSTALLED_APPS.
|
85 | 89 | """
|
86 |
| - try: |
87 |
| - # If import_module succeeds, entry is a path to an app module, |
88 |
| - # which may specify an app config class with default_app_config. |
89 |
| - # Otherwise, entry is a path to an app config class or an error. |
90 |
| - module = import_module(entry) |
91 |
| - |
92 |
| - except ImportError: |
93 |
| - # Track that importing as an app module failed. If importing as an |
94 |
| - # app config class fails too, we'll trigger the ImportError again. |
95 |
| - module = None |
96 |
| - |
97 |
| - mod_path, _, cls_name = entry.rpartition('.') |
98 |
| - |
99 |
| - # Raise the original exception when entry cannot be a path to an |
100 |
| - # app config class. |
101 |
| - if not mod_path: |
102 |
| - raise |
| 90 | + # create() eventually returns app_config_class(app_name, app_module). |
| 91 | + app_config_class = None |
| 92 | + app_name = None |
| 93 | + app_module = None |
103 | 94 |
|
| 95 | + # If import_module succeeds, entry points to the app module. |
| 96 | + try: |
| 97 | + app_module = import_module(entry) |
| 98 | + except Exception: |
| 99 | + pass |
104 | 100 | else:
|
| 101 | + # If app_module has an apps submodule that defines a single |
| 102 | + # AppConfig subclass, use it automatically. |
| 103 | + # To prevent this, an AppConfig subclass can declare a class |
| 104 | + # variable default = False. |
| 105 | + # If the apps module defines more than one AppConfig subclass, |
| 106 | + # the default one can declare default = True. |
| 107 | + if module_has_submodule(app_module, APPS_MODULE_NAME): |
| 108 | + mod_path = '%s.%s' % (entry, APPS_MODULE_NAME) |
| 109 | + mod = import_module(mod_path) |
| 110 | + # Check if there's exactly one AppConfig candidate, |
| 111 | + # excluding those that explicitly define default = False. |
| 112 | + app_configs = [ |
| 113 | + (name, candidate) |
| 114 | + for name, candidate in inspect.getmembers(mod, inspect.isclass) |
| 115 | + if ( |
| 116 | + issubclass(candidate, cls) and |
| 117 | + candidate is not cls and |
| 118 | + getattr(candidate, 'default', True) |
| 119 | + ) |
| 120 | + ] |
| 121 | + if len(app_configs) == 1: |
| 122 | + app_config_class = app_configs[0][1] |
| 123 | + app_config_name = '%s.%s' % (mod_path, app_configs[0][0]) |
| 124 | + else: |
| 125 | + # Check if there's exactly one AppConfig subclass, |
| 126 | + # among those that explicitly define default = True. |
| 127 | + app_configs = [ |
| 128 | + (name, candidate) |
| 129 | + for name, candidate in app_configs |
| 130 | + if getattr(candidate, 'default', False) |
| 131 | + ] |
| 132 | + if len(app_configs) > 1: |
| 133 | + candidates = [repr(name) for name, _ in app_configs] |
| 134 | + raise RuntimeError( |
| 135 | + '%r declares more than one default AppConfig: ' |
| 136 | + '%s.' % (mod_path, ', '.join(candidates)) |
| 137 | + ) |
| 138 | + elif len(app_configs) == 1: |
| 139 | + app_config_class = app_configs[0][1] |
| 140 | + app_config_name = '%s.%s' % (mod_path, app_configs[0][0]) |
| 141 | + |
| 142 | + # If app_module specifies a default_app_config, follow the link. |
| 143 | + # default_app_config is deprecated, but still takes over the |
| 144 | + # automatic detection for backwards compatibility during the |
| 145 | + # deprecation period. |
105 | 146 | try:
|
106 |
| - # If this works, the app module specifies an app config class. |
107 |
| - entry = module.default_app_config |
| 147 | + new_entry = app_module.default_app_config |
108 | 148 | except AttributeError:
|
109 |
| - # Otherwise, it simply uses the default app config class. |
110 |
| - return cls(entry, module) |
| 149 | + # Use the default app config class if we didn't find anything. |
| 150 | + if app_config_class is None: |
| 151 | + app_config_class = cls |
| 152 | + app_name = entry |
111 | 153 | else:
|
112 |
| - mod_path, _, cls_name = entry.rpartition('.') |
113 |
| - |
114 |
| - # If we're reaching this point, we must attempt to load the app config |
115 |
| - # class located at <mod_path>.<cls_name> |
116 |
| - mod = import_module(mod_path) |
117 |
| - try: |
118 |
| - cls = getattr(mod, cls_name) |
119 |
| - except AttributeError: |
120 |
| - if module is None: |
121 |
| - # If importing as an app module failed, check if the module |
122 |
| - # contains any valid AppConfigs and show them as choices. |
123 |
| - # Otherwise, that error probably contains the most informative |
124 |
| - # traceback, so trigger it again. |
125 |
| - candidates = sorted( |
126 |
| - repr(name) for name, candidate in mod.__dict__.items() |
127 |
| - if isinstance(candidate, type) and |
128 |
| - issubclass(candidate, AppConfig) and |
129 |
| - candidate is not AppConfig |
| 154 | + message = ( |
| 155 | + '%r defines default_app_config = %r. ' % (entry, new_entry) |
130 | 156 | )
|
131 |
| - if candidates: |
132 |
| - raise ImproperlyConfigured( |
133 |
| - "'%s' does not contain a class '%s'. Choices are: %s." |
134 |
| - % (mod_path, cls_name, ', '.join(candidates)) |
| 157 | + if new_entry == app_config_name: |
| 158 | + message += ( |
| 159 | + 'Django now detects this configuration automatically. ' |
| 160 | + 'You can remove default_app_config.' |
135 | 161 | )
|
136 |
| - import_module(entry) |
| 162 | + else: |
| 163 | + message += ( |
| 164 | + "However, Django's automatic detection picked another " |
| 165 | + "configuration, %r. You should move the default " |
| 166 | + "config class to the apps submodule of your " |
| 167 | + "application and, if this module defines several " |
| 168 | + "config classes, mark the default one with default = " |
| 169 | + "True." % app_config_name |
| 170 | + ) |
| 171 | + warnings.warn(message, RemovedInDjango41Warning, stacklevel=2) |
| 172 | + entry = new_entry |
| 173 | + app_config_class = None |
| 174 | + |
| 175 | + # If import_string succeeds, entry is an app config class. |
| 176 | + if app_config_class is None: |
| 177 | + try: |
| 178 | + app_config_class = import_string(entry) |
| 179 | + except Exception: |
| 180 | + pass |
| 181 | + # If both import_module and import_string failed, it means that entry |
| 182 | + # doesn't have a valid value. |
| 183 | + if app_module is None and app_config_class is None: |
| 184 | + # If the last component of entry starts with an uppercase letter, |
| 185 | + # then it was likely intended to be an app config class; if not, |
| 186 | + # an app module. Provide a nice error message in both cases. |
| 187 | + mod_path, _, cls_name = entry.rpartition('.') |
| 188 | + if mod_path and cls_name[0].isupper(): |
| 189 | + # We could simply re-trigger the string import exception, but |
| 190 | + # we're going the extra mile and providing a better error |
| 191 | + # message for typos in INSTALLED_APPS. |
| 192 | + # This may raise ImportError, which is the best exception |
| 193 | + # possible if the module at mod_path cannot be imported. |
| 194 | + mod = import_module(mod_path) |
| 195 | + candidates = [ |
| 196 | + repr(name) |
| 197 | + for name, candidate in inspect.getmembers(mod, inspect.isclass) |
| 198 | + if issubclass(candidate, cls) and candidate is not cls |
| 199 | + ] |
| 200 | + msg = "Module '%s' does not contain a '%s' class." % (mod_path, cls_name) |
| 201 | + if candidates: |
| 202 | + msg += ' Choices are: %s.' % ', '.join(candidates) |
| 203 | + raise ImportError(msg) |
137 | 204 | else:
|
138 |
| - raise |
| 205 | + # Re-trigger the module import exception. |
| 206 | + import_module(entry) |
139 | 207 |
|
140 | 208 | # Check for obvious errors. (This check prevents duck typing, but
|
141 | 209 | # it could be removed if it became a problem in practice.)
|
142 |
| - if not issubclass(cls, AppConfig): |
| 210 | + if not issubclass(app_config_class, AppConfig): |
143 | 211 | raise ImproperlyConfigured(
|
144 | 212 | "'%s' isn't a subclass of AppConfig." % entry)
|
145 | 213 |
|
146 | 214 | # Obtain app name here rather than in AppClass.__init__ to keep
|
147 | 215 | # all error checking for entries in INSTALLED_APPS in one place.
|
148 |
| - try: |
149 |
| - app_name = cls.name |
150 |
| - except AttributeError: |
151 |
| - raise ImproperlyConfigured( |
152 |
| - "'%s' must supply a name attribute." % entry) |
| 216 | + if app_name is None: |
| 217 | + try: |
| 218 | + app_name = app_config_class.name |
| 219 | + except AttributeError: |
| 220 | + raise ImproperlyConfigured( |
| 221 | + "'%s' must supply a name attribute." % entry |
| 222 | + ) |
153 | 223 |
|
154 | 224 | # Ensure app_name points to a valid module.
|
155 | 225 | try:
|
156 | 226 | app_module = import_module(app_name)
|
157 | 227 | except ImportError:
|
158 | 228 | raise ImproperlyConfigured(
|
159 | 229 | "Cannot import '%s'. Check that '%s.%s.name' is correct." % (
|
160 |
| - app_name, mod_path, cls_name, |
| 230 | + app_name, |
| 231 | + app_config_class.__module__, |
| 232 | + app_config_class.__qualname__, |
161 | 233 | )
|
162 | 234 | )
|
163 | 235 |
|
164 | 236 | # Entry is a path to an app config class.
|
165 |
| - return cls(app_name, app_module) |
| 237 | + return app_config_class(app_name, app_module) |
166 | 238 |
|
167 | 239 | def get_model(self, model_name, require_ready=True):
|
168 | 240 | """
|
|
0 commit comments