Commit ff8a02ae authored by Matt Westcott's avatar Matt Westcott Committed by Markus Holtermann
Browse files

Fixed #24340 -- Added nested deconstruction for list, tuple and dict values

Nested deconstruction should recursively deconstruct items within list,
tuple and dict values.
parent 14f20c1f
Loading
Loading
Loading
Loading
+28 −14
Original line number Diff line number Diff line
@@ -52,8 +52,20 @@ class MigrationAutodetector(object):
        Used for full comparison for rename/alter; sometimes a single-level
        deconstruction will not compare correctly.
        """
        if not hasattr(obj, 'deconstruct') or isinstance(obj, type):
        if isinstance(obj, list):
            return [self.deep_deconstruct(value) for value in obj]
        elif isinstance(obj, tuple):
            return tuple(self.deep_deconstruct(value) for value in obj)
        elif isinstance(obj, dict):
            return {
                key: self.deep_deconstruct(value)
                for key, value in obj.items()
            }
        elif isinstance(obj, type):
            # If this is a type that implements 'deconstruct' as an instance method,
            # avoid treating this as being deconstructible itself - see #22951
            return obj
        elif hasattr(obj, 'deconstruct'):
            deconstructed = obj.deconstruct()
            if isinstance(obj, models.Field):
                # we have a field which also returns a name
@@ -67,6 +79,8 @@ class MigrationAutodetector(object):
                    for key, value in kwargs.items()
                },
            )
        else:
            return obj

    def only_relation_agnostic_fields(self, fields):
        """
+208 −1
Original line number Diff line number Diff line
@@ -17,8 +17,16 @@ class DeconstructableObject(object):
    A custom deconstructable object.
    """

    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

    def deconstruct(self):
        return self.__module__ + '.' + self.__class__.__name__, [], {}
        return (
            self.__module__ + '.' + self.__class__.__name__,
            self.args,
            self.kwargs
        )


class AutodetectorTests(TestCase):
@@ -63,6 +71,104 @@ class AutodetectorTests(TestCase):
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=models.IntegerField())),
    ])
    author_name_deconstructable_list_1 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=[DeconstructableObject(), 123])),
    ])
    author_name_deconstructable_list_2 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=[DeconstructableObject(), 123])),
    ])
    author_name_deconstructable_list_3 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=[DeconstructableObject(), 999])),
    ])
    author_name_deconstructable_tuple_1 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=(DeconstructableObject(), 123))),
    ])
    author_name_deconstructable_tuple_2 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=(DeconstructableObject(), 123))),
    ])
    author_name_deconstructable_tuple_3 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=(DeconstructableObject(), 999))),
    ])
    author_name_deconstructable_dict_1 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default={
            'item': DeconstructableObject(), 'otheritem': 123
        })),
    ])
    author_name_deconstructable_dict_2 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default={
            'item': DeconstructableObject(), 'otheritem': 123
        })),
    ])
    author_name_deconstructable_dict_3 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default={
            'item': DeconstructableObject(), 'otheritem': 999
        })),
    ])
    author_name_nested_deconstructable_1 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=DeconstructableObject(
            DeconstructableObject(1),
            (DeconstructableObject('t1'), DeconstructableObject('t2'),),
            a=DeconstructableObject('A'),
            b=DeconstructableObject(B=DeconstructableObject('c')),
        ))),
    ])
    author_name_nested_deconstructable_2 = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=DeconstructableObject(
            DeconstructableObject(1),
            (DeconstructableObject('t1'), DeconstructableObject('t2'),),
            a=DeconstructableObject('A'),
            b=DeconstructableObject(B=DeconstructableObject('c')),
        ))),
    ])
    author_name_nested_deconstructable_changed_arg = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=DeconstructableObject(
            DeconstructableObject(1),
            (DeconstructableObject('t1'), DeconstructableObject('t2-changed'),),
            a=DeconstructableObject('A'),
            b=DeconstructableObject(B=DeconstructableObject('c')),
        ))),
    ])
    author_name_nested_deconstructable_extra_arg = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=DeconstructableObject(
            DeconstructableObject(1),
            (DeconstructableObject('t1'), DeconstructableObject('t2'),),
            None,
            a=DeconstructableObject('A'),
            b=DeconstructableObject(B=DeconstructableObject('c')),
        ))),
    ])
    author_name_nested_deconstructable_changed_kwarg = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=DeconstructableObject(
            DeconstructableObject(1),
            (DeconstructableObject('t1'), DeconstructableObject('t2'),),
            a=DeconstructableObject('A'),
            b=DeconstructableObject(B=DeconstructableObject('c-changed')),
        ))),
    ])
    author_name_nested_deconstructable_extra_kwarg = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
        ("name", models.CharField(max_length=200, default=DeconstructableObject(
            DeconstructableObject(1),
            (DeconstructableObject('t1'), DeconstructableObject('t2'),),
            a=DeconstructableObject('A'),
            b=DeconstructableObject(B=DeconstructableObject('c')),
            c=None,
        ))),
    ])
    author_custom_pk = ModelState("testapp", "Author", [("pk_field", models.IntegerField(primary_key=True))])
    author_with_biography_non_blank = ModelState("testapp", "Author", [
        ("id", models.AutoField(primary_key=True)),
@@ -1225,6 +1331,107 @@ class AutodetectorTests(TestCase):
        changes = autodetector._detect_changes()
        self.assertEqual(changes, {})

    def test_deconstructable_list(self):
        """Nested deconstruction descends into lists."""
        # When lists contain items that deconstruct to identical values, those lists
        # should be considered equal for the purpose of detecting state changes
        # (even if the original items are unequal).
        before = self.make_project_state([self.author_name_deconstructable_list_1])
        after = self.make_project_state([self.author_name_deconstructable_list_2])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(changes, {})

        # Legitimate differences within the deconstructed lists should be reported
        # as a change
        before = self.make_project_state([self.author_name_deconstructable_list_1])
        after = self.make_project_state([self.author_name_deconstructable_list_3])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(len(changes), 1)

    def test_deconstructable_tuple(self):
        """Nested deconstruction descends into tuples."""
        # When tuples contain items that deconstruct to identical values, those tuples
        # should be considered equal for the purpose of detecting state changes
        # (even if the original items are unequal).
        before = self.make_project_state([self.author_name_deconstructable_tuple_1])
        after = self.make_project_state([self.author_name_deconstructable_tuple_2])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(changes, {})

        # Legitimate differences within the deconstructed tuples should be reported
        # as a change
        before = self.make_project_state([self.author_name_deconstructable_tuple_1])
        after = self.make_project_state([self.author_name_deconstructable_tuple_3])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(len(changes), 1)

    def test_deconstructable_dict(self):
        """Nested deconstruction descends into dict values."""
        # When dicts contain items whose values deconstruct to identical values,
        # those dicts should be considered equal for the purpose of detecting
        # state changes (even if the original values are unequal).
        before = self.make_project_state([self.author_name_deconstructable_dict_1])
        after = self.make_project_state([self.author_name_deconstructable_dict_2])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(changes, {})

        # Legitimate differences within the deconstructed dicts should be reported
        # as a change
        before = self.make_project_state([self.author_name_deconstructable_dict_1])
        after = self.make_project_state([self.author_name_deconstructable_dict_3])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(len(changes), 1)

    def test_nested_deconstructable_objects(self):
        """
        Nested deconstruction is applied recursively to the args/kwargs of
        deconstructed objects.
        """
        # If the items within a deconstructed object's args/kwargs have the same
        # deconstructed values - whether or not the items themselves are different
        # instances - then the object as a whole is regarded as unchanged.
        before = self.make_project_state([self.author_name_nested_deconstructable_1])
        after = self.make_project_state([self.author_name_nested_deconstructable_2])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(changes, {})

        # Differences that exist solely within the args list of a deconstructed object
        # should be reported as changes
        before = self.make_project_state([self.author_name_nested_deconstructable_1])
        after = self.make_project_state([self.author_name_nested_deconstructable_changed_arg])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(len(changes), 1)

        # Additional args should also be reported as a change
        before = self.make_project_state([self.author_name_nested_deconstructable_1])
        after = self.make_project_state([self.author_name_nested_deconstructable_extra_arg])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(len(changes), 1)

        # Differences that exist solely within the kwargs dict of a deconstructed object
        # should be reported as changes
        before = self.make_project_state([self.author_name_nested_deconstructable_1])
        after = self.make_project_state([self.author_name_nested_deconstructable_changed_kwarg])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(len(changes), 1)

        # Additional kwargs should also be reported as a change
        before = self.make_project_state([self.author_name_nested_deconstructable_1])
        after = self.make_project_state([self.author_name_nested_deconstructable_extra_kwarg])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertEqual(len(changes), 1)

    def test_deconstruct_type(self):
        """
        #22951 -- Uninstanted classes with deconstruct are correctly returned