Repository: adamchainz/django-mysql

Issue:
Subclasses of Set/List fields have broken deconstruct() methods
Analogue of https://github.com/django/django/commit/7c50e7eeccd4b4581542547f5ddb603bbd62a725 - I copy/pasta'd this from `django.contrib.postgres`.



Discussion:

Resolving patch:
diff --git a/django_mysql/models/fields/lists.py b/django_mysql/models/fields/lists.py
--- a/django_mysql/models/fields/lists.py
+++ b/django_mysql/models/fields/lists.py
@@ -73,7 +73,14 @@ def set_attributes_from_name(self, name):
 
     def deconstruct(self):
         name, path, args, kwargs = super(ListFieldMixin, self).deconstruct()
-        path = 'django_mysql.models.%s' % self.__class__.__name__
+
+        bad_paths = (
+            'django_mysql.models.fields.lists.' + self.__class__.__name__,
+            'django_mysql.models.fields.' + self.__class__.__name__
+        )
+        if path in bad_paths:
+            path = 'django_mysql.models.' + self.__class__.__name__
+
         args.insert(0, self.base_field)
         kwargs['size'] = self.size
         return name, path, args, kwargs
diff --git a/django_mysql/models/fields/sets.py b/django_mysql/models/fields/sets.py
--- a/django_mysql/models/fields/sets.py
+++ b/django_mysql/models/fields/sets.py
@@ -72,7 +72,14 @@ def set_attributes_from_name(self, name):
 
     def deconstruct(self):
         name, path, args, kwargs = super(SetFieldMixin, self).deconstruct()
-        path = 'django_mysql.models.%s' % self.__class__.__name__
+
+        bad_paths = (
+            'django_mysql.models.fields.sets.' + self.__class__.__name__,
+            'django_mysql.models.fields.' + self.__class__.__name__
+        )
+        if path in bad_paths:
+            path = 'django_mysql.models.' + self.__class__.__name__
+
         args.insert(0, self.base_field)
         kwargs['size'] = self.size
         return name, path, args, kwargs
diff --git a/django_mysql/models/fields/sizes.py b/django_mysql/models/fields/sizes.py
--- a/django_mysql/models/fields/sizes.py
+++ b/django_mysql/models/fields/sizes.py
@@ -24,7 +24,14 @@ def check(self, **kwargs):
 
     def deconstruct(self):
         name, path, args, kwargs = super(SizedBinaryField, self).deconstruct()
-        path = 'django_mysql.models.%s' % self.__class__.__name__
+
+        bad_paths = (
+            'django_mysql.models.fields.sizes.' + self.__class__.__name__,
+            'django_mysql.models.fields.' + self.__class__.__name__
+        )
+        if path in bad_paths:
+            path = 'django_mysql.models.' + self.__class__.__name__
+
         kwargs['size_class'] = self.size_class
         return name, path, args, kwargs
 
@@ -59,7 +66,14 @@ def check(self, **kwargs):
 
     def deconstruct(self):
         name, path, args, kwargs = super(SizedTextField, self).deconstruct()
-        path = 'django_mysql.models.%s' % self.__class__.__name__
+
+        bad_paths = (
+            'django_mysql.models.fields.sizes.' + self.__class__.__name__,
+            'django_mysql.models.fields.' + self.__class__.__name__
+        )
+        if path in bad_paths:
+            path = 'django_mysql.models.' + self.__class__.__name__
+
         kwargs['size_class'] = self.size_class
         return name, path, args, kwargs
 


Test patch:
diff --git a/tests/testapp/test_listtextfield.py b/tests/testapp/test_listtextfield.py
--- a/tests/testapp/test_listtextfield.py
+++ b/tests/testapp/test_listtextfield.py
@@ -261,7 +261,13 @@ class InvalidListTextModel(TemporaryModel):
         assert 'Base field for list must be' in errors[0].msg
 
 
-class TestMigrations(TransactionTestCase):
+class ListTextFieldSubclass(ListTextField):
+    """
+    Used below, has a different path for deconstruct()
+    """
+
+
+class TestDeconstruct(TestCase):
 
     def test_deconstruct(self):
         field = ListTextField(models.IntegerField(), max_length=32)
@@ -281,6 +287,26 @@ def test_deconstruct_args(self):
         new = ListTextField(*args, **kwargs)
         assert new.base_field.max_length == field.base_field.max_length
 
+    def test_bad_import_deconstruct(self):
+        from django_mysql.models.fields import ListTextField as LTField
+        field = LTField(models.IntegerField())
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'django_mysql.models.ListTextField'
+
+    def test_bad_import2_deconstruct(self):
+        from django_mysql.models.fields.lists import ListTextField as LTField
+        field = LTField(models.IntegerField())
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'django_mysql.models.ListTextField'
+
+    def test_subclass_deconstruct(self):
+        field = ListTextFieldSubclass(models.IntegerField())
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'tests.testapp.test_listtextfield.ListTextFieldSubclass'
+
+
+class TestMigrations(TransactionTestCase):
+
     def test_makemigrations(self):
         field = ListTextField(models.CharField(max_length=5), max_length=32)
         statement, imports = MigrationWriter.serialize(field)
diff --git a/tests/testapp/test_settextfield.py b/tests/testapp/test_settextfield.py
--- a/tests/testapp/test_settextfield.py
+++ b/tests/testapp/test_settextfield.py
@@ -235,7 +235,13 @@ class InvalidSetTextModel(TemporaryModel):
         assert 'Base field for set must be' in errors[0].msg
 
 
-class TestMigrations(TransactionTestCase):
+class SetTextFieldSubclass(SetTextField):
+    """
+    Used below, has a different path for deconstruct()
+    """
+
+
+class TestDeconstruct(TestCase):
 
     def test_deconstruct(self):
         field = SetTextField(models.IntegerField(), max_length=32)
@@ -255,6 +261,26 @@ def test_deconstruct_args(self):
         new = SetTextField(*args, **kwargs)
         assert new.base_field.max_length == field.base_field.max_length
 
+    def test_bad_import_deconstruct(self):
+        from django_mysql.models.fields import SetTextField as STField
+        field = STField(models.IntegerField())
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'django_mysql.models.SetTextField'
+
+    def test_bad_import2_deconstruct(self):
+        from django_mysql.models.fields.sets import SetTextField as STField
+        field = STField(models.IntegerField())
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'django_mysql.models.SetTextField'
+
+    def test_subclass_deconstruct(self):
+        field = SetTextFieldSubclass(models.IntegerField())
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'tests.testapp.test_settextfield.SetTextFieldSubclass'
+
+
+class TestMigrations(TransactionTestCase):
+
     def test_makemigrations(self):
         field = SetTextField(models.CharField(max_length=5))
         statement, imports = MigrationWriter.serialize(field)
diff --git a/tests/testapp/test_size_fields.py b/tests/testapp/test_size_fields.py
--- a/tests/testapp/test_size_fields.py
+++ b/tests/testapp/test_size_fields.py
@@ -25,6 +25,12 @@ def migrate(name):
                  verbosity=0, skip_checks=True)
 
 
+class SubSizedBinaryField(SizedBinaryField):
+    """
+    Used below, has a different path for deconstruct()
+    """
+
+
 @forceDataError
 class SizedBinaryFieldTests(TestCase):
 
@@ -55,6 +61,16 @@ def test_binary_1_max_length(self):
             m.save()
         assert excinfo.value.args[0] == 1406
 
+    def test_deconstruct_path(self):
+        field = SizedBinaryField(size_class=1)
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'django_mysql.models.SizedBinaryField'
+
+    def test_deconstruct_subclass_path(self):
+        field = SubSizedBinaryField(size_class=1)
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'tests.testapp.test_size_fields.SubSizedBinaryField'
+
     def test_deconstruct_size_class_4(self):
         field = SizedBinaryField(size_class=4)
         name, path, args, kwargs = field.deconstruct()
@@ -119,6 +135,12 @@ def test_adding_field_with_default(self):
             assert table_name not in table_names(cursor)
 
 
+class SubSizedTextField(SizedTextField):
+    """
+    Used below, has a different path for deconstruct()
+    """
+
+
 @forceDataError
 class SizedTextFieldTests(TestCase):
 
@@ -148,6 +170,16 @@ def test_tinytext_max_length(self):
             m.save()
         assert excinfo.value.args[0] == 1406
 
+    def test_deconstruct_path(self):
+        field = SizedTextField(size_class=1)
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'django_mysql.models.SizedTextField'
+
+    def test_deconstruct_subclass_path(self):
+        field = SubSizedTextField(size_class=1)
+        name, path, args, kwargs = field.deconstruct()
+        assert path == 'tests.testapp.test_size_fields.SubSizedTextField'
+
     def test_deconstruct_size_class_4(self):
         field = SizedTextField(size_class=4)
         name, path, args, kwargs = field.deconstruct()