Skip to content

Commit 3f2821a

Browse files
authored
Fixed #31180 -- Configured applications automatically.
1 parent 6ec5eb5 commit 3f2821a

File tree

39 files changed

+370
-133
lines changed

39 files changed

+370
-133
lines changed

django/apps/config.py

Lines changed: 126 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import inspect
12
import os
3+
import warnings
24
from importlib import import_module
35

46
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
69

10+
APPS_MODULE_NAME = 'apps'
711
MODELS_MODULE_NAME = 'models'
812

913

@@ -83,86 +87,154 @@ def create(cls, entry):
8387
"""
8488
Factory that creates an app config from an entry in INSTALLED_APPS.
8589
"""
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
10394

95+
# If import_module succeeds, entry points to the app module.
96+
try:
97+
app_module = import_module(entry)
98+
except Exception:
99+
pass
104100
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.
105146
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
108148
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
111153
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)
130156
)
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.'
135161
)
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)
137204
else:
138-
raise
205+
# Re-trigger the module import exception.
206+
import_module(entry)
139207

140208
# Check for obvious errors. (This check prevents duck typing, but
141209
# 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):
143211
raise ImproperlyConfigured(
144212
"'%s' isn't a subclass of AppConfig." % entry)
145213

146214
# Obtain app name here rather than in AppClass.__init__ to keep
147215
# 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+
)
153223

154224
# Ensure app_name points to a valid module.
155225
try:
156226
app_module = import_module(app_name)
157227
except ImportError:
158228
raise ImproperlyConfigured(
159229
"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__,
161233
)
162234
)
163235

164236
# 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)
166238

167239
def get_model(self, model_name, require_ready=True):
168240
"""

django/contrib/admin/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,3 @@
2222

2323
def autodiscover():
2424
autodiscover_modules('admin', register_to=site)
25-
26-
27-
default_app_config = 'django.contrib.admin.apps.AdminConfig'

django/contrib/admin/apps.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def ready(self):
1919
class AdminConfig(SimpleAdminConfig):
2020
"""The default AppConfig for admin which does autodiscovery."""
2121

22+
default = True
23+
2224
def ready(self):
2325
super().ready()
2426
self.module.autodiscover()

django/contrib/admindocs/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.admindocs.apps.AdminDocsConfig'

django/contrib/auth/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,3 @@ def update_session_auth_hash(request, user):
217217
request.session.cycle_key()
218218
if hasattr(user, 'get_session_auth_hash') and request.user == user:
219219
request.session[HASH_SESSION_KEY] = user.get_session_auth_hash()
220-
221-
222-
default_app_config = 'django.contrib.auth.apps.AuthConfig'
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.contenttypes.apps.ContentTypesConfig'

django/contrib/flatpages/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.flatpages.apps.FlatPagesConfig'

django/contrib/gis/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.gis.apps.GISConfig'

django/contrib/humanize/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.humanize.apps.HumanizeConfig'

django/contrib/messages/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
from django.contrib.messages.api import * # NOQA
22
from django.contrib.messages.constants import * # NOQA
3-
4-
default_app_config = 'django.contrib.messages.apps.MessagesConfig'

django/contrib/postgres/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.postgres.apps.PostgresConfig'

django/contrib/redirects/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.redirects.apps.RedirectsConfig'

django/contrib/sessions/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.sessions.apps.SessionsConfig'

django/contrib/sitemaps/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,3 @@ def lastmod(self, item):
158158
if self.date_field is not None:
159159
return getattr(item, self.date_field)
160160
return None
161-
162-
163-
default_app_config = 'django.contrib.sitemaps.apps.SiteMapsConfig'

django/contrib/sites/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.sites.apps.SitesConfig'
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.staticfiles.apps.StaticFilesConfig'
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
default_app_config = 'django.contrib.syndication.apps.SyndicationConfig'

docs/internals/deprecation.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ details on these changes.
2424
* The ``whitelist`` argument and ``domain_whitelist`` attribute of
2525
``django.core.validators.EmailValidator`` will be removed.
2626

27+
* The ``default_app_config`` module variable will be removed.
28+
2729
.. _deprecation-removed-in-4.0:
2830

2931
4.0

0 commit comments

Comments
 (0)