diff --git a/django/apps/config.py b/django/apps/config.py index aeb47923d8..f5c971fc9c 100644 --- a/django/apps/config.py +++ b/django/apps/config.py @@ -118,8 +118,21 @@ class AppConfig: cls = getattr(mod, cls_name) except AttributeError: if module is None: - # If importing as an app module failed, that error probably - # contains the most informative traceback. Trigger it again. + # 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 + ) + if candidates: + raise ImproperlyConfigured( + "'%s' does not contain a class '%s'. Choices are: %s." + % (mod_path, cls_name, ', '.join(candidates)) + ) import_module(entry) else: raise diff --git a/tests/apps/tests.py b/tests/apps/tests.py index 2fec1d8b4c..cd22a4d45c 100644 --- a/tests/apps/tests.py +++ b/tests/apps/tests.py @@ -81,10 +81,18 @@ class AppsTests(SimpleTestCase): pass def test_no_such_app_config(self): - """ - Tests when INSTALLED_APPS contains an entry that doesn't exist. - """ - with self.assertRaises(ImportError): + msg = "No module named 'apps.NoSuchConfig'" + with self.assertRaisesMessage(ImportError, msg): + with self.settings(INSTALLED_APPS=['apps.NoSuchConfig']): + pass + + def test_no_such_app_config_with_choices(self): + msg = ( + "'apps.apps' does not contain a class 'NoSuchConfig'. Choices are: " + "'BadConfig', 'MyAdmin', 'MyAuth', 'NoSuchApp', 'PlainAppsConfig', " + "'RelabeledAppsConfig'." + ) + with self.assertRaisesMessage(ImproperlyConfigured, msg): with self.settings(INSTALLED_APPS=['apps.apps.NoSuchConfig']): pass