]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Support dataclass default with init=False
authorFederico Caselli <cfederico87@gmail.com>
Sat, 3 Jun 2023 10:40:00 +0000 (12:40 +0200)
committerFederico Caselli <cfederico87@gmail.com>
Sat, 3 Jun 2023 10:40:00 +0000 (12:40 +0200)
Fixed an issue where generating dataclasses fields that specified a
default`` value and set ``init=False`` would not work.
The dataclasses behavior in this case is to set the default
value on the class, that's not compatible with the descriptors used
by SQLAlchemy. To support this case the default is transformed to
a ``default_factory`` when generating the dataclass.

Fixes: #9879
Change-Id: I5151d388232eacd506a100ba18ce26970bf83cf3

doc/build/changelog/unreleased_20/9879.rst [new file with mode: 0644]
lib/sqlalchemy/orm/interfaces.py
test/orm/declarative/test_dc_transforms.py

diff --git a/doc/build/changelog/unreleased_20/9879.rst b/doc/build/changelog/unreleased_20/9879.rst
new file mode 100644 (file)
index 0000000..d111230
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, orm, dataclasses
+    :tickets: 9879
+
+    Fixed an issue where generating dataclasses fields that specified a
+    ``default`` value and set ``init=False`` would not work.
+    The dataclasses behavior in this case is to set the default
+    value on the class, that's not compatible with the descriptors used
+    by SQLAlchemy. To support this case the default is transformed to
+    a ``default_factory`` when generating the dataclass.
index 4da8f63e680c1a53083fbab493aa31a7cc4deb88..d9df2b3b19f0cb8a1476d9cf34a16306743f79ca 100644 (file)
@@ -217,6 +217,16 @@ class _AttributeOptions(NamedTuple):
         if self.dataclasses_kw_only is not _NoArg.NO_ARG:
             kw["kw_only"] = self.dataclasses_kw_only
 
+        if (
+            "init" in kw
+            and not kw["init"]
+            and "default" in kw
+            and "default_factory" not in kw  # illegal but let field raise
+        ):
+            # fix for #9879
+            default = kw.pop("default")
+            kw["default_factory"] = lambda: default
+
         return dataclasses.field(**kw)
 
     @classmethod
index 576ee7fbfeb6cd73d2627762bd109493fea78ec0..678dc51a27c314b4d3ede43af4a71bcc5f8a0c59 100644 (file)
@@ -758,6 +758,38 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase):
 
         is_true(isinstance(ec.error.__cause__, TypeError))
 
+    def test_dataclass_default(self, dc_decl_base):
+        """test for #9879"""
+
+        def c10():
+            return 10
+
+        def c20():
+            return 20
+
+        class A(dc_decl_base):
+            __tablename__ = "a"
+            id: Mapped[int] = mapped_column(primary_key=True)
+            def_init: Mapped[int] = mapped_column(default=42)
+            call_init: Mapped[int] = mapped_column(default_factory=c10)
+            def_no_init: Mapped[int] = mapped_column(default=13, init=False)
+            call_no_init: Mapped[int] = mapped_column(
+                default_factory=c20, init=False
+            )
+
+        a = A(id=100)
+        eq_(a.def_init, 42)
+        eq_(a.call_init, 10)
+        eq_(a.def_no_init, 13)
+        eq_(a.call_no_init, 20)
+
+        fields = {f.name: f for f in dataclasses.fields(A)}
+        eq_(fields["def_init"].default, 42)
+        eq_(fields["call_init"].default_factory, c10)
+        eq_(fields["def_no_init"].default, dataclasses.MISSING)
+        ne_(fields["def_no_init"].default_factory, dataclasses.MISSING)
+        eq_(fields["call_no_init"].default_factory, c20)
+
 
 class RelationshipDefaultFactoryTest(fixtures.TestBase):
     def test_list(self, dc_decl_base: Type[MappedAsDataclass]):