Fixed #21275 -- Fixed a serializer error when generating migrations for contrib.auth.

The migration serializer now looks for a deconstruct method on any object.
This commit is contained in:
Loic Bistuer 2013-10-22 00:33:57 +07:00 committed by Tim Graham
parent 28b70425af
commit e565e1332d
5 changed files with 62 additions and 3 deletions

View File

@ -362,7 +362,7 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin):
help_text=_('Required. 30 characters or fewer. Letters, numbers and ' help_text=_('Required. 30 characters or fewer. Letters, numbers and '
'@/./+/-/_ characters'), '@/./+/-/_ characters'),
validators=[ validators=[
validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid') validators.RegexValidator(r'^[\w.@+-]+$', _('Enter a valid username.'), 'invalid')
]) ])
first_name = models.CharField(_('first name'), max_length=30, blank=True) first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=30, blank=True) last_name = models.CharField(_('last name'), max_length=30, blank=True)

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import re import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _, ungettext_lazy from django.utils.translation import ugettext_lazy as _, ungettext_lazy
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.ipv6 import is_valid_ipv6_address from django.utils.ipv6 import is_valid_ipv6_address
@ -14,6 +15,7 @@ from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit
EMPTY_VALUES = (None, '', [], (), {}) EMPTY_VALUES = (None, '', [], (), {})
@deconstructible
class RegexValidator(object): class RegexValidator(object):
regex = '' regex = ''
message = _('Enter a valid value.') message = _('Enter a valid value.')
@ -39,6 +41,7 @@ class RegexValidator(object):
raise ValidationError(self.message, code=self.code) raise ValidationError(self.message, code=self.code)
@deconstructible
class URLValidator(RegexValidator): class URLValidator(RegexValidator):
regex = re.compile( regex = re.compile(
r'^(?:http|ftp)s?://' # http:// or https:// r'^(?:http|ftp)s?://' # http:// or https://
@ -77,6 +80,7 @@ def validate_integer(value):
raise ValidationError(_('Enter a valid integer.'), code='invalid') raise ValidationError(_('Enter a valid integer.'), code='invalid')
@deconstructible
class EmailValidator(object): class EmailValidator(object):
message = _('Enter a valid email address.') message = _('Enter a valid email address.')
code = 'invalid' code = 'invalid'
@ -173,6 +177,7 @@ comma_separated_int_list_re = re.compile('^[\d,]+$')
validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _('Enter only digits separated by commas.'), 'invalid') validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _('Enter only digits separated by commas.'), 'invalid')
@deconstructible
class BaseValidator(object): class BaseValidator(object):
compare = lambda self, a, b: a is not b compare = lambda self, a, b: a is not b
clean = lambda self, x: x clean = lambda self, x: x
@ -189,18 +194,21 @@ class BaseValidator(object):
raise ValidationError(self.message, code=self.code, params=params) raise ValidationError(self.message, code=self.code, params=params)
@deconstructible
class MaxValueValidator(BaseValidator): class MaxValueValidator(BaseValidator):
compare = lambda self, a, b: a > b compare = lambda self, a, b: a > b
message = _('Ensure this value is less than or equal to %(limit_value)s.') message = _('Ensure this value is less than or equal to %(limit_value)s.')
code = 'max_value' code = 'max_value'
@deconstructible
class MinValueValidator(BaseValidator): class MinValueValidator(BaseValidator):
compare = lambda self, a, b: a < b compare = lambda self, a, b: a < b
message = _('Ensure this value is greater than or equal to %(limit_value)s.') message = _('Ensure this value is greater than or equal to %(limit_value)s.')
code = 'min_value' code = 'min_value'
@deconstructible
class MinLengthValidator(BaseValidator): class MinLengthValidator(BaseValidator):
compare = lambda self, a, b: a < b compare = lambda self, a, b: a < b
clean = lambda self, x: len(x) clean = lambda self, x: len(x)
@ -211,6 +219,7 @@ class MinLengthValidator(BaseValidator):
code = 'min_length' code = 'min_length'
@deconstructible
class MaxLengthValidator(BaseValidator): class MaxLengthValidator(BaseValidator):
compare = lambda self, a, b: a > b compare = lambda self, a, b: a > b
clean = lambda self, x: len(x) clean = lambda self, x: len(x)

View File

@ -146,6 +146,9 @@ class MigrationWriter(object):
elif isinstance(value, models.Field): elif isinstance(value, models.Field):
attr_name, path, args, kwargs = value.deconstruct() attr_name, path, args, kwargs = value.deconstruct()
return cls.serialize_deconstructed(path, args, kwargs) return cls.serialize_deconstructed(path, args, kwargs)
# Anything that knows how to deconstruct itself.
elif hasattr(value, 'deconstruct'):
return cls.serialize_deconstructed(*value.deconstruct())
# Functions # Functions
elif isinstance(value, (types.FunctionType, types.BuiltinFunctionType)): elif isinstance(value, (types.FunctionType, types.BuiltinFunctionType)):
# @classmethod? # @classmethod?
@ -153,8 +156,6 @@ 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 hasattr(value, 'deconstruct'):
return cls.serialize_deconstructed(*value.deconstruct())
elif value.__name__ == '<lambda>': elif value.__name__ == '<lambda>':
raise ValueError("Cannot serialize function: lambda") raise ValueError("Cannot serialize function: lambda")
elif value.__module__ is None: elif value.__module__ is None:

View File

@ -0,0 +1,35 @@
def deconstructible(*args, **kwargs):
"""
Class decorator that allow the decorated class to be serialized
by the migrations subsystem.
Accepts an optional kwarg `path` to specify the import path.
"""
path = kwargs.pop('path', None)
def decorator(klass):
def __new__(cls, *args, **kwargs):
# We capture the arguments to make returning them trivial
obj = super(klass, cls).__new__(cls)
obj._constructor_args = (args, kwargs)
return obj
def deconstruct(obj):
"""
Returns a 3-tuple of class import path, positional arguments,
and keyword arguments.
"""
return (
path or '%s.%s' % (obj.__class__.__module__, obj.__class__.__name__),
obj._constructor_args[0],
obj._constructor_args[1],
)
klass.__new__ = staticmethod(__new__)
klass.deconstruct = deconstruct
return klass
if not args:
return decorator
return decorator(*args, **kwargs)

View File

@ -6,11 +6,13 @@ import copy
import datetime import datetime
import os import os
from django.core.validators import RegexValidator, EmailValidator
from django.db import models, migrations from django.db import models, migrations
from django.db.migrations.writer import MigrationWriter from django.db.migrations.writer import MigrationWriter
from django.db.models.loading import cache from django.db.models.loading import cache
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils import six from django.utils import six
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -77,6 +79,18 @@ class WriterTests(TestCase):
self.assertSerializedEqual(datetime.datetime.today) self.assertSerializedEqual(datetime.datetime.today)
self.assertSerializedEqual(datetime.date.today()) self.assertSerializedEqual(datetime.date.today())
self.assertSerializedEqual(datetime.date.today) self.assertSerializedEqual(datetime.date.today)
# Classes
validator = RegexValidator(message="hello")
string, imports = MigrationWriter.serialize(validator)
self.assertEqual(string, "django.core.validators.RegexValidator(message=%s)" % repr("hello"))
self.serialize_round_trip(validator)
validator = EmailValidator(message="hello") # Test with a subclass.
string, imports = MigrationWriter.serialize(validator)
self.assertEqual(string, "django.core.validators.EmailValidator(message=%s)" % repr("hello"))
self.serialize_round_trip(validator)
validator = deconstructible(path="custom.EmailValidator")(EmailValidator)(message="hello")
string, imports = MigrationWriter.serialize(validator)
self.assertEqual(string, "custom.EmailValidator(message=%s)" % repr("hello"))
# Django fields # Django fields
self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedFieldEqual(models.CharField(max_length=255))
self.assertSerializedFieldEqual(models.TextField(null=True, blank=True)) self.assertSerializedFieldEqual(models.TextField(null=True, blank=True))