Commit 6fd455ad authored by Andrew Godwin's avatar Andrew Godwin
Browse files

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

parent 250e2b42
Loading
Loading
Loading
Loading
+22 −6
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ import types
from django.apps import apps
from django.db import models
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.functional import Promise

@@ -284,13 +284,29 @@ class MigrationWriter(object):
                klass = value.__self__
                module = klass.__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")
            elif value.__module__ is None:
            if value.__module__ is None:
                raise ValueError("Cannot serialize function %r: No module" % value)
            else:
                module = value.__module__
                return "%s.%s" % (module, value.__name__), set(["import %s" % module])
            # Python 3 is a lot easier, and only uses this branch if it's not local.
            if getattr(value, "__qualname__", None) and getattr(value, "__module__", None):
                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
        elif isinstance(value, type):
            special_cases = [
+19 −0
Original line number Diff line number Diff line
@@ -491,11 +491,30 @@ Django can serialize the following:
- Any class reference
- 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:

- Arbitrary class instances (e.g. ``MyClass(4.3, 5.7)``)
- 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:

Adding a deconstruct() method
+27 −0
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
import datetime
import os
import tokenize
import unittest

from django.core.validators import RegexValidator, EmailValidator
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


class TestModel1(object):
    def upload_to(self):
        return "somewhere dynamic"
    thing = models.FileField(upload_to=upload_to)


class WriterTests(TestCase):
    """
    Tests the migration writer (makes migration files from Migration instances)
@@ -137,6 +144,26 @@ class WriterTests(TestCase):
        self.assertSerializedEqual(one_item_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):
        """
        Tests serializing a simple migration.