Fixed #24340 -- Added nested deconstruction for list, tuple and dict values
Nested deconstruction should recursively deconstruct items within list, tuple and dict values.
This commit is contained in:
parent
14f20c1fdc
commit
ff8a02ae0b
|
@ -52,21 +52,35 @@ class MigrationAutodetector(object):
|
||||||
Used for full comparison for rename/alter; sometimes a single-level
|
Used for full comparison for rename/alter; sometimes a single-level
|
||||||
deconstruction will not compare correctly.
|
deconstruction will not compare correctly.
|
||||||
"""
|
"""
|
||||||
if not hasattr(obj, 'deconstruct') or isinstance(obj, type):
|
if isinstance(obj, list):
|
||||||
return obj
|
return [self.deep_deconstruct(value) for value in obj]
|
||||||
deconstructed = obj.deconstruct()
|
elif isinstance(obj, tuple):
|
||||||
if isinstance(obj, models.Field):
|
return tuple(self.deep_deconstruct(value) for value in obj)
|
||||||
# we have a field which also returns a name
|
elif isinstance(obj, dict):
|
||||||
deconstructed = deconstructed[1:]
|
return {
|
||||||
path, args, kwargs = deconstructed
|
|
||||||
return (
|
|
||||||
path,
|
|
||||||
[self.deep_deconstruct(value) for value in args],
|
|
||||||
{
|
|
||||||
key: self.deep_deconstruct(value)
|
key: self.deep_deconstruct(value)
|
||||||
for key, value in kwargs.items()
|
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
|
||||||
|
deconstructed = deconstructed[1:]
|
||||||
|
path, args, kwargs = deconstructed
|
||||||
|
return (
|
||||||
|
path,
|
||||||
|
[self.deep_deconstruct(value) for value in args],
|
||||||
|
{
|
||||||
|
key: self.deep_deconstruct(value)
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return obj
|
||||||
|
|
||||||
def only_relation_agnostic_fields(self, fields):
|
def only_relation_agnostic_fields(self, fields):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -17,8 +17,16 @@ class DeconstructableObject(object):
|
||||||
A custom deconstructable object.
|
A custom deconstructable object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def deconstruct(self):
|
def deconstruct(self):
|
||||||
return self.__module__ + '.' + self.__class__.__name__, [], {}
|
return (
|
||||||
|
self.__module__ + '.' + self.__class__.__name__,
|
||||||
|
self.args,
|
||||||
|
self.kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AutodetectorTests(TestCase):
|
class AutodetectorTests(TestCase):
|
||||||
|
@ -63,6 +71,104 @@ class AutodetectorTests(TestCase):
|
||||||
("id", models.AutoField(primary_key=True)),
|
("id", models.AutoField(primary_key=True)),
|
||||||
("name", models.CharField(max_length=200, default=models.IntegerField())),
|
("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_custom_pk = ModelState("testapp", "Author", [("pk_field", models.IntegerField(primary_key=True))])
|
||||||
author_with_biography_non_blank = ModelState("testapp", "Author", [
|
author_with_biography_non_blank = ModelState("testapp", "Author", [
|
||||||
("id", models.AutoField(primary_key=True)),
|
("id", models.AutoField(primary_key=True)),
|
||||||
|
@ -1225,6 +1331,107 @@ class AutodetectorTests(TestCase):
|
||||||
changes = autodetector._detect_changes()
|
changes = autodetector._detect_changes()
|
||||||
self.assertEqual(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):
|
def test_deconstruct_type(self):
|
||||||
"""
|
"""
|
||||||
#22951 -- Uninstanted classes with deconstruct are correctly returned
|
#22951 -- Uninstanted classes with deconstruct are correctly returned
|
||||||
|
|
Loading…
Reference in New Issue