diff --git a/django/apps/registry.py b/django/apps/registry.py index 4f10ad8346..e5d46e9305 100644 --- a/django/apps/registry.py +++ b/django/apps/registry.py @@ -248,14 +248,11 @@ class Apps(object): model_name is case-insensitive. - Returns None if no application exists with this label, or no model - exists with this name in the application. + Raises LookupError if no application exists with this label, or no + model exists with this name in the application. """ self.populate_models() - try: - return self.get_app_config(app_label).get_model(model_name.lower()) - except LookupError: - return None + return self.get_app_config(app_label).get_model(model_name.lower()) def register_model(self, app_label, model): # Since this method is called when models are imported, it cannot @@ -289,9 +286,13 @@ class Apps(object): the given app_label. It's safe to call this method at import time, even while the registry - is being populated. It returns None for models that aren't loaded yet. + is being populated. """ - return self.all_models[app_label].get(model_name.lower()) + model = self.all_models[app_label].get(model_name.lower()) + if model is None: + raise LookupError( + "Model '%s.%s' not registered." % (app_label, model_name)) + return model def set_available_apps(self, available): """ diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index ebb7687dea..7afb11ef34 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -188,8 +188,9 @@ class ModelDetailView(BaseAdminDocsView): apps.get_app_config(self.kwargs['app_label']) except LookupError: raise Http404(_("App %(app_label)r not found") % self.kwargs) - model = apps.get_model(self.kwargs['app_label'], self.kwargs['model_name']) - if model is None: + try: + model = apps.get_model(self.kwargs['app_label'], self.kwargs['model_name']) + except LookupError: raise Http404(_("Model %(model_name)r not found in app %(app_label)r") % self.kwargs) opts = model._meta diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index ef44737cc0..f2bcaba390 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -129,8 +129,9 @@ def get_user_model(): app_label, model_name = settings.AUTH_USER_MODEL.split('.') except ValueError: raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'") - user_model = apps.get_model(app_label, model_name) - if user_model is None: + try: + user_model = apps.get_model(app_label, model_name) + except LookupError: raise ImproperlyConfigured("AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL) return user_model diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index c2111e3b98..306b051284 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -61,7 +61,9 @@ def _check_permission_clashing(custom, builtin, ctype): def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs): - if apps.get_model('auth', 'Permission') is None: + try: + apps.get_model('auth', 'Permission') + except LookupError: return if not router.allow_migrate(db, auth_app.Permission): @@ -117,7 +119,9 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw def create_superuser(app, created_models, verbosity, db, **kwargs): - if apps.get_model('auth', 'Permission') is None: + try: + apps.get_model('auth', 'Permission') + except LookupError: return UserModel = get_user_model() diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index bad8eb50f7..165f2b6574 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -54,7 +54,7 @@ def post_comment(request, next=None, using=None): except TypeError: return CommentPostBadRequest( "Invalid content_type value: %r" % escape(ctype)) - except AttributeError: + except LookupError: return CommentPostBadRequest( "The given content-type %r does not resolve to a valid model." % \ escape(ctype)) diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py index 87171b1ff2..6b2d2d22f1 100644 --- a/django/contrib/contenttypes/management.py +++ b/django/contrib/contenttypes/management.py @@ -12,7 +12,9 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, * Creates content types for models in the given app, removing any model entries that no longer have a matching model class. """ - if apps.get_model('contenttypes', 'ContentType') is None: + try: + apps.get_model('contenttypes', 'ContentType') + except LookupError: return if not router.allow_migrate(db, ContentType): diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index 6b80b9fe7e..0dc048cfa2 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -157,7 +157,10 @@ class ContentType(models.Model): def model_class(self): "Returns the Python model class for this type of content." - return apps.get_model(self.app_label, self.model) + try: + return apps.get_model(self.app_label, self.model) + except LookupError: + return None def get_object_for_this_type(self, **kwargs): """ diff --git a/django/contrib/gis/sitemaps/views.py b/django/contrib/gis/sitemaps/views.py index 20a91130bb..a65612a541 100644 --- a/django/contrib/gis/sitemaps/views.py +++ b/django/contrib/gis/sitemaps/views.py @@ -81,8 +81,9 @@ def kml(request, label, model, field_name=None, compress=False, using=DEFAULT_DB must be that of a geographic field. """ placemarks = [] - klass = apps.get_model(label, model) - if not klass: + try: + klass = apps.get_model(label, model) + except LookupError: raise Http404('You must supply a valid app label and module name. Got "%s.%s"' % (label, model)) if field_name: diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index 82d8432bc1..c4501c5b24 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -66,8 +66,9 @@ class Command(BaseCommand): for exclude in excludes: if '.' in exclude: app_label, model_name = exclude.split('.', 1) - model = apps.get_model(app_label, model_name) - if not model: + try: + model = apps.get_model(app_label, model_name) + except LookupError: raise CommandError('Unknown model in excludes: %s' % exclude) excluded_models.add(model) else: @@ -96,8 +97,9 @@ class Command(BaseCommand): raise CommandError("Unknown application: %s" % app_label) if app_config.models_module is None or app_config in excluded_apps: continue - model = apps.get_model(app_label, model_label) - if model is None: + try: + model = apps.get_model(app_label, model_label) + except LookupError: raise CommandError("Unknown model: %s.%s" % (app_label, model_label)) app_list_value = app_list.setdefault(app_config, []) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index c3b55770ce..0af92bb5b7 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -42,7 +42,9 @@ def get_validation_errors(outfile, app=None): except ValueError: e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable) continue - if not apps.get_model(app_label, model_name): + try: + apps.get_model(app_label, model_name) + except LookupError: e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped) # No need to perform any other validation checks on a swapped model. continue diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index a46adeef7a..9a59f61d70 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -156,8 +156,6 @@ def _get_model(model_identifier): """ try: Model = apps.get_model(*model_identifier.split(".")) - except TypeError: - Model = None - if Model is None: + except (LookupError, TypeError): raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier) return Model diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 81e41f8a32..06dd134754 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -278,9 +278,7 @@ class Deserializer(base.Deserializer): % (node.nodeName, attr)) try: Model = apps.get_model(*model_identifier.split(".")) - except TypeError: - Model = None - if Model is None: + except (LookupError, TypeError): raise base.DeserializationError( "<%s> node has invalid model identifier: '%s'" % (node.nodeName, model_identifier)) diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 21009f4da5..7eedcdbac6 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -192,11 +192,12 @@ class ModelState(object): meta_contents["unique_together"] = list(meta_contents["unique_together"]) meta = type("Meta", tuple(), meta_contents) # Then, work out our bases - bases = tuple( - (apps.get_model(*base.split(".", 1)) if isinstance(base, six.string_types) else base) - for base in self.bases - ) - if None in bases: + try: + bases = tuple( + (apps.get_model(*base.split(".", 1)) if isinstance(base, six.string_types) else base) + for base in self.bases + ) + except LookupError: raise InvalidBasesError("Cannot resolve one or more bases from %r" % (self.bases,)) # Turn fields into a dict for the body, add other bits body = dict(self.fields) diff --git a/django/db/models/base.py b/django/db/models/base.py index 66c1d96762..03f1e8b693 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -151,9 +151,10 @@ class ModelBase(type): new_class._base_manager = new_class._base_manager._copy_to_model(new_class) # Bail out early if we have already created this class. - m = new_class._meta.apps.get_registered_model(new_class._meta.app_label, name) - if m is not None: - return m + try: + return new_class._meta.apps.get_registered_model(new_class._meta.app_label, name) + except LookupError: + pass # Add all attributes to the class. for obj_name, obj in attrs.items(): diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 247067d60a..beaa48c6f1 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -68,13 +68,14 @@ def add_lazy_relation(cls, field, relation, operation): # string right away. If get_model returns None, it means that the related # model isn't loaded yet, so we need to pend the relation until the class # is prepared. - model = cls._meta.apps.get_registered_model(app_label, model_name) - if model: - operation(field, model, cls) - else: + try: + model = cls._meta.apps.get_registered_model(app_label, model_name) + except LookupError: key = (app_label, model_name) value = (cls, field, operation) cls._meta.apps._pending_lookups.setdefault(key, []).append(value) + else: + operation(field, model, cls) def do_pending_lookups(sender, **kwargs): diff --git a/django/db/models/signals.py b/django/db/models/signals.py index 97352460ee..f5e1ac2cf4 100644 --- a/django/db/models/signals.py +++ b/django/db/models/signals.py @@ -39,8 +39,9 @@ class ModelSignal(Signal): "Specified sender must either be a model or a " "model name of the 'app_label.ModelName' form." ) - sender = apps.get_registered_model(app_label, model_name) - if sender is None: + try: + sender = apps.get_registered_model(app_label, model_name) + except LookupError: ref = (app_label, model_name) refs = self.unresolved_references.setdefault(ref, []) refs.append((receiver, weak, dispatch_uid)) diff --git a/docs/ref/applications.txt b/docs/ref/applications.txt index 8f868f751e..1fc74f6bee 100644 --- a/docs/ref/applications.txt +++ b/docs/ref/applications.txt @@ -202,5 +202,5 @@ Application registry .. method:: apps.get_model(app_label, model_name) Returns the :class:`~django.db.models.Model` with the given ``app_label`` - and ``model_name``. Returns ``None`` if no such application or model - exists. ``model_name`` is case-insensitive. + and ``model_name``. Raises :exc:`~exceptions.LookupError` if no such + application or model exists. ``model_name`` is case-insensitive. diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index dbbdcf9c81..2c1b2bf26f 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -602,9 +602,15 @@ in addition to application modules, you should review code that accesses this setting directly and use the app registry (:attr:`django.apps.apps`) instead. The "app registry" that manages the list of installed applications doesn't -have the same features as the old "app cache". However, even though the "app -cache" was a private API, most of its methods were temporarily preserved and -will go through a deprecation path. +have the same features as the old "app cache". Even though the "app cache" was +a private API, obsolete methods will be removed after a standard deprecation +period. In addition, the following changes take effect immediately: + +* ``get_model`` raises :exc:`~exceptions.LookupError` instead of returning + ``None`` when no model is found. + +* The ``only_installed`` and ``seed_cache`` arguments of ``get_model`` no + longer exist. While the new implementation is believed to be more robust, regressions cannot be ruled out, especially during the import sequence. Circular imports that diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index d61205330a..04ca30e88d 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -72,8 +72,8 @@ class GetModelsTest(TestCase): self.not_installed_module = models def test_get_model_only_returns_installed_models(self): - self.assertEqual( - apps.get_model("not_installed", "NotInstalledModel"), None) + with self.assertRaises(LookupError): + apps.get_model("not_installed", "NotInstalledModel") def test_get_models_only_returns_installed_models(self): self.assertNotIn( diff --git a/tests/apps/tests.py b/tests/apps/tests.py index 4e988493fc..4cd35c38cf 100644 --- a/tests/apps/tests.py +++ b/tests/apps/tests.py @@ -148,9 +148,11 @@ class AppsTests(TestCase): Tests that the models in the models.py file were loaded correctly. """ self.assertEqual(apps.get_model("apps", "TotallyNormal"), TotallyNormal) - self.assertEqual(apps.get_model("apps", "SoAlternative"), None) + with self.assertRaises(LookupError): + apps.get_model("apps", "SoAlternative") - self.assertEqual(new_apps.get_model("apps", "TotallyNormal"), None) + with self.assertRaises(LookupError): + new_apps.get_model("apps", "TotallyNormal") self.assertEqual(new_apps.get_model("apps", "SoAlternative"), SoAlternative) def test_dynamic_load(self): @@ -174,4 +176,6 @@ class AppsTests(TestCase): old_models, apps.get_models(apps.get_app_config("apps").models_module), ) + with self.assertRaises(LookupError): + apps.get_model("apps", "SouthPonies") self.assertEqual(new_apps.get_model("apps", "SouthPonies"), temp_model)