summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2023-01-31 16:23:37 +0000
committerGerrit Code Review <gerrit@ci3.zzzcomputing.com>2023-01-31 16:23:37 +0000
commit4ba851323e510a3b5b44fbae3c0382e5ce69d615 (patch)
tree7516fb56db6e5b07ebf40ef84fe7c168f902f27a
parent11e9bc8125125f2c4ca61150b2e248f28f0c9c9d (diff)
parent4d3a50c2f1af894b081c5f64c529da89f35f1839 (diff)
downloadsqlalchemy-4ba851323e510a3b5b44fbae3c0382e5ce69d615.tar.gz
Merge "MappedAsDataclass applies @dataclasses.dataclass unconditionally" into main
-rw-r--r--doc/build/changelog/unreleased_20/9179.rst20
-rw-r--r--lib/sqlalchemy/orm/decl_api.py14
-rw-r--r--lib/sqlalchemy/orm/decl_base.py12
-rw-r--r--test/orm/declarative/test_dc_transforms.py170
4 files changed, 210 insertions, 6 deletions
diff --git a/doc/build/changelog/unreleased_20/9179.rst b/doc/build/changelog/unreleased_20/9179.rst
new file mode 100644
index 000000000..489812cf7
--- /dev/null
+++ b/doc/build/changelog/unreleased_20/9179.rst
@@ -0,0 +1,20 @@
+.. change::
+ :tags: bug, orm
+ :tickets: 9179
+
+ When using the :class:`.MappedAsDataclass` superclass, all classes within
+ the hierarchy that are subclasses of this class will now be run through the
+ ``@dataclasses.dataclass`` function whether or not they are actually
+ mapped, so that non-ORM fields declared on non-mapped classes within the
+ hierarchy will be used when mapped subclasses are turned into dataclasses.
+ This behavior applies both to intermediary classes mapped with
+ ``__abstract__ = True`` as well as to the user-defined declarative base
+ itself, assuming :class:`.MappedAsDataclass` is present as a superclass for
+ these classes.
+
+ This allows non-mapped attributes such as ``InitVar`` declarations on
+ superclasses to be used, without the need to run the
+ ``@dataclasses.dataclass`` decorator explicitly on each non-mapped class.
+ The new behavior is considered as correct as this is what the :pep:`681`
+ implementation expects when using a superclass to indicate dataclass
+ behavior.
diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py
index 62e78c94d..a46c1a7fb 100644
--- a/lib/sqlalchemy/orm/decl_api.py
+++ b/lib/sqlalchemy/orm/decl_api.py
@@ -45,6 +45,7 @@ from ._orm_constructors import relationship
from ._orm_constructors import synonym
from .attributes import InstrumentedAttribute
from .base import _inspect_mapped_class
+from .base import _is_mapped_class
from .base import Mapped
from .decl_base import _add_attribute
from .decl_base import _as_declarative
@@ -599,20 +600,29 @@ class MappedAsDataclass(metaclass=DCTransformDeclarative):
"kw_only": kw_only,
}
+ current_transforms: _DataclassArguments
+
if hasattr(cls, "_sa_apply_dc_transforms"):
current = cls._sa_apply_dc_transforms # type: ignore[attr-defined]
_ClassScanMapperConfig._assert_dc_arguments(current)
- cls._sa_apply_dc_transforms = {
+ cls._sa_apply_dc_transforms = current_transforms = { # type: ignore # noqa: E501
k: current.get(k, _NoArg.NO_ARG) if v is _NoArg.NO_ARG else v
for k, v in apply_dc_transforms.items()
}
else:
- cls._sa_apply_dc_transforms = apply_dc_transforms
+ cls._sa_apply_dc_transforms = (
+ current_transforms
+ ) = apply_dc_transforms
super().__init_subclass__()
+ if not _is_mapped_class(cls):
+ _ClassScanMapperConfig._apply_dataclasses_to_any_class(
+ current_transforms, cls
+ )
+
class DeclarativeBase(
inspection.Inspectable[InstanceState[Any]],
diff --git a/lib/sqlalchemy/orm/decl_base.py b/lib/sqlalchemy/orm/decl_base.py
index 9e8b02359..0462a8945 100644
--- a/lib/sqlalchemy/orm/decl_base.py
+++ b/lib/sqlalchemy/orm/decl_base.py
@@ -1078,10 +1078,18 @@ class _ClassScanMapperConfig(_MapperConfig):
self.cls.__annotations__ = annotations
- self._assert_dc_arguments(dataclass_setup_arguments)
+ self._apply_dataclasses_to_any_class(
+ dataclass_setup_arguments, self.cls
+ )
+
+ @classmethod
+ def _apply_dataclasses_to_any_class(
+ cls, dataclass_setup_arguments: _DataclassArguments, klass: Type[_O]
+ ) -> None:
+ cls._assert_dc_arguments(dataclass_setup_arguments)
dataclasses.dataclass(
- self.cls,
+ klass,
**{
k: v
for k, v in dataclass_setup_arguments.items()
diff --git a/test/orm/declarative/test_dc_transforms.py b/test/orm/declarative/test_dc_transforms.py
index 5f35d7a01..63450f4a1 100644
--- a/test/orm/declarative/test_dc_transforms.py
+++ b/test/orm/declarative/test_dc_transforms.py
@@ -823,6 +823,167 @@ class RelationshipDefaultFactoryTest(fixtures.TestBase):
)
+class DataclassesForNonMappedClassesTest(fixtures.TestBase):
+ """test for cases added in #9179"""
+
+ def test_base_is_dc(self):
+ class Parent(MappedAsDataclass, DeclarativeBase):
+ a: int
+
+ class Child(Parent):
+ __tablename__ = "child"
+ b: Mapped[int] = mapped_column(primary_key=True)
+
+ eq_regex(repr(Child(5, 6)), r".*\.Child\(a=5, b=6\)")
+
+ def test_base_is_dc_plus_options(self):
+ class Parent(MappedAsDataclass, DeclarativeBase, unsafe_hash=True):
+ a: int
+
+ class Child(Parent, repr=False):
+ __tablename__ = "child"
+ b: Mapped[int] = mapped_column(primary_key=True)
+
+ c1 = Child(5, 6)
+ eq_(hash(c1), hash(Child(5, 6)))
+
+ # still reprs, because base has a repr, but b not included
+ eq_regex(repr(c1), r".*\.Child\(a=5\)")
+
+ def test_base_is_dc_init_var(self):
+ class Parent(MappedAsDataclass, DeclarativeBase):
+ a: InitVar[int]
+
+ class Child(Parent):
+ __tablename__ = "child"
+ b: Mapped[int] = mapped_column(primary_key=True)
+
+ c1 = Child(a=5, b=6)
+ eq_regex(repr(c1), r".*\.Child\(b=6\)")
+
+ def test_base_is_dc_field(self):
+ class Parent(MappedAsDataclass, DeclarativeBase):
+ a: int = dataclasses.field(default=10)
+
+ class Child(Parent):
+ __tablename__ = "child"
+ b: Mapped[int] = mapped_column(primary_key=True, default=7)
+
+ c1 = Child(a=5, b=6)
+ eq_regex(repr(c1), r".*\.Child\(a=5, b=6\)")
+
+ c1 = Child(b=6)
+ eq_regex(repr(c1), r".*\.Child\(a=10, b=6\)")
+
+ c1 = Child()
+ eq_regex(repr(c1), r".*\.Child\(a=10, b=7\)")
+
+ def test_abstract_and_base_is_dc(self):
+ class Parent(MappedAsDataclass, DeclarativeBase):
+ a: int
+
+ class Mixin(Parent):
+ __abstract__ = True
+ b: int
+
+ class Child(Mixin):
+ __tablename__ = "child"
+ c: Mapped[int] = mapped_column(primary_key=True)
+
+ eq_regex(repr(Child(5, 6, 7)), r".*\.Child\(a=5, b=6, c=7\)")
+
+ def test_abstract_and_base_is_dc_plus_options(self):
+ class Parent(MappedAsDataclass, DeclarativeBase):
+ a: int
+
+ class Mixin(Parent, unsafe_hash=True):
+ __abstract__ = True
+ b: int
+
+ class Child(Mixin, repr=False):
+ __tablename__ = "child"
+ c: Mapped[int] = mapped_column(primary_key=True)
+
+ eq_(hash(Child(5, 6, 7)), hash(Child(5, 6, 7)))
+
+ eq_regex(repr(Child(5, 6, 7)), r".*\.Child\(a=5, b=6\)")
+
+ def test_abstract_and_base_is_dc_init_var(self):
+ class Parent(MappedAsDataclass, DeclarativeBase):
+ a: InitVar[int]
+
+ class Mixin(Parent):
+ __abstract__ = True
+ b: InitVar[int]
+
+ class Child(Mixin):
+ __tablename__ = "child"
+ c: Mapped[int] = mapped_column(primary_key=True)
+
+ c1 = Child(a=5, b=6, c=7)
+ eq_regex(repr(c1), r".*\.Child\(c=7\)")
+
+ def test_abstract_and_base_is_dc_field(self):
+ class Parent(MappedAsDataclass, DeclarativeBase):
+ a: int = dataclasses.field(default=10)
+
+ class Mixin(Parent):
+ __abstract__ = True
+ b: int = dataclasses.field(default=7)
+
+ class Child(Mixin):
+ __tablename__ = "child"
+ c: Mapped[int] = mapped_column(primary_key=True, default=9)
+
+ c1 = Child(b=6, c=7)
+ eq_regex(repr(c1), r".*\.Child\(a=10, b=6, c=7\)")
+
+ c1 = Child()
+ eq_regex(repr(c1), r".*\.Child\(a=10, b=7, c=9\)")
+
+ def test_abstract_is_dc(self):
+ class Parent(DeclarativeBase):
+ a: int
+
+ class Mixin(MappedAsDataclass, Parent):
+ __abstract__ = True
+ b: int
+
+ class Child(Mixin):
+ __tablename__ = "child"
+ c: Mapped[int] = mapped_column(primary_key=True)
+
+ eq_regex(repr(Child(6, 7)), r".*\.Child\(b=6, c=7\)")
+
+ def test_mixin_and_base_is_dc(self):
+ class Parent(MappedAsDataclass, DeclarativeBase):
+ a: int
+
+ @dataclasses.dataclass
+ class Mixin:
+ b: int
+
+ class Child(Mixin, Parent):
+ __tablename__ = "child"
+ c: Mapped[int] = mapped_column(primary_key=True)
+
+ eq_regex(repr(Child(5, 6, 7)), r".*\.Child\(a=5, b=6, c=7\)")
+
+ def test_mixin_and_base_is_dc_init_var(self):
+ class Parent(MappedAsDataclass, DeclarativeBase):
+ a: InitVar[int]
+
+ @dataclasses.dataclass
+ class Mixin:
+ b: InitVar[int]
+
+ class Child(Mixin, Parent):
+ __tablename__ = "child"
+ c: Mapped[int] = mapped_column(primary_key=True)
+
+ eq_regex(repr(Child(a=5, b=6, c=7)), r".*\.Child\(c=7\)")
+
+
class DataclassArgsTest(fixtures.TestBase):
dc_arg_names = ("init", "repr", "eq", "order", "unsafe_hash")
if compat.py310:
@@ -986,12 +1147,17 @@ class DataclassArgsTest(fixtures.TestBase):
create("g", 10) >= create("b", 7)
def _assert_repr(self, cls, create, dc_arguments):
+ assert "__repr__" in cls.__dict__
a1 = create("some data", 12)
eq_regex(repr(a1), r".*A\(id=None, data='some data', x=12\)")
def _assert_not_repr(self, cls, create, dc_arguments):
- a1 = create("some data", 12)
- eq_regex(repr(a1), r"<.*A object at 0x.*>")
+ assert "__repr__" not in cls.__dict__
+
+ # if a superclass has __repr__, then we still get repr.
+ # so can't test this
+ # a1 = create("some data", 12)
+ # eq_regex(repr(a1), r"<.*A object at 0x.*>")
def _assert_init(self, cls, create, dc_arguments):
if not dc_arguments.get("kw_only", False):