summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2023-02-03 10:50:14 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2023-02-03 10:50:14 -0500
commit188ed2228b3f80ece2d01a77b320de2967cd8f7d (patch)
tree7b35f117b074a9f2da97f4b974d5b38b8e1681a9
parent0635235090c85e2c1a18676ca49652d2c2094925 (diff)
downloadsqlalchemy-188ed2228b3f80ece2d01a77b320de2967cd8f7d.tar.gz
disallow ORM instrumented attributes from reaching dataclasses
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
-rw-r--r--doc/build/changelog/unreleased_20/9226.rst9
-rw-r--r--lib/sqlalchemy/orm/decl_base.py11
-rw-r--r--test/orm/declarative/test_dc_transforms.py125
3 files changed, 145 insertions, 0 deletions
diff --git a/doc/build/changelog/unreleased_20/9226.rst b/doc/build/changelog/unreleased_20/9226.rst
new file mode 100644
index 000000000..36547602b
--- /dev/null
+++ b/doc/build/changelog/unreleased_20/9226.rst
@@ -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.
diff --git a/lib/sqlalchemy/orm/decl_base.py b/lib/sqlalchemy/orm/decl_base.py
index a858f12cb..9f4930243 100644
--- a/lib/sqlalchemy/orm/decl_base.py
+++ b/lib/sqlalchemy/orm/decl_base.py
@@ -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 = {}
diff --git a/test/orm/declarative/test_dc_transforms.py b/test/orm/declarative/test_dc_transforms.py
index 63450f4a1..857ad9a53 100644
--- a/test/orm/declarative/test_dc_transforms.py
+++ b/test/orm/declarative/test_dc_transforms.py
@@ -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")