diff --git a/django/db/migrations/serializer.py b/django/db/migrations/serializer.py index 84515fdfcf..50d9cacdc1 100644 --- a/django/db/migrations/serializer.py +++ b/django/db/migrations/serializer.py @@ -102,10 +102,20 @@ class DeconstructibleSerializer(BaseSerializer): arg_string, arg_imports = serializer_factory(arg).serialize() strings.append(arg_string) imports.update(arg_imports) + non_ident_kwargs = {} for kw, arg in sorted(kwargs.items()): - arg_string, arg_imports = serializer_factory(arg).serialize() - imports.update(arg_imports) - strings.append("%s=%s" % (kw, arg_string)) + if kw.isidentifier(): + arg_string, arg_imports = serializer_factory(arg).serialize() + imports.update(arg_imports) + strings.append("%s=%s" % (kw, arg_string)) + else: + non_ident_kwargs[kw] = arg + if non_ident_kwargs: + # Serialize non-identifier keyword arguments as a dict. + kw_string, kw_imports = serializer_factory(non_ident_kwargs).serialize() + strings.append(f"**{kw_string}") + imports.update(kw_imports) + return "%s(%s)" % (name, ", ".join(strings)), imports @staticmethod diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index 341da6fd68..611530bb0a 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -181,6 +181,9 @@ Migrations * Migrations now support serialization of :class:`zoneinfo.ZoneInfo` instances. +* Serialization of deconstructible objects now supports keyword arguments with + names that are not valid Python identifiers. + Models ~~~~~~ diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index e9441c62d9..6028f103e4 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -113,6 +113,7 @@ databrowse datafile datetimes declaratively +deconstructible deduplicates deduplication deepcopy diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 6afee23da2..fc3d3bc909 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -41,6 +41,13 @@ class DeconstructibleInstances: return ("DeconstructibleInstances", [], {}) +@deconstructible +class DeconstructibleArbitrary: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + class Money(decimal.Decimal): def deconstruct(self): return ( @@ -1143,6 +1150,24 @@ class WriterTests(SimpleTestCase): "models.CharField(default=migrations.test_writer.DeconstructibleInstances)", ) + def test_serialize_non_identifier_keyword_args(self): + instance = DeconstructibleArbitrary( + **{"kebab-case": 1, "my_list": [1, 2, 3], "123foo": {"456bar": set()}}, + regular="kebab-case", + **{"simple": 1, "complex": 3.1416}, + ) + string, imports = MigrationWriter.serialize(instance) + self.assertEqual( + string, + "migrations.test_writer.DeconstructibleArbitrary(complex=3.1416, " + "my_list=[1, 2, 3], regular='kebab-case', simple=1, " + "**{'123foo': {'456bar': set()}, 'kebab-case': 1})", + ) + self.assertEqual(imports, {"import migrations.test_writer"}) + result = self.serialize_round_trip(instance) + self.assertEqual(result.args, instance.args) + self.assertEqual(result.kwargs, instance.kwargs) + def test_register_serializer(self): class ComplexSerializer(BaseSerializer): def serialize(self):