diff options
| author | Adam Johnson <me@adamj.eu> | 2025-05-11 23:07:47 +0200 |
|---|---|---|
| committer | nessita <124304+nessita@users.noreply.github.com> | 2025-05-13 21:42:19 -0300 |
| commit | 4647e2b8663cbd22a07af70bf0f8540946763851 (patch) | |
| tree | 13f94549c5f70b31ebdb3e031242941ca2338e63 | |
| parent | 0f94ecd49d1af92d47bfa690ca39d4d71b5091cb (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.py | 16 | ||||
| -rw-r--r-- | docs/releases/6.0.txt | 3 | ||||
| -rw-r--r-- | docs/spelling_wordlist | 1 | ||||
| -rw-r--r-- | tests/migrations/test_writer.py | 25 |
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): |
