--- /dev/null
+.. change::
+ :tags: bug, typing
+ :tickets: 12855
+
+ Added new decorator :func:`_orm.mapped_as_dataclass`, which is a function
+ based form of :meth:`_orm.registry.mapped_as_dataclass`; the method form
+ :meth:`_orm.registry.mapped_as_dataclass` does not seem to be correctly
+ recognized within the scope of :pep:`681` in recent mypy versions.
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
-When using the decorator form, only the :meth:`_orm.registry.mapped_as_dataclass`
+When using the decorator form, the :meth:`_orm.registry.mapped_as_dataclass`
decorator is supported::
from sqlalchemy.orm import Mapped
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
+The same method is available in a standalone function form, which may
+have better compatibility with some versions of the mypy type checker::
+
+ from sqlalchemy.orm import Mapped
+ from sqlalchemy.orm import mapped_as_dataclass
+ from sqlalchemy.orm import mapped_column
+ from sqlalchemy.orm import registry
+
+
+ reg = registry()
+
+
+ @mapped_as_dataclass(reg)
+ class User:
+ __tablename__ = "user_account"
+
+ id: Mapped[int] = mapped_column(init=False, primary_key=True)
+ name: Mapped[str]
+
+.. versionadded:: 2.0.44 Added :func:`_orm.mapped_as_dataclass` after observing
+ mypy compatibility issues with the method form of the same feature
+
Class level feature configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str]
-When using the decorator form with :meth:`_orm.registry.mapped_as_dataclass`,
-class configuration arguments are passed to the decorator directly::
+When using the decorator form with :meth:`_orm.registry.mapped_as_dataclass` or
+:func:`_orm.mapped_as_dataclass`, class configuration arguments are passed to
+the decorator directly::
from sqlalchemy.orm import registry
from sqlalchemy.orm import Mapped
Class Mapping API
=================
-.. autoclass:: registry
- :members:
-
.. autofunction:: add_mapped_attribute
+.. autofunction:: as_declarative
+
+.. autofunction:: class_mapper
+
+.. autofunction:: clear_mappers
+
.. autofunction:: column_property
+.. autofunction:: configure_mappers
+
.. autofunction:: declarative_base
-.. autofunction:: as_declarative
+.. autoclass:: DeclarativeBase
+ :members:
+ :special-members: __table__, __mapper__, __mapper_args__, __tablename__, __table_args__
-.. autofunction:: mapped_column
+.. autoclass:: DeclarativeBaseNoMeta
+ :members:
+ :special-members: __table__, __mapper__, __mapper_args__, __tablename__, __table_args__
.. autoclass:: declared_attr
:class:`_orm.declared_attr`
-.. autoclass:: DeclarativeBase
- :members:
- :special-members: __table__, __mapper__, __mapper_args__, __tablename__, __table_args__
-
-.. autoclass:: DeclarativeBaseNoMeta
- :members:
- :special-members: __table__, __mapper__, __mapper_args__, __tablename__, __table_args__
-
.. autofunction:: has_inherited_table
-.. autofunction:: synonym_for
+.. autofunction:: sqlalchemy.orm.util.identity_key
-.. autofunction:: object_mapper
+.. autofunction:: mapped_as_dataclass
-.. autofunction:: class_mapper
+.. autofunction:: mapped_column
-.. autofunction:: configure_mappers
+.. autoclass:: MappedAsDataclass
+ :members:
-.. autofunction:: clear_mappers
+.. autoclass:: MappedClassProtocol
+ :no-members:
-.. autofunction:: sqlalchemy.orm.util.identity_key
+.. autoclass:: Mapper
+ :members:
-.. autofunction:: polymorphic_union
+.. autofunction:: object_mapper
.. autofunction:: orm_insert_sentinel
-.. autofunction:: reconstructor
+.. autofunction:: polymorphic_union
-.. autoclass:: Mapper
- :members:
+.. autofunction:: reconstructor
-.. autoclass:: MappedAsDataclass
+.. autoclass:: registry
:members:
-.. autoclass:: MappedClassProtocol
- :no-members:
+.. autofunction:: synonym_for
+
+
from .decl_api import DeclarativeMeta as DeclarativeMeta
from .decl_api import declared_attr as declared_attr
from .decl_api import has_inherited_table as has_inherited_table
+from .decl_api import mapped_as_dataclass as mapped_as_dataclass
from .decl_api import MappedAsDataclass as MappedAsDataclass
from .decl_api import registry as registry
from .decl_api import synonym_for as synonym_for
:ref:`orm_declarative_native_dataclasses` - complete background
on SQLAlchemy native dataclass mapping
+ :func:`_orm.mapped_as_dataclass` - functional version that may
+ provide better compatibility with mypy
.. versionadded:: 2.0
).as_declarative_base(**kw)
+@compat_typing.dataclass_transform(
+ field_specifiers=(
+ MappedColumn,
+ RelationshipProperty,
+ Composite,
+ Synonym,
+ mapped_column,
+ relationship,
+ composite,
+ synonym,
+ deferred,
+ ),
+)
+def mapped_as_dataclass(
+ registry: RegistryType,
+ /,
+ *,
+ init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
+ eq: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ order: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ unsafe_hash: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ match_args: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
+ dataclass_callable: Union[
+ _NoArg, Callable[..., Type[Any]]
+ ] = _NoArg.NO_ARG,
+) -> Callable[[Type[_O]], Type[_O]]:
+ """Standalone function form of :meth:`_orm.registry.mapped_as_dataclass`
+ which may have better compatibility with mypy.
+
+ The :class:`_orm.registry` is passed as the first argument to the
+ decorator.
+
+ e.g.::
+
+ from sqlalchemy.orm import Mapped
+ from sqlalchemy.orm import mapped_as_dataclass
+ from sqlalchemy.orm import mapped_column
+ from sqlalchemy.orm import registry
+
+ some_registry = registry()
+
+
+ @mapped_as_dataclass(some_registry)
+ class Relationships:
+ __tablename__ = "relationships"
+
+ entity_id1: Mapped[int] = mapped_column(primary_key=True)
+ entity_id2: Mapped[int] = mapped_column(primary_key=True)
+ level: Mapped[int] = mapped_column(Integer)
+
+ .. versionadded:: 2.0.44
+
+ """
+ return registry.mapped_as_dataclass(
+ init=init,
+ repr=repr,
+ eq=eq,
+ order=order,
+ unsafe_hash=unsafe_hash,
+ match_args=match_args,
+ kw_only=kw_only,
+ dataclass_callable=dataclass_callable,
+ )
+
+
@inspection._inspects(
DeclarativeMeta, DeclarativeBase, DeclarativeAttributeIntercept
)
from sqlalchemy.orm import deferred
from sqlalchemy.orm import interfaces
from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_as_dataclass
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
from sqlalchemy.orm import MappedColumn
# TODO: get this test to work with future anno mode as well
# anno only: @testing.exclusions.closed("doesn't work for future annotations mode yet") # noqa: E501
- @testing.variation("dc_type", ["decorator", "superclass"])
+ @testing.variation("dc_type", ["fn_decorator", "decorator", "superclass"])
def test_dataclass_fn(self, dc_type: Variation):
annotations = {}
annotations[kls] = kls.__annotations__
return dataclasses.dataclass(kls, **kw) # type: ignore
- if dc_type.decorator:
+ if dc_type.fn_decorator:
+ reg = registry()
+
+ @mapped_as_dataclass(reg, dataclass_callable=dc_callable)
+ class MappedClass:
+ __tablename__ = "mapped_class"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str]
+
+ eq_(annotations, {MappedClass: {"id": int, "name": str}})
+
+ elif dc_type.decorator:
reg = registry()
@reg.mapped_as_dataclass(dataclass_callable=dc_callable)
"are not being mixed. ",
):
- @registry.mapped_as_dataclass
+ @mapped_as_dataclass(registry)
class Foo(Mixin):
bar_value: Mapped[float] = mapped_column(default=78)
def test_replace_operation_works_w_history_etc(
self, registry: _RegistryType
):
- @registry.mapped_as_dataclass
+ @mapped_as_dataclass(registry)
class A:
__tablename__ = "a"
default_factory=list
)
- @registry.mapped_as_dataclass
+ @mapped_as_dataclass(registry)
class B:
__tablename__ = "b"
)
eq_(fas.kwonlyargs, [])
+ @testing.variation("decorator_type", ["fn", "method"])
def test_dc_arguments_decorator(
self,
dc_argument_fixture,
mapped_expr_constructor,
registry: _RegistryType,
+ decorator_type,
):
- @registry.mapped_as_dataclass(**dc_argument_fixture[0])
+ if decorator_type.fn:
+ dec = mapped_as_dataclass(registry, **dc_argument_fixture[0])
+ else:
+ dec = registry.mapped_as_dataclass(**dc_argument_fixture[0])
+
+ @dec
class A:
__tablename__ = "a"
from sqlalchemy.orm import deferred
from sqlalchemy.orm import interfaces
from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_as_dataclass
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
from sqlalchemy.orm import MappedColumn
@testing.exclusions.closed(
"doesn't work for future annotations mode yet"
) # noqa: E501
- @testing.variation("dc_type", ["decorator", "superclass"])
+ @testing.variation("dc_type", ["fn_decorator", "decorator", "superclass"])
def test_dataclass_fn(self, dc_type: Variation):
annotations = {}
annotations[kls] = kls.__annotations__
return dataclasses.dataclass(kls, **kw) # type: ignore
- if dc_type.decorator:
+ if dc_type.fn_decorator:
+ reg = registry()
+
+ @mapped_as_dataclass(reg, dataclass_callable=dc_callable)
+ class MappedClass:
+ __tablename__ = "mapped_class"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str]
+
+ eq_(annotations, {MappedClass: {"id": int, "name": str}})
+
+ elif dc_type.decorator:
reg = registry()
@reg.mapped_as_dataclass(dataclass_callable=dc_callable)
"are not being mixed. ",
):
- @registry.mapped_as_dataclass
+ @mapped_as_dataclass(registry)
class Foo(Mixin):
bar_value: Mapped[float] = mapped_column(default=78)
def test_replace_operation_works_w_history_etc(
self, registry: _RegistryType
):
- @registry.mapped_as_dataclass
+ @mapped_as_dataclass(registry)
class A:
__tablename__ = "a"
default_factory=list
)
- @registry.mapped_as_dataclass
+ @mapped_as_dataclass(registry)
class B:
__tablename__ = "b"
)
eq_(fas.kwonlyargs, [])
+ @testing.variation("decorator_type", ["fn", "method"])
def test_dc_arguments_decorator(
self,
dc_argument_fixture,
mapped_expr_constructor,
registry: _RegistryType,
+ decorator_type,
):
- @registry.mapped_as_dataclass(**dc_argument_fixture[0])
+ if decorator_type.fn:
+ dec = mapped_as_dataclass(registry, **dc_argument_fixture[0])
+ else:
+ dec = registry.mapped_as_dataclass(**dc_argument_fixture[0])
+
+ @dec
class A:
__tablename__ = "a"
--- /dev/null
+from sqlalchemy import Integer
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_as_dataclass
+from sqlalchemy.orm import mapped_column
+from sqlalchemy.orm import registry
+
+some_target_tables_registry = registry()
+
+
+@mapped_as_dataclass(some_target_tables_registry)
+class Relationships:
+ __tablename__ = "relationships"
+
+ entity_id1: Mapped[int] = mapped_column(primary_key=True)
+ entity_id2: Mapped[int] = mapped_column(primary_key=True)
+ level: Mapped[int] = mapped_column(Integer)
+
+
+rs = Relationships(entity_id1=1, entity_id2=2, level=1)
+
+
+# EXPECTED_TYPE: int
+reveal_type(rs.entity_id1)