]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
disallow ORM instrumented attributes from reaching dataclasses
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 3 Feb 2023 15:50:14 +0000 (10:50 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 3 Feb 2023 15:50:14 +0000 (10:50 -0500)
More adjustments to ORM Declarative Dataclasses mappings, building on the
improved support for mixins with dataclasses added in 2.0.1 via
:ticket:`9179`, where a combination of using mixins plus ORM inheritance
would mis-classify fields in some cases leading to their dataclass
arguments such as ``init=False`` being lost.

Fixes: #9226
Change-Id: Ia36f413e23e91dfbdb900f5ff3f8cdd3d5847064

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

diff --git a/doc/build/changelog/unreleased_20/9226.rst b/doc/build/changelog/unreleased_20/9226.rst
new file mode 100644 (file)
index 0000000..3654760
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 9226
+
+    More adjustments to ORM Declarative Dataclasses mappings, building on the
+    improved support for mixins with dataclasses added in 2.0.1 via
+    :ticket:`9179`, where a combination of using mixins plus ORM inheritance
+    would mis-classify fields in some cases leading to their dataclass
+    arguments such as ``init=False`` being lost.
index a858f12cb947c6c1adb564453e0c603537b45a61..9f49302438aca9335f8864c5189a9a7184e3c653 100644 (file)
@@ -1060,6 +1060,17 @@ class _ClassScanMapperConfig(_MapperConfig):
                     attr_value,
                     originating_module,
                 ) in self.collected_annotations.items()
+                if key not in self.collected_attributes
+                # issue #9226; check for attributes that we've collected
+                # which are already instrumented, which we would assume
+                # mean we are in an ORM inheritance mapping and this attribute
+                # is already mapped on the superclass.   Under no circumstance
+                # should any QueryableAttribute be sent to the dataclass()
+                # function; anything that's mapped should be Field and
+                # that's it
+                or not isinstance(
+                    self.collected_attributes[key], QueryableAttribute
+                )
             )
         ]
         annotations = {}
index 63450f4a170f0e510717f807aff26055023f7ce7..857ad9a536675900204fe0d123246714d5ba698e 100644 (file)
@@ -30,6 +30,7 @@ from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.orm import column_property
 from sqlalchemy.orm import composite
 from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import declared_attr
 from sqlalchemy.orm import deferred
 from sqlalchemy.orm import interfaces
 from sqlalchemy.orm import Mapped
@@ -983,6 +984,130 @@ class DataclassesForNonMappedClassesTest(fixtures.TestBase):
 
         eq_regex(repr(Child(a=5, b=6, c=7)), r".*\.Child\(c=7\)")
 
+    @testing.variation(
+        "dataclass_scope",
+        ["on_base", "on_mixin", "on_base_class", "on_sub_class"],
+    )
+    def test_mixin_w_inheritance(self, dataclass_scope):
+        """test #9226"""
+
+        if dataclass_scope.on_base:
+
+            class Base(DeclarativeBase, MappedAsDataclass):
+                pass
+
+        else:
+
+            class Base(DeclarativeBase):
+                pass
+
+        if dataclass_scope.on_mixin:
+
+            class Mixin(MappedAsDataclass):
+                @declared_attr.directive
+                @classmethod
+                def __tablename__(cls) -> str:
+                    return cls.__name__.lower()
+
+                @declared_attr.directive
+                @classmethod
+                def __mapper_args__(cls) -> Dict[str, Any]:
+                    return {
+                        "polymorphic_identity": cls.__name__,
+                        "polymorphic_on": "polymorphic_type",
+                    }
+
+                @declared_attr
+                @classmethod
+                def polymorphic_type(cls) -> Mapped[str]:
+                    return mapped_column(
+                        String,
+                        insert_default=cls.__name__,
+                        init=False,
+                    )
+
+        else:
+
+            class Mixin:
+                @declared_attr.directive
+                @classmethod
+                def __tablename__(cls) -> str:
+                    return cls.__name__.lower()
+
+                @declared_attr.directive
+                @classmethod
+                def __mapper_args__(cls) -> Dict[str, Any]:
+                    return {
+                        "polymorphic_identity": cls.__name__,
+                        "polymorphic_on": "polymorphic_type",
+                    }
+
+                if dataclass_scope.on_base or dataclass_scope.on_base_class:
+
+                    @declared_attr
+                    @classmethod
+                    def polymorphic_type(cls) -> Mapped[str]:
+                        return mapped_column(
+                            String,
+                            insert_default=cls.__name__,
+                            init=False,
+                        )
+
+                else:
+
+                    @declared_attr
+                    @classmethod
+                    def polymorphic_type(cls) -> Mapped[str]:
+                        return mapped_column(
+                            String,
+                            insert_default=cls.__name__,
+                        )
+
+        if dataclass_scope.on_base_class:
+
+            class Book(Mixin, MappedAsDataclass, Base):
+                id: Mapped[int] = mapped_column(
+                    Integer,
+                    primary_key=True,
+                    init=False,
+                )
+
+        else:
+
+            class Book(Mixin, Base):
+                if not dataclass_scope.on_sub_class:
+                    id: Mapped[int] = mapped_column(  # noqa: A001
+                        Integer, primary_key=True, init=False
+                    )
+                else:
+                    id: Mapped[int] = mapped_column(  # noqa: A001
+                        Integer,
+                        primary_key=True,
+                    )
+
+        if dataclass_scope.on_sub_class:
+
+            class Novel(MappedAsDataclass, Book):
+                id: Mapped[int] = mapped_column(  # noqa: A001
+                    ForeignKey("book.id"),
+                    primary_key=True,
+                    init=False,
+                )
+                description: Mapped[Optional[str]]
+
+        else:
+
+            class Novel(Book):
+                id: Mapped[int] = mapped_column(
+                    ForeignKey("book.id"),
+                    primary_key=True,
+                    init=False,
+                )
+                description: Mapped[Optional[str]]
+
+        n1 = Novel("the description")
+        eq_(n1.description, "the description")
+
 
 class DataclassArgsTest(fixtures.TestBase):
     dc_arg_names = ("init", "repr", "eq", "order", "unsafe_hash")