Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/build/changelog/unreleased_21/10064.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. change::
:tags: bug, orm, dataclasses
:tickets: 10064

Added an :class:`~sqlalchemy.exc.SAWarning` when an unmapped class
(such as a mixin or abstract base class) configured with
:class:`_orm.MappedAsDataclass` is directly instantiated. Direct
instantiation of unmapped dataclass models is not supported and will
become an error in a future release; concrete mapped subclasses are
unaffected and instantiate normally without warning.
26 changes: 26 additions & 0 deletions lib/sqlalchemy/orm/decl_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import collections
import dataclasses
import functools
import itertools
import re
from typing import Any
Expand Down Expand Up @@ -2053,6 +2054,31 @@ def __init__(
enable_descriptor_defaults=False, revert=True
)

if self.dataclass_setup_arguments:
self._install_unmapped_dataclass_init_warning()

def _install_unmapped_dataclass_init_warning(self) -> None:
cls_ = self.cls
decl_api = util.preloaded.orm_decl_api
if not issubclass(cls_, decl_api.MappedAsDataclass):
return

orig_init = cls_.__dict__.get("__init__", None)
if orig_init is None:
return

@functools.wraps(orig_init)
def _warn_on_init(self_: Any, *args: Any, **kwargs: Any) -> None:
if type(self_) is cls_:
util.warn(
f"Direct instantiation of unmapped dataclass {cls_.__name__!r} "
f"is not supported and will become an error in a future release.",
exc.SAWarning,
)
return orig_init(self_, *args, **kwargs)

cls_.__init__ = _warn_on_init # type: ignore[misc]

def _scan_attributes(self) -> None:
cls = self.cls

Expand Down
53 changes: 53 additions & 0 deletions test/orm/declarative/test_dc_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from sqlalchemy.testing import expect_deprecated
from sqlalchemy.testing import expect_raises
from sqlalchemy.testing import expect_raises_message
from sqlalchemy.testing import expect_warnings
from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
from sqlalchemy.testing import is_false
Expand Down Expand Up @@ -3440,3 +3441,55 @@ class A(dc_decl_base):
eq_(a1.some_syn_2, 10)
eq_(a1.some_syn, 10)
eq_(a1.some_int, 10)


class UnmappedDataclassInstantiationTest(fixtures.TestBase):
"""Test warning emitted when directly instantiating unmapped dataclass mixins/abstracts (#10064)."""

def test_warn_on_unmapped_mixin_instantiation(self):
class MyMixin(MappedAsDataclass):
data: Mapped[str]

with expect_warnings(
r"Direct instantiation of unmapped dataclass 'MyMixin' "
r"is not supported and will become an error in a future release\."
):
m = MyMixin(data="foo")
eq_(m.data, "foo")

def test_warn_on_unmapped_abstract_instantiation(self):
class Base(DeclarativeBase):
pass

class MyAbstract(MappedAsDataclass, Base):
__abstract__ = True
id: Mapped[int] = mapped_column(primary_key=True)
data: Mapped[str]

with expect_warnings(
r"Direct instantiation of unmapped dataclass 'MyAbstract' "
r"is not supported and will become an error in a future release\."
):
a = MyAbstract(id=1, data="bar")
eq_(a.id, 1)
eq_(a.data, "bar")

def test_no_warn_on_concrete_mapped_subclass(self):
class Base(DeclarativeBase):
pass

class MyMixin(MappedAsDataclass):
data: Mapped[str]

class MyAbstract(MappedAsDataclass, Base):
__abstract__ = True
id: Mapped[int] = mapped_column(primary_key=True)

class Concrete(MyAbstract, MyMixin):
__tablename__ = "concrete"

# Concrete subclass should instantiate cleanly with NO warnings
c = Concrete(id=1, data="test")
eq_(c.id, 1)
eq_(c.data, "test")