From e565e1332ddfbb44fe7e6139375e3c243af7398d Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Tue, 22 Oct 2013 00:33:57 +0700 Subject: [PATCH] Fixed #21275 -- Fixed a serializer error when generating migrations for contrib.auth. The migration serializer now looks for a deconstruct method on any object. --- django/contrib/auth/models.py | 2 +- django/core/validators.py | 9 +++++++++ django/db/migrations/writer.py | 5 +++-- django/utils/deconstruct.py | 35 +++++++++++++++++++++++++++++++++ tests/migrations/test_writer.py | 14 +++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 django/utils/deconstruct.py diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 950d854d5e5..75107688d74 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -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) diff --git a/django/core/validators.py b/django/core/validators.py index 78716ccd406..796c57e4b0b 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals 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 @@ -14,6 +15,7 @@ from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit EMPTY_VALUES = (None, '', [], (), {}) +@deconstructible class RegexValidator(object): regex = '' message = _('Enter a valid value.') @@ -39,6 +41,7 @@ class RegexValidator(object): raise ValidationError(self.message, code=self.code) +@deconstructible class URLValidator(RegexValidator): regex = re.compile( r'^(?:http|ftp)s?://' # http:// or https:// @@ -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' @@ -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') +@deconstructible class BaseValidator(object): compare = lambda self, a, b: a is not b clean = lambda self, x: x @@ -189,18 +194,21 @@ class BaseValidator(object): 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) @@ -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) diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index 0ee737811dd..22b0977ba40 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -146,6 +146,9 @@ class MigrationWriter(object): 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? @@ -153,8 +156,6 @@ class MigrationWriter(object): 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__ == '': raise ValueError("Cannot serialize function: lambda") elif value.__module__ is None: diff --git a/django/utils/deconstruct.py b/django/utils/deconstruct.py new file mode 100644 index 00000000000..7774e69997d --- /dev/null +++ b/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) diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 3e6fe507938..0d64d403503 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -6,11 +6,13 @@ import copy 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 _ @@ -77,6 +79,18 @@ class WriterTests(TestCase): self.assertSerializedEqual(datetime.datetime.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 self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedFieldEqual(models.TextField(null=True, blank=True))