diff --git a/django/utils/deconstruct.py b/django/utils/deconstruct.py index 7774e69997..627c2a9c68 100644 --- a/django/utils/deconstruct.py +++ b/django/utils/deconstruct.py @@ -1,3 +1,5 @@ +from importlib import import_module + def deconstructible(*args, **kwargs): """ Class decorator that allow the decorated class to be serialized @@ -19,8 +21,25 @@ def deconstructible(*args, **kwargs): Returns a 3-tuple of class import path, positional arguments, and keyword arguments. """ + # Python 2/fallback version + if path: + module_name, _, name = path.rpartition('.') + else: + module_name = obj.__module__ + name = obj.__class__.__name__ + # Make sure it's actually there and not an inner class + module = import_module(module_name) + if not hasattr(module, name): + raise ValueError( + "Could not find object %s in %s.\n" + "Please note that you cannot serialize things like inner " + "classes. Please move the object into the main module " + "body to use migrations.\n" + "For more information, see " + "https://docs.djangoproject.com/en/dev/topics/migrations/#serializing-values" + % (name, module_name)) return ( - path or '%s.%s' % (obj.__class__.__module__, obj.__class__.__name__), + path or '%s.%s' % (obj.__class__.__module__, name), obj._constructor_args[0], obj._constructor_args[1], ) diff --git a/docs/releases/1.7.1.txt b/docs/releases/1.7.1.txt index 2ed63c41b2..85421f2fa4 100644 --- a/docs/releases/1.7.1.txt +++ b/docs/releases/1.7.1.txt @@ -20,3 +20,6 @@ Bugfixes * Fixed serialization of ``type`` objects in migrations (:ticket:`22951`). * Allowed inline and hidden references to admin fields (:ticket:`23431`). + +* The ``@deconstructible`` decorator now fails with a ``ValueError`` if the + decorated object cannot automatically be imported (:ticket:`23418`). diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 3839f78cbd..f6c866d725 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -167,9 +167,17 @@ class WriterTests(TestCase): self.assertEqual(string, "django.core.validators.EmailValidator(message='hello')") self.serialize_round_trip(validator) - validator = deconstructible(path="custom.EmailValidator")(EmailValidator)(message="hello") + validator = deconstructible(path="migrations.test_writer.EmailValidator")(EmailValidator)(message="hello") string = MigrationWriter.serialize(validator)[0] - self.assertEqual(string, "custom.EmailValidator(message='hello')") + self.assertEqual(string, "migrations.test_writer.EmailValidator(message='hello')") + + validator = deconstructible(path="custom.EmailValidator")(EmailValidator)(message="hello") + with self.assertRaisesMessage(ImportError, "No module named 'custom'"): + MigrationWriter.serialize(validator) + + validator = deconstructible(path="django.core.validators.EmailValidator2")(EmailValidator)(message="hello") + with self.assertRaisesMessage(ValueError, "Could not find object EmailValidator2 in django.core.validators."): + MigrationWriter.serialize(validator) def test_serialize_empty_nonempty_tuple(self): """