Skip to content

Commit

Permalink
Fixed #21275 -- Migration serializer look for a deconstruct method on…
Browse files Browse the repository at this point in the history
… any object.
  • Loading branch information
loic committed Oct 21, 2013
1 parent 8d6953d commit 92e9e0c
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 3 deletions.
2 changes: 1 addition & 1 deletion django/contrib/auth/models.py
Expand Up @@ -362,7 +362,7 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin):
help_text=_('Required. 30 characters or fewer. Letters, numbers and '
'@/./+/-/_ characters'),
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)
last_name = models.CharField(_('last name'), max_length=30, blank=True)
Expand Down
9 changes: 9 additions & 0 deletions django/core/validators.py
Expand Up @@ -3,6 +3,7 @@
import re

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.encoding import force_text
from django.utils.ipv6 import is_valid_ipv6_address
Expand All @@ -14,6 +15,7 @@
EMPTY_VALUES = (None, '', [], (), {})


@deconstructible
class RegexValidator(object):
regex = ''
message = _('Enter a valid value.')
Expand All @@ -39,6 +41,7 @@ def __call__(self, value):
raise ValidationError(self.message, code=self.code)


@deconstructible
class URLValidator(RegexValidator):
regex = re.compile(
r'^(?:http|ftp)s?://' # http:// or https://
Expand Down Expand Up @@ -77,6 +80,7 @@ def validate_integer(value):
raise ValidationError(_('Enter a valid integer.'), code='invalid')


@deconstructible
class EmailValidator(object):
message = _('Enter a valid email address.')
code = 'invalid'
Expand Down Expand Up @@ -173,6 +177,7 @@ def ip_address_validators(protocol, unpack_ipv4):
validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _('Enter only digits separated by commas.'), 'invalid')


@deconstructible
class BaseValidator(object):
compare = lambda self, a, b: a is not b
clean = lambda self, x: x
Expand All @@ -189,18 +194,21 @@ def __call__(self, value):
raise ValidationError(self.message, code=self.code, params=params)


@deconstructible
class MaxValueValidator(BaseValidator):
compare = lambda self, a, b: a > b
message = _('Ensure this value is less than or equal to %(limit_value)s.')
code = 'max_value'


@deconstructible
class MinValueValidator(BaseValidator):
compare = lambda self, a, b: a < b
message = _('Ensure this value is greater than or equal to %(limit_value)s.')
code = 'min_value'


@deconstructible
class MinLengthValidator(BaseValidator):
compare = lambda self, a, b: a < b
clean = lambda self, x: len(x)
Expand All @@ -211,6 +219,7 @@ class MinLengthValidator(BaseValidator):
code = 'min_length'


@deconstructible
class MaxLengthValidator(BaseValidator):
compare = lambda self, a, b: a > b
clean = lambda self, x: len(x)
Expand Down
5 changes: 3 additions & 2 deletions django/db/migrations/writer.py
Expand Up @@ -146,15 +146,16 @@ def serialize(cls, value):
elif isinstance(value, models.Field):
attr_name, path, args, kwargs = value.deconstruct()
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
elif isinstance(value, (types.FunctionType, types.BuiltinFunctionType)):
# @classmethod?
if getattr(value, "__self__", None) and isinstance(value.__self__, type):
klass = value.__self__
module = klass.__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>':
raise ValueError("Cannot serialize function: lambda")
elif value.__module__ is None:
Expand Down
35 changes: 35 additions & 0 deletions django/utils/deconstruct.py
@@ -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)
14 changes: 14 additions & 0 deletions tests/migrations/test_writer.py
Expand Up @@ -6,11 +6,13 @@
import datetime
import os

from django.core.validators import RegexValidator, EmailValidator
from django.db import models, migrations
from django.db.migrations.writer import MigrationWriter
from django.db.models.loading import cache
from django.test import TestCase, override_settings
from django.utils import six
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _


Expand Down Expand Up @@ -77,6 +79,18 @@ def test_serialize(self):
self.assertSerializedEqual(datetime.datetime.today)
self.assertSerializedEqual(datetime.date.today())
self.assertSerializedEqual(datetime.date.today)
# Class
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
self.assertSerializedFieldEqual(models.CharField(max_length=255))
self.assertSerializedFieldEqual(models.TextField(null=True, blank=True))
Expand Down

0 comments on commit 92e9e0c

Please sign in to comment.