diff --git a/django/apps/base.py b/django/apps/base.py index 778419fbef..860777bb03 100644 --- a/django/apps/base.py +++ b/django/apps/base.py @@ -22,7 +22,8 @@ class AppConfig(object): self.app_module = app_module # Module containing models eg. . + # from 'django/contrib/admin/models.pyc'>. None if the application + # doesn't have a models module. self.models_module = models_module # Mapping of lower case model names to model classes. diff --git a/django/apps/cache.py b/django/apps/cache.py index 76e580eb40..4d1c75dd5b 100644 --- a/django/apps/cache.py +++ b/django/apps/cache.py @@ -88,7 +88,7 @@ class BaseAppCache(object): for app_name in settings.INSTALLED_APPS: if app_name in self.handled: continue - self.load_app(app_name, True) + self.load_app(app_name, can_postpone=True) if not self.nesting_level: for app_name in self.postponed: self.load_app(app_name) @@ -115,10 +115,10 @@ class BaseAppCache(object): models_module = import_module('%s.%s' % (app_name, MODELS_MODULE_NAME)) except ImportError: self.nesting_level -= 1 - # If the app doesn't have a models module, we can just ignore the - # ImportError and return no models for it. + # If the app doesn't have a models module, we can just swallow the + # ImportError and return no models for this app. if not module_has_submodule(app_module, MODELS_MODULE_NAME): - return None + models_module = None # But if the app does have a models module, we need to figure out # whether to suppress or propagate the error. If can_postpone is # True then it may be that the package is still being imported by @@ -129,7 +129,7 @@ class BaseAppCache(object): else: if can_postpone: self.postponed.append(app_name) - return None + return else: raise @@ -154,22 +154,27 @@ class BaseAppCache(object): """ return self.loaded - def get_app_configs(self, only_installed=True): + def get_app_configs(self, only_installed=True, only_with_models_module=False): """ Return an iterable of application configurations. If only_installed is True (default), only applications explicitly listed in INSTALLED_APPS are considered. + + If only_with_models_module in True (non-default), only applications + containing a models module are considered. """ self.populate() for app_config in self.app_configs.values(): if only_installed and not app_config.installed: continue + if only_with_models_module and app_config.models_module is None: + continue if self.available_apps is not None and app_config.name not in self.available_apps: continue yield app_config - def get_app_config(self, app_label, only_installed=True): + def get_app_config(self, app_label, only_installed=True, only_with_models_module=False): """ Returns the application configuration for the given app_label. @@ -180,11 +185,18 @@ class BaseAppCache(object): If only_installed is True (default), only applications explicitly listed in INSTALLED_APPS are considered. + + If only_with_models_module in True (non-default), only applications + containing a models module are considered. """ self.populate() app_config = self.app_configs.get(app_label) - if app_config is None or (only_installed and not app_config.installed): + if app_config is None: raise LookupError("No app with label %r." % app_label) + if only_installed and not app_config.installed: + raise LookupError("App with label %r isn't in INSTALLED_APPS." % app_label) + if only_with_models_module and app_config.models_module is None: + raise LookupError("App with label %r doesn't have a models module." % app_label) if self.available_apps is not None and app_config.name not in self.available_apps: raise UnavailableApp("App with label %r isn't available." % app_label) return app_config diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py index dfe5bb3c65..35a8a7fdc8 100644 --- a/django/contrib/contenttypes/management.py +++ b/django/contrib/contenttypes/management.py @@ -86,7 +86,7 @@ If you're unsure, answer 'no'. def update_all_contenttypes(verbosity=2, **kwargs): - for app_config in app_cache.get_app_configs(): + for app_config in app_cache.get_app_configs(only_with_models_module=True): update_contenttypes(app_config.models_module, None, verbosity, **kwargs) signals.post_migrate.connect(update_contenttypes) diff --git a/django/core/management/base.py b/django/core/management/base.py index c7d9c939e6..4e95ba8298 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -345,12 +345,16 @@ class AppCommand(BaseCommand): if not app_labels: raise CommandError('Enter at least one appname.') try: - app_list = [app_cache.get_app_config(app_label).models_module for app_label in app_labels] + app_configs = [app_cache.get_app_config(app_label) for app_label in app_labels] except (LookupError, ImportError) as e: raise CommandError("%s. Are you sure your INSTALLED_APPS setting is correct?" % e) output = [] - for app in app_list: - app_output = self.handle_app(app, **options) + for app_config in app_configs: + if app_config.models_module is None: + raise CommandError( + "AppCommand cannot handle app %r because it doesn't have " + "a models module." % app_config.label) + app_output = self.handle_app(app_config.models_module, **options) if app_output: output.append(app_output) return '\n'.join(output) diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index af8da18322..9aebb6c7d6 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -70,7 +70,8 @@ class Command(BaseCommand): else: try: app_obj = app_cache.get_app_config(exclude).models_module - excluded_apps.add(app_obj) + if app_obj is not None: + excluded_apps.add(app_obj) except LookupError: raise CommandError('Unknown app in excludes: %s' % exclude) @@ -78,7 +79,7 @@ class Command(BaseCommand): if primary_keys: raise CommandError("You can only use --pks option with one model") app_list = OrderedDict((app_config.models_module, None) - for app_config in app_cache.get_app_configs() + for app_config in app_cache.get_app_configs(only_with_models_module=True) if app_config.models_module not in excluded_apps) else: if len(app_labels) > 1 and primary_keys: @@ -91,7 +92,7 @@ class Command(BaseCommand): app = app_cache.get_app_config(app_label).models_module except LookupError: raise CommandError("Unknown application: %s" % app_label) - if app in excluded_apps: + if app is None or app in excluded_apps: continue model = app_cache.get_model(app_label, model_label) if model is None: @@ -111,7 +112,7 @@ class Command(BaseCommand): app = app_cache.get_app_config(app_label).models_module except LookupError: raise CommandError("Unknown application: %s" % app_label) - if app in excluded_apps: + if app is None or app in excluded_apps: continue app_list[app] = None diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index 0701bf82a9..d35a1efa5a 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -94,6 +94,6 @@ Are you sure you want to do this? # Emit the post migrate signal. This allows individual applications to # respond as if the database had been migrated from scratch. all_models = [] - for app_config in app_cache.get_app_configs(): + for app_config in app_cache.get_app_configs(only_with_models_module=True): all_models.extend(router.get_migratable_models(app_config.models_module, database, include_auto_created=True)) emit_post_migrate_signal(set(all_models), verbosity, interactive, database) diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index ce5ffae4a5..a6b9677d2e 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -182,7 +182,7 @@ class Command(BaseCommand): all_models = [ (app_config.label, router.get_migratable_models(app_config.models_module, connection.alias, include_auto_created=True)) - for app_config in app_cache.get_app_configs() + for app_config in app_cache.get_app_configs(only_with_models_module=True) if app_config.label in apps ] diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 909faf0117..b2500d3787 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -207,7 +207,7 @@ def custom_sql_for_model(model, style, connection): def emit_pre_migrate_signal(create_models, verbosity, interactive, db): # Emit the pre_migrate signal for every application. - for app_config in app_cache.get_app_configs(): + for app_config in app_cache.get_app_configs(only_with_models_module=True): if verbosity >= 2: print("Running pre-migrate handlers for application %s" % app_config.label) models.signals.pre_migrate.send( @@ -221,7 +221,7 @@ def emit_pre_migrate_signal(create_models, verbosity, interactive, db): def emit_post_migrate_signal(created_models, verbosity, interactive, db): # Emit the post_migrate signal for every application. - for app_config in app_cache.get_app_configs(): + for app_config in app_cache.get_app_configs(only_with_models_module=True): if verbosity >= 2: print("Running post-migrate handlers for application %s" % app_config.label) models.signals.post_migrate.send( diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 6e5d03ff96..86905afe77 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1271,7 +1271,7 @@ class BaseDatabaseIntrospection(object): from django.apps import app_cache from django.db import router tables = set() - for app_config in app_cache.get_app_configs(): + for app_config in app_cache.get_app_configs(only_with_models_module=True): for model in router.get_migratable_models(app_config.models_module, self.connection.alias): if not model._meta.managed: continue @@ -1292,7 +1292,7 @@ class BaseDatabaseIntrospection(object): from django.apps import app_cache from django.db import router all_models = [] - for app_config in app_cache.get_app_configs(): + for app_config in app_cache.get_app_configs(only_with_models_module=True): all_models.extend(router.get_migratable_models(app_config.models_module, self.connection.alias)) tables = list(map(self.table_name_converter, tables)) return set([ @@ -1307,7 +1307,7 @@ class BaseDatabaseIntrospection(object): sequence_list = [] - for app_config in app_cache.get_app_configs(): + for app_config in app_cache.get_app_configs(only_with_models_module=True): for model in router.get_migratable_models(app_config.models_module, self.connection.alias): if not model._meta.managed: continue diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py index af311fdf02..9a54f14e75 100644 --- a/django/db/migrations/loader.py +++ b/django/db/migrations/loader.py @@ -55,7 +55,7 @@ class MigrationLoader(object): self.disk_migrations = {} self.unmigrated_apps = set() self.migrated_apps = set() - for app_config in app_cache.get_app_configs(): + for app_config in app_cache.get_app_configs(only_with_models_module=True): # Get the migrations module directory module_name = self.migrations_module(app_config.label) was_loaded = module_name in sys.modules diff --git a/tests/empty/no_models/tests.py b/tests/empty/no_models/tests.py deleted file mode 100644 index 8b8db1af39..0000000000 --- a/tests/empty/no_models/tests.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.test import TestCase - - -class NoModelTests(TestCase): - """ A placeholder test case. See empty.tests for more info. """ - pass diff --git a/tests/empty/tests.py b/tests/empty/tests.py index 9fcd7a978e..7cebb87c2a 100644 --- a/tests/empty/tests.py +++ b/tests/empty/tests.py @@ -1,7 +1,4 @@ -from django.apps import app_cache from django.test import TestCase -from django.test.utils import override_settings -from django.utils import six from .models import Empty @@ -16,20 +13,3 @@ class EmptyModelTests(TestCase): self.assertTrue(m.id is not None) existing = Empty(m.id) existing.save() - - -class NoModelTests(TestCase): - """ - Test for #7198 to ensure that the proper error message is raised - when attempting to load an app with no models.py file. - - Because the test runner won't currently load a test module with no - models.py file, this TestCase instead lives in this module. - - It seemed like an appropriate home for it. - """ - @override_settings(INSTALLED_APPS=("empty.no_models",)) - def test_no_models(self): - with six.assertRaisesRegex(self, LookupError, - "No app with label 'no_models'."): - app_cache.get_app_config('no_models') diff --git a/tests/no_models/__init__.py b/tests/no_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/no_models/tests.py b/tests/no_models/tests.py new file mode 100644 index 0000000000..f9ff80485e --- /dev/null +++ b/tests/no_models/tests.py @@ -0,0 +1,10 @@ +from django.apps import app_cache +from django.test import TestCase + + +class NoModelTests(TestCase): + + def test_no_models(self): + """Test that it's possible to load an app with no models.py file.""" + app_config = app_cache.get_app_config('no_models') + self.assertIsNone(app_config.models_module)