Skip to content

Commit 4647e2b

Browse files
adamchainznessita
authored andcommitted
Refs #36383 -- Extended DeconstructibleSerializer to support non-identifier keyword arguments.
In Python, keyword arguments must normally be valid identifiers (i.e., variable names that follow Python's naming rules). However, Python dicts can have keys that aren't valid identifiers, like "foo-bar" or "123foo". This commit ensures that keyword arguments that are nt valid identifiers, are properly handled when deconstructing an object.
1 parent 0f94ecd commit 4647e2b

File tree

4 files changed

+42
-3
lines changed

4 files changed

+42
-3
lines changed

django/db/migrations/serializer.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,20 @@ def serialize_deconstructed(path, args, kwargs):
102102
arg_string, arg_imports = serializer_factory(arg).serialize()
103103
strings.append(arg_string)
104104
imports.update(arg_imports)
105+
non_ident_kwargs = {}
105106
for kw, arg in sorted(kwargs.items()):
106-
arg_string, arg_imports = serializer_factory(arg).serialize()
107-
imports.update(arg_imports)
108-
strings.append("%s=%s" % (kw, arg_string))
107+
if kw.isidentifier():
108+
arg_string, arg_imports = serializer_factory(arg).serialize()
109+
imports.update(arg_imports)
110+
strings.append("%s=%s" % (kw, arg_string))
111+
else:
112+
non_ident_kwargs[kw] = arg
113+
if non_ident_kwargs:
114+
# Serialize non-identifier keyword arguments as a dict.
115+
kw_string, kw_imports = serializer_factory(non_ident_kwargs).serialize()
116+
strings.append(f"**{kw_string}")
117+
imports.update(kw_imports)
118+
109119
return "%s(%s)" % (name, ", ".join(strings)), imports
110120

111121
@staticmethod

docs/releases/6.0.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ Migrations
181181

182182
* Migrations now support serialization of :class:`zoneinfo.ZoneInfo` instances.
183183

184+
* Serialization of deconstructible objects now supports keyword arguments with
185+
names that are not valid Python identifiers.
186+
184187
Models
185188
~~~~~~
186189

docs/spelling_wordlist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ databrowse
113113
datafile
114114
datetimes
115115
declaratively
116+
deconstructible
116117
deduplicates
117118
deduplication
118119
deepcopy

tests/migrations/test_writer.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ def deconstruct(self):
4141
return ("DeconstructibleInstances", [], {})
4242

4343

44+
@deconstructible
45+
class DeconstructibleArbitrary:
46+
def __init__(self, *args, **kwargs):
47+
self.args = args
48+
self.kwargs = kwargs
49+
50+
4451
class Money(decimal.Decimal):
4552
def deconstruct(self):
4653
return (
@@ -1143,6 +1150,24 @@ def test_deconstruct_class_arguments(self):
11431150
"models.CharField(default=migrations.test_writer.DeconstructibleInstances)",
11441151
)
11451152

1153+
def test_serialize_non_identifier_keyword_args(self):
1154+
instance = DeconstructibleArbitrary(
1155+
**{"kebab-case": 1, "my_list": [1, 2, 3], "123foo": {"456bar": set()}},
1156+
regular="kebab-case",
1157+
**{"simple": 1, "complex": 3.1416},
1158+
)
1159+
string, imports = MigrationWriter.serialize(instance)
1160+
self.assertEqual(
1161+
string,
1162+
"migrations.test_writer.DeconstructibleArbitrary(complex=3.1416, "
1163+
"my_list=[1, 2, 3], regular='kebab-case', simple=1, "
1164+
"**{'123foo': {'456bar': set()}, 'kebab-case': 1})",
1165+
)
1166+
self.assertEqual(imports, {"import migrations.test_writer"})
1167+
result = self.serialize_round_trip(instance)
1168+
self.assertEqual(result.args, instance.args)
1169+
self.assertEqual(result.kwargs, instance.kwargs)
1170+
11461171
def test_register_serializer(self):
11471172
class ComplexSerializer(BaseSerializer):
11481173
def serialize(self):

0 commit comments

Comments
 (0)