From 02966a30dd31d2b9d35f8c481a448b9bf377895e Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Thu, 6 Jul 2023 18:09:21 +0200 Subject: [PATCH] Fixed #34697 -- Fixed non-deterministic order of dependencies and sets/frozensets in migrations. Co-authored-by: Dakota Hawkins --- AUTHORS | 1 + django/db/migrations/serializer.py | 9 +++++++-- django/db/migrations/writer.py | 4 +++- tests/migrations/test_writer.py | 32 ++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 6cafad023a..acdca4ec19 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1043,6 +1043,7 @@ answer newbie questions, and generally made Django that much better: ye7cakf02@sneakemail.com ymasuda@ethercube.com Yoong Kang Lim + Yury V. Zaytsev Yusuke Miyazaki yyyyyyyan Zac Hatfield-Dodds diff --git a/django/db/migrations/serializer.py b/django/db/migrations/serializer.py index 454feaa829..d88cda6e20 100644 --- a/django/db/migrations/serializer.py +++ b/django/db/migrations/serializer.py @@ -46,6 +46,11 @@ class BaseSequenceSerializer(BaseSerializer): return value % (", ".join(strings)), imports +class BaseUnorderedSequenceSerializer(BaseSequenceSerializer): + def __init__(self, value): + super().__init__(sorted(value, key=repr)) + + class BaseSimpleSerializer(BaseSerializer): def serialize(self): return repr(self.value), set() @@ -151,7 +156,7 @@ class FloatSerializer(BaseSimpleSerializer): return super().serialize() -class FrozensetSerializer(BaseSequenceSerializer): +class FrozensetSerializer(BaseUnorderedSequenceSerializer): def _format(self): return "frozenset([%s])" @@ -279,7 +284,7 @@ class SequenceSerializer(BaseSequenceSerializer): return "[%s]" -class SetSerializer(BaseSequenceSerializer): +class SetSerializer(BaseUnorderedSequenceSerializer): def _format(self): # Serialize as a set literal except when value is empty because {} # is an empty dict. diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index 79b89b269d..3dd3014355 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -154,7 +154,9 @@ class MigrationWriter: imports.add("from django.conf import settings") else: dependencies.append(" %s," % self.serialize(dependency)[0]) - items["dependencies"] = "\n".join(dependencies) + "\n" if dependencies else "" + items["dependencies"] = ( + "\n".join(sorted(dependencies)) + "\n" if dependencies else "" + ) # Format imports nicely, swapping imports of functions from migration files # for comments diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 33b52bd538..bef8f64061 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -768,12 +768,17 @@ class WriterTests(SimpleTestCase): def test_serialize_frozensets(self): self.assertSerializedEqual(frozenset()) self.assertSerializedEqual(frozenset("let it go")) + self.assertSerializedResultEqual( + frozenset("cba"), ("frozenset(['a', 'b', 'c'])", set()) + ) def test_serialize_set(self): self.assertSerializedEqual(set()) self.assertSerializedResultEqual(set(), ("set()", set())) self.assertSerializedEqual({"a"}) self.assertSerializedResultEqual({"a"}, ("{'a'}", set())) + self.assertSerializedEqual({"c", "b", "a"}) + self.assertSerializedResultEqual({"c", "b", "a"}, ("{'a', 'b', 'c'}", set())) def test_serialize_timedelta(self): self.assertSerializedEqual(datetime.timedelta()) @@ -891,6 +896,33 @@ class WriterTests(SimpleTestCase): result["custom_migration_operations"].more_operations.TestOperation, ) + def test_sorted_dependencies(self): + migration = type( + "Migration", + (migrations.Migration,), + { + "operations": [ + migrations.AddField("mymodel", "myfield", models.IntegerField()), + ], + "dependencies": [ + ("testapp10", "0005_fifth"), + ("testapp02", "0005_third"), + ("testapp02", "0004_sixth"), + ("testapp01", "0001_initial"), + ], + }, + ) + output = MigrationWriter(migration, include_header=False).as_string() + self.assertIn( + " dependencies = [\n" + " ('testapp01', '0001_initial'),\n" + " ('testapp02', '0004_sixth'),\n" + " ('testapp02', '0005_third'),\n" + " ('testapp10', '0005_fifth'),\n" + " ]", + output, + ) + def test_sorted_imports(self): """ #24155 - Tests ordering of imports.