Jelajahi Sumber

Made it possible to create apps without a models module.

This commit reverts f44c4a5d0f and 39bbd165.

django.test.simple will be updated in a separate commit as it requires
invasive changes.
Aymeric Augustin 11 tahun lalu
induk
melakukan
5ba743e262

+ 2 - 1
django/apps/base.py

@@ -22,7 +22,8 @@ class AppConfig(object):
         self.app_module = app_module
 
         # Module containing models eg. <module 'django.contrib.admin.models'
-        # from 'django/contrib/admin/models.pyc'>.
+        # 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.

+ 20 - 8
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

+ 1 - 1
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)

+ 7 - 3
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)

+ 5 - 4
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
 

+ 1 - 1
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)

+ 1 - 1
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
         ]
 

+ 2 - 2
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(

+ 3 - 3
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

+ 1 - 1
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

+ 0 - 6
tests/empty/no_models/tests.py

@@ -1,6 +0,0 @@
-from django.test import TestCase
-
-
-class NoModelTests(TestCase):
-    """ A placeholder test case. See empty.tests for more info. """
-    pass

+ 0 - 20
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')

+ 0 - 0
tests/no_models/__init__.py


+ 10 - 0
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)