diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2023-02-03 10:50:14 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2023-02-03 10:50:14 -0500 |
| commit | 188ed2228b3f80ece2d01a77b320de2967cd8f7d (patch) | |
| tree | 7b35f117b074a9f2da97f4b974d5b38b8e1681a9 | |
| parent | 0635235090c85e2c1a18676ca49652d2c2094925 (diff) | |
| download | sqlalchemy-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.rst | 9 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/decl_base.py | 11 | ||||
| -rw-r--r-- | test/orm/declarative/test_dc_transforms.py | 125 |
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") |
