Fixed #22436: More careful checking on method ref'ce serialization
This commit is contained in:
parent
250e2b422b
commit
6fd455adfc
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue