Fixed #22436: More careful checking on method ref'ce serialization

This commit is contained in:
Andrew Godwin 2014-06-07 17:04:58 -07:00
parent 250e2b422b
commit 6fd455adfc
3 changed files with 68 additions and 6 deletions

View File

@ -12,7 +12,7 @@ import types
from django.apps import apps from django.apps import apps
from django.db import models from django.db import models
from django.db.migrations.loader import MigrationLoader from django.db.migrations.loader import MigrationLoader
from django.utils import datetime_safe, six from django.utils import datetime_safe, six, importlib
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.functional import Promise from django.utils.functional import Promise
@ -284,13 +284,29 @@ class MigrationWriter(object):
klass = value.__self__ klass = value.__self__
module = klass.__module__ module = klass.__module__
return "%s.%s.%s" % (module, klass.__name__, value.__name__), set(["import %s" % module]) return "%s.%s.%s" % (module, klass.__name__, value.__name__), set(["import %s" % module])
elif value.__name__ == '<lambda>': # Further error checking
if value.__name__ == '<lambda>':
raise ValueError("Cannot serialize function: lambda") raise ValueError("Cannot serialize function: lambda")
elif value.__module__ is None: if value.__module__ is None:
raise ValueError("Cannot serialize function %r: No module" % value) raise ValueError("Cannot serialize function %r: No module" % value)
else: # Python 3 is a lot easier, and only uses this branch if it's not local.
module = value.__module__ if getattr(value, "__qualname__", None) and getattr(value, "__module__", None):
return "%s.%s" % (module, value.__name__), set(["import %s" % module]) if "<" not in value.__qualname__: # Qualname can include <locals>
return "%s.%s" % (value.__module__, value.__qualname__), set(["import %s" % value.__module__])
# Python 2/fallback version
module_name = value.__module__
# Make sure it's actually there and not an unbound method
module = importlib.import_module(module_name)
if not hasattr(module, value.__name__):
raise ValueError(
"Could not find function %s in %s.\nPlease note that "
"due to Python 2 limitations, you cannot serialize "
"unbound method functions (e.g. a method declared\n"
"and used in the same class body). Please move the "
"function into the main module body to use migrations.\n"
"For more information, see https://docs.djangoproject.com/en/1.7/topics/migrations/#serializing-values"
)
return "%s.%s" % (module_name, value.__name__), set(["import %s" % module_name])
# Classes # Classes
elif isinstance(value, type): elif isinstance(value, type):
special_cases = [ special_cases = [

View File

@ -491,11 +491,30 @@ Django can serialize the following:
- Any class reference - Any class reference
- Anything with a custom ``deconstruct()`` method (:ref:`see below <custom-deconstruct-method>`) - Anything with a custom ``deconstruct()`` method (:ref:`see below <custom-deconstruct-method>`)
Django can serialize the following on Python 3 only:
- Unbound methods used from within the class body (see below)
Django cannot serialize: Django cannot serialize:
- Arbitrary class instances (e.g. ``MyClass(4.3, 5.7)``) - Arbitrary class instances (e.g. ``MyClass(4.3, 5.7)``)
- Lambdas - Lambdas
Due to the fact ``__qualname__`` was only introduced in Python 3, Django can only
serialize the following pattern (an unbound method used within the class body)
on Python 3, and will fail to serialize a reference to it on Python 2::
class MyModel(models.Model):
def upload_to(self):
return "something dynamic"
my_file = models.FileField(upload_to=upload_to)
If you are using Python 2, we recommend you move your methods for upload_to
and similar arguments that accept callables (e.g. ``default``) to live in
the main module body, rather than the class body.
.. _custom-deconstruct-method: .. _custom-deconstruct-method:
Adding a deconstruct() method Adding a deconstruct() method

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import datetime import datetime
import os import os
import tokenize import tokenize
import unittest
from django.core.validators import RegexValidator, EmailValidator from django.core.validators import RegexValidator, EmailValidator
from django.db import models, migrations from django.db import models, migrations
@ -16,6 +17,12 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import get_default_timezone from django.utils.timezone import get_default_timezone
class TestModel1(object):
def upload_to(self):
return "somewhere dynamic"
thing = models.FileField(upload_to=upload_to)
class WriterTests(TestCase): class WriterTests(TestCase):
""" """
Tests the migration writer (makes migration files from Migration instances) Tests the migration writer (makes migration files from Migration instances)
@ -137,6 +144,26 @@ class WriterTests(TestCase):
self.assertSerializedEqual(one_item_tuple) self.assertSerializedEqual(one_item_tuple)
self.assertSerializedEqual(many_items_tuple) self.assertSerializedEqual(many_items_tuple)
@unittest.skipUnless(six.PY2, "Only applies on Python 2")
def test_serialize_direct_function_reference(self):
"""
Ticket #22436: You cannot use a function straight from its body
(e.g. define the method and use it in the same body)
"""
with self.assertRaises(ValueError):
self.serialize_round_trip(TestModel1.thing)
def test_serialize_local_function_reference(self):
"""
Neither py2 or py3 can serialize a reference in a local scope.
"""
class TestModel2(object):
def upload_to(self):
return "somewhere dynamic"
thing = models.FileField(upload_to=upload_to)
with self.assertRaises(ValueError):
self.serialize_round_trip(TestModel2.thing)
def test_simple_migration(self): def test_simple_migration(self):
""" """
Tests serializing a simple migration. Tests serializing a simple migration.