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.
This commit is contained in:
Aymeric Augustin 2013-12-14 18:51:58 +01:00
parent 69039becde
commit 5ba743e262
14 changed files with 53 additions and 51 deletions

View File

@ -22,7 +22,8 @@ class AppConfig(object):
self.app_module = app_module self.app_module = app_module
# Module containing models eg. <module 'django.contrib.admin.models' # 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 self.models_module = models_module
# Mapping of lower case model names to model classes. # Mapping of lower case model names to model classes.

View File

@ -88,7 +88,7 @@ class BaseAppCache(object):
for app_name in settings.INSTALLED_APPS: for app_name in settings.INSTALLED_APPS:
if app_name in self.handled: if app_name in self.handled:
continue continue
self.load_app(app_name, True) self.load_app(app_name, can_postpone=True)
if not self.nesting_level: if not self.nesting_level:
for app_name in self.postponed: for app_name in self.postponed:
self.load_app(app_name) self.load_app(app_name)
@ -115,10 +115,10 @@ class BaseAppCache(object):
models_module = import_module('%s.%s' % (app_name, MODELS_MODULE_NAME)) models_module = import_module('%s.%s' % (app_name, MODELS_MODULE_NAME))
except ImportError: except ImportError:
self.nesting_level -= 1 self.nesting_level -= 1
# If the app doesn't have a models module, we can just ignore the # If the app doesn't have a models module, we can just swallow the
# ImportError and return no models for it. # ImportError and return no models for this app.
if not module_has_submodule(app_module, MODELS_MODULE_NAME): 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 # 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 # whether to suppress or propagate the error. If can_postpone is
# True then it may be that the package is still being imported by # True then it may be that the package is still being imported by
@ -129,7 +129,7 @@ class BaseAppCache(object):
else: else:
if can_postpone: if can_postpone:
self.postponed.append(app_name) self.postponed.append(app_name)
return None return
else: else:
raise raise
@ -154,22 +154,27 @@ class BaseAppCache(object):
""" """
return self.loaded 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. Return an iterable of application configurations.
If only_installed is True (default), only applications explicitly If only_installed is True (default), only applications explicitly
listed in INSTALLED_APPS are considered. 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() self.populate()
for app_config in self.app_configs.values(): for app_config in self.app_configs.values():
if only_installed and not app_config.installed: if only_installed and not app_config.installed:
continue 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: if self.available_apps is not None and app_config.name not in self.available_apps:
continue continue
yield app_config 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. 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 If only_installed is True (default), only applications explicitly
listed in INSTALLED_APPS are considered. 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() self.populate()
app_config = self.app_configs.get(app_label) 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) 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: 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) raise UnavailableApp("App with label %r isn't available." % app_label)
return app_config return app_config

View File

@ -86,7 +86,7 @@ If you're unsure, answer 'no'.
def update_all_contenttypes(verbosity=2, **kwargs): 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) update_contenttypes(app_config.models_module, None, verbosity, **kwargs)
signals.post_migrate.connect(update_contenttypes) signals.post_migrate.connect(update_contenttypes)

View File

@ -345,12 +345,16 @@ class AppCommand(BaseCommand):
if not app_labels: if not app_labels:
raise CommandError('Enter at least one appname.') raise CommandError('Enter at least one appname.')
try: 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: except (LookupError, ImportError) as e:
raise CommandError("%s. Are you sure your INSTALLED_APPS setting is correct?" % e) raise CommandError("%s. Are you sure your INSTALLED_APPS setting is correct?" % e)
output = [] output = []
for app in app_list: for app_config in app_configs:
app_output = self.handle_app(app, **options) 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: if app_output:
output.append(app_output) output.append(app_output)
return '\n'.join(output) return '\n'.join(output)

View File

@ -70,6 +70,7 @@ class Command(BaseCommand):
else: else:
try: try:
app_obj = app_cache.get_app_config(exclude).models_module app_obj = app_cache.get_app_config(exclude).models_module
if app_obj is not None:
excluded_apps.add(app_obj) excluded_apps.add(app_obj)
except LookupError: except LookupError:
raise CommandError('Unknown app in excludes: %s' % exclude) raise CommandError('Unknown app in excludes: %s' % exclude)
@ -78,7 +79,7 @@ class Command(BaseCommand):
if primary_keys: if primary_keys:
raise CommandError("You can only use --pks option with one model") raise CommandError("You can only use --pks option with one model")
app_list = OrderedDict((app_config.models_module, None) 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) if app_config.models_module not in excluded_apps)
else: else:
if len(app_labels) > 1 and primary_keys: 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 app = app_cache.get_app_config(app_label).models_module
except LookupError: except LookupError:
raise CommandError("Unknown application: %s" % app_label) raise CommandError("Unknown application: %s" % app_label)
if app in excluded_apps: if app is None or app in excluded_apps:
continue continue
model = app_cache.get_model(app_label, model_label) model = app_cache.get_model(app_label, model_label)
if model is None: if model is None:
@ -111,7 +112,7 @@ class Command(BaseCommand):
app = app_cache.get_app_config(app_label).models_module app = app_cache.get_app_config(app_label).models_module
except LookupError: except LookupError:
raise CommandError("Unknown application: %s" % app_label) raise CommandError("Unknown application: %s" % app_label)
if app in excluded_apps: if app is None or app in excluded_apps:
continue continue
app_list[app] = None app_list[app] = None

View File

@ -94,6 +94,6 @@ Are you sure you want to do this?
# Emit the post migrate signal. This allows individual applications to # Emit the post migrate signal. This allows individual applications to
# respond as if the database had been migrated from scratch. # respond as if the database had been migrated from scratch.
all_models = [] 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)) 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) emit_post_migrate_signal(set(all_models), verbosity, interactive, database)

View File

@ -182,7 +182,7 @@ class Command(BaseCommand):
all_models = [ all_models = [
(app_config.label, (app_config.label,
router.get_migratable_models(app_config.models_module, connection.alias, include_auto_created=True)) 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 if app_config.label in apps
] ]

View File

@ -207,7 +207,7 @@ def custom_sql_for_model(model, style, connection):
def emit_pre_migrate_signal(create_models, verbosity, interactive, db): def emit_pre_migrate_signal(create_models, verbosity, interactive, db):
# Emit the pre_migrate signal for every application. # 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: if verbosity >= 2:
print("Running pre-migrate handlers for application %s" % app_config.label) print("Running pre-migrate handlers for application %s" % app_config.label)
models.signals.pre_migrate.send( 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): def emit_post_migrate_signal(created_models, verbosity, interactive, db):
# Emit the post_migrate signal for every application. # 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: if verbosity >= 2:
print("Running post-migrate handlers for application %s" % app_config.label) print("Running post-migrate handlers for application %s" % app_config.label)
models.signals.post_migrate.send( models.signals.post_migrate.send(

View File

@ -1271,7 +1271,7 @@ class BaseDatabaseIntrospection(object):
from django.apps import app_cache from django.apps import app_cache
from django.db import router from django.db import router
tables = set() 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): for model in router.get_migratable_models(app_config.models_module, self.connection.alias):
if not model._meta.managed: if not model._meta.managed:
continue continue
@ -1292,7 +1292,7 @@ class BaseDatabaseIntrospection(object):
from django.apps import app_cache from django.apps import app_cache
from django.db import router from django.db import router
all_models = [] 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)) all_models.extend(router.get_migratable_models(app_config.models_module, self.connection.alias))
tables = list(map(self.table_name_converter, tables)) tables = list(map(self.table_name_converter, tables))
return set([ return set([
@ -1307,7 +1307,7 @@ class BaseDatabaseIntrospection(object):
sequence_list = [] 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): for model in router.get_migratable_models(app_config.models_module, self.connection.alias):
if not model._meta.managed: if not model._meta.managed:
continue continue

View File

@ -55,7 +55,7 @@ class MigrationLoader(object):
self.disk_migrations = {} self.disk_migrations = {}
self.unmigrated_apps = set() self.unmigrated_apps = set()
self.migrated_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 # Get the migrations module directory
module_name = self.migrations_module(app_config.label) module_name = self.migrations_module(app_config.label)
was_loaded = module_name in sys.modules was_loaded = module_name in sys.modules

View File

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

View File

@ -1,7 +1,4 @@
from django.apps import app_cache
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from django.utils import six
from .models import Empty from .models import Empty
@ -16,20 +13,3 @@ class EmptyModelTests(TestCase):
self.assertTrue(m.id is not None) self.assertTrue(m.id is not None)
existing = Empty(m.id) existing = Empty(m.id)
existing.save() 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')

View File

10
tests/no_models/tests.py Normal file
View File

@ -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)