From e565e1332ddfbb44fe7e6139375e3c243af7398d Mon Sep 17 00:00:00 2001
From: Loic Bistuer <loic.bistuer@sixmedia.com>
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 950d854d5e..75107688d7 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 78716ccd40..796c57e4b0 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 0ee737811d..22b0977ba4 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__ == '<lambda>':
                 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 0000000000..7774e69997
--- /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 3e6fe50793..0d64d40350 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))