summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam Johnson <me@adamj.eu>2025-05-11 23:07:47 +0200
committernessita <124304+nessita@users.noreply.github.com>2025-05-13 21:42:19 -0300
commit4647e2b8663cbd22a07af70bf0f8540946763851 (patch)
tree13f94549c5f70b31ebdb3e031242941ca2338e63
parent0f94ecd49d1af92d47bfa690ca39d4d71b5091cb (diff)
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.
-rw-r--r--django/db/migrations/serializer.py16
-rw-r--r--docs/releases/6.0.txt3
-rw-r--r--docs/spelling_wordlist1
-rw-r--r--tests/migrations/test_writer.py25
4 files changed, 42 insertions, 3 deletions
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):