From: Sigmund Lahn Date: Thu, 3 Jul 2025 16:14:13 +0000 (-0400) Subject: Add ``dataclass_metadata`` parameter to orm cols X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=aeb7830d22cdb4ac387242d46833cb2f67e0a952;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add ``dataclass_metadata`` parameter to orm cols Added ``dataclass_metadata`` argument to a all column functions used in the ORM that accept dataclasses parameters. It's passed to the underlying dataclass ``metadata`` attribute of the dataclass field. Pull request courtesy Sigmund Lahn. Fixes: #10674 Closes: #12619 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/12619 Pull-request-sha: 9ea6a95b2bc6816e5b69d6d559783a6009877838 Change-Id: I4551c24b85cebee4064df6ab752d0700f0f191f1 --- diff --git a/doc/build/changelog/unreleased_20/10674.rst b/doc/build/changelog/unreleased_20/10674.rst new file mode 100644 index 0000000000..8c2c04fec8 --- /dev/null +++ b/doc/build/changelog/unreleased_20/10674.rst @@ -0,0 +1,9 @@ +.. change:: + :tags: usecase, rom + :tickets: 10674 + + Added ``dataclass_metadata`` argument to a all column functions + used in the ORM that accept dataclasses parameters. + It's passed to the underlying dataclass ``metadata`` attribute + of the dataclass field. + Pull request courtesy Sigmund Lahn. \ No newline at end of file diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py index f96018e51e..22d2bb570d 100644 --- a/lib/sqlalchemy/ext/associationproxy.py +++ b/lib/sqlalchemy/ext/associationproxy.py @@ -99,6 +99,7 @@ def association_proxy( compare: Union[_NoArg, bool] = _NoArg.NO_ARG, kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG, hash: Union[_NoArg, bool, None] = _NoArg.NO_ARG, # noqa: A002 + dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] = _NoArg.NO_ARG, ) -> AssociationProxy[Any]: r"""Return a Python property implementing a view of a target attribute which references an attribute on members of the @@ -204,6 +205,12 @@ def association_proxy( .. versionadded:: 2.0.36 + :param dataclass_metadata: Specific to + :ref:`orm_declarative_native_dataclasses`, supplies metadata + to be attached to the generated dataclass field. + + .. versionadded:: 2.0.42 + :param info: optional, will be assigned to :attr:`.AssociationProxy.info` if present. @@ -243,7 +250,14 @@ def association_proxy( cascade_scalar_deletes=cascade_scalar_deletes, create_on_none_assignment=create_on_none_assignment, attribute_options=_AttributeOptions( - init, repr, default, default_factory, compare, kw_only, hash + init, + repr, + default, + default_factory, + compare, + kw_only, + hash, + dataclass_metadata, ), ) diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py index d2f526309b..bcd21fc964 100644 --- a/lib/sqlalchemy/orm/_orm_constructors.py +++ b/lib/sqlalchemy/orm/_orm_constructors.py @@ -12,6 +12,7 @@ from typing import Any from typing import Callable from typing import Collection from typing import Iterable +from typing import Mapping from typing import NoReturn from typing import Optional from typing import overload @@ -136,6 +137,7 @@ def mapped_column( system: bool = False, comment: Optional[str] = None, sort_order: Union[_NoArg, int] = _NoArg.NO_ARG, + dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] = _NoArg.NO_ARG, **kw: Any, ) -> MappedColumn[Any]: r"""declare a new ORM-mapped :class:`_schema.Column` construct @@ -341,6 +343,12 @@ def mapped_column( .. versionadded:: 2.0.36 + :param dataclass_metadata: Specific to + :ref:`orm_declarative_native_dataclasses`, supplies metadata + to be attached to the generated dataclass field. + + .. versionadded:: 2.0.42 + :param \**kw: All remaining keyword arguments are passed through to the constructor for the :class:`_schema.Column`. @@ -355,7 +363,14 @@ def mapped_column( autoincrement=autoincrement, insert_default=insert_default, attribute_options=_AttributeOptions( - init, repr, default, default_factory, compare, kw_only, hash + init, + repr, + default, + default_factory, + compare, + kw_only, + hash, + dataclass_metadata, ), doc=doc, key=key, @@ -461,6 +476,7 @@ def column_property( expire_on_flush: bool = True, info: Optional[_InfoType] = None, doc: Optional[str] = None, + dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] = _NoArg.NO_ARG, ) -> MappedSQLExpression[_T]: r"""Provide a column-level property for use with a mapping. @@ -583,6 +599,12 @@ def column_property( .. versionadded:: 2.0.36 + :param dataclass_metadata: Specific to + :ref:`orm_declarative_native_dataclasses`, supplies metadata + to be attached to the generated dataclass field. + + .. versionadded:: 2.0.42 + """ return MappedSQLExpression( column, @@ -595,6 +617,7 @@ def column_property( compare, kw_only, hash, + dataclass_metadata, ), group=group, deferred=deferred, @@ -627,6 +650,7 @@ def composite( hash: Union[_NoArg, bool, None] = _NoArg.NO_ARG, # noqa: A002 info: Optional[_InfoType] = None, doc: Optional[str] = None, + dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] = _NoArg.NO_ARG, **__kw: Any, ) -> Composite[Any]: ... @@ -697,6 +721,7 @@ def composite( hash: Union[_NoArg, bool, None] = _NoArg.NO_ARG, # noqa: A002 info: Optional[_InfoType] = None, doc: Optional[str] = None, + dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] = _NoArg.NO_ARG, **__kw: Any, ) -> Composite[Any]: r"""Return a composite column-based property for use with a Mapper. @@ -775,6 +800,13 @@ def composite( class. .. versionadded:: 2.0.36 + + :param dataclass_metadata: Specific to + :ref:`orm_declarative_native_dataclasses`, supplies metadata + to be attached to the generated dataclass field. + + .. versionadded:: 2.0.42 + """ if __kw: raise _no_kw() @@ -783,7 +815,14 @@ def composite( _class_or_attr, *attrs, attribute_options=_AttributeOptions( - init, repr, default, default_factory, compare, kw_only, hash + init, + repr, + default, + default_factory, + compare, + kw_only, + hash, + dataclass_metadata, ), group=group, deferred=deferred, @@ -1037,6 +1076,7 @@ def relationship( info: Optional[_InfoType] = None, omit_join: Literal[None, False] = None, sync_backref: Optional[bool] = None, + dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] = _NoArg.NO_ARG, **kw: Any, ) -> _RelationshipDeclared[Any]: """Provide a relationship between two mapped classes. @@ -1853,6 +1893,13 @@ def relationship( class. .. versionadded:: 2.0.36 + + :param dataclass_metadata: Specific to + :ref:`orm_declarative_native_dataclasses`, supplies metadata + to be attached to the generated dataclass field. + + .. versionadded:: 2.0.42 + """ return _RelationshipDeclared( @@ -1870,7 +1917,14 @@ def relationship( cascade=cascade, viewonly=viewonly, attribute_options=_AttributeOptions( - init, repr, default, default_factory, compare, kw_only, hash + init, + repr, + default, + default_factory, + compare, + kw_only, + hash, + dataclass_metadata, ), lazy=lazy, passive_deletes=passive_deletes, @@ -1908,6 +1962,7 @@ def synonym( hash: Union[_NoArg, bool, None] = _NoArg.NO_ARG, # noqa: A002 info: Optional[_InfoType] = None, doc: Optional[str] = None, + dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] = _NoArg.NO_ARG, ) -> Synonym[Any]: """Denote an attribute name as a synonym to a mapped property, in that the attribute will mirror the value and expression behavior @@ -2021,7 +2076,14 @@ def synonym( descriptor=descriptor, comparator_factory=comparator_factory, attribute_options=_AttributeOptions( - init, repr, default, default_factory, compare, kw_only, hash + init, + repr, + default, + default_factory, + compare, + kw_only, + hash, + dataclass_metadata, ), doc=doc, info=info, @@ -2156,6 +2218,7 @@ def deferred( expire_on_flush: bool = True, info: Optional[_InfoType] = None, doc: Optional[str] = None, + dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] = _NoArg.NO_ARG, ) -> MappedSQLExpression[_T]: r"""Indicate a column-based mapped attribute that by default will not load unless accessed. @@ -2186,7 +2249,14 @@ def deferred( column, *additional_columns, attribute_options=_AttributeOptions( - init, repr, default, default_factory, compare, kw_only, hash + init, + repr, + default, + default_factory, + compare, + kw_only, + hash, + dataclass_metadata, ), group=group, deferred=True, @@ -2228,6 +2298,7 @@ def query_expression( compare, _NoArg.NO_ARG, _NoArg.NO_ARG, + _NoArg.NO_ARG, ), expire_on_flush=expire_on_flush, info=info, diff --git a/lib/sqlalchemy/orm/decl_base.py b/lib/sqlalchemy/orm/decl_base.py index 646df0540a..9d9538bdf0 100644 --- a/lib/sqlalchemy/orm/decl_base.py +++ b/lib/sqlalchemy/orm/decl_base.py @@ -1581,9 +1581,15 @@ class _ClassScanMapperConfig(_MapperConfig): "default_factory", "repr", "default", + "dataclass_metadata", ] else: - argnames = ["init", "default_factory", "repr"] + argnames = [ + "init", + "default_factory", + "repr", + "dataclass_metadata", + ] args = { a diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 266acbc472..49f4b94fae 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -29,6 +29,7 @@ from typing import Dict from typing import Generic from typing import Iterator from typing import List +from typing import Mapping from typing import NamedTuple from typing import NoReturn from typing import Optional @@ -227,6 +228,7 @@ class _AttributeOptions(NamedTuple): dataclasses_compare: Union[_NoArg, bool] dataclasses_kw_only: Union[_NoArg, bool] dataclasses_hash: Union[_NoArg, bool, None] + dataclasses_dataclass_metadata: Union[_NoArg, Mapping[Any, Any], None] def _as_dataclass_field( self, key: str, dataclass_setup_arguments: _DataclassArguments @@ -248,6 +250,8 @@ class _AttributeOptions(NamedTuple): kw["kw_only"] = self.dataclasses_kw_only if self.dataclasses_hash is not _NoArg.NO_ARG: kw["hash"] = self.dataclasses_hash + if self.dataclasses_dataclass_metadata is not _NoArg.NO_ARG: + kw["metadata"] = self.dataclasses_dataclass_metadata if "default" in kw and callable(kw["default"]): # callable defaults are ambiguous. deprecate them in favour of @@ -286,7 +290,7 @@ class _AttributeOptions(NamedTuple): key: str, annotation: _AnnotationScanType, mapped_container: Optional[Any], - elem: _T, + elem: Any, dataclass_setup_arguments: _DataclassArguments, ) -> Union[ Tuple[str, _AnnotationScanType], @@ -335,6 +339,7 @@ _DEFAULT_ATTRIBUTE_OPTIONS = _AttributeOptions( _NoArg.NO_ARG, _NoArg.NO_ARG, _NoArg.NO_ARG, + _NoArg.NO_ARG, ) _DEFAULT_READONLY_ATTRIBUTE_OPTIONS = _AttributeOptions( @@ -345,6 +350,7 @@ _DEFAULT_READONLY_ATTRIBUTE_OPTIONS = _AttributeOptions( _NoArg.NO_ARG, _NoArg.NO_ARG, _NoArg.NO_ARG, + _NoArg.NO_ARG, ) diff --git a/test/orm/declarative/test_dc_transforms.py b/test/orm/declarative/test_dc_transforms.py index 2d20c102aa..3bc260077c 100644 --- a/test/orm/declarative/test_dc_transforms.py +++ b/test/orm/declarative/test_dc_transforms.py @@ -907,6 +907,19 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): eq_(fields["value"].default, cd) eq_(fields["no_init"].default, cd) + def test_dataclass_metadata(self, dc_decl_base): + class A(dc_decl_base): + __tablename__ = "a" + id: Mapped[int] = mapped_column(primary_key=True) + value: Mapped[str] = mapped_column( + dataclass_metadata={"meta_key": "meta_value"} + ) + + fields = {f.name: f for f in dataclasses.fields(A)} + + eq_(fields["id"].metadata, {}) + eq_(fields["value"].metadata, {"meta_key": "meta_value"}) + class RelationshipDefaultFactoryTest(fixtures.TestBase): @@ -1865,9 +1878,10 @@ class DataclassArgsTest(fixtures.TestBase): "compare": True, "kw_only": False, "hash": False, + "dataclass_metadata": None, } exp = interfaces._AttributeOptions( - False, False, None, list, True, False, False + False, False, None, list, True, False, False, None ) else: kw = {} @@ -1896,6 +1910,7 @@ class DataclassArgsTest(fixtures.TestBase): True, _NoArg.NO_ARG, _NoArg.NO_ARG, + _NoArg.NO_ARG, ) else: kw = {} diff --git a/test/orm/declarative/test_dc_transforms_future_anno_sync.py b/test/orm/declarative/test_dc_transforms_future_anno_sync.py index 455d63428b..035abc19c1 100644 --- a/test/orm/declarative/test_dc_transforms_future_anno_sync.py +++ b/test/orm/declarative/test_dc_transforms_future_anno_sync.py @@ -920,6 +920,19 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): eq_(fields["value"].default, cd) eq_(fields["no_init"].default, cd) + def test_dataclass_metadata(self, dc_decl_base): + class A(dc_decl_base): + __tablename__ = "a" + id: Mapped[int] = mapped_column(primary_key=True) + value: Mapped[str] = mapped_column( + dataclass_metadata={"meta_key": "meta_value"} + ) + + fields = {f.name: f for f in dataclasses.fields(A)} + + eq_(fields["id"].metadata, {}) + eq_(fields["value"].metadata, {"meta_key": "meta_value"}) + class RelationshipDefaultFactoryTest(fixtures.TestBase): @@ -1884,9 +1897,10 @@ class DataclassArgsTest(fixtures.TestBase): "compare": True, "kw_only": False, "hash": False, + "dataclass_metadata": None, } exp = interfaces._AttributeOptions( - False, False, None, list, True, False, False + False, False, None, list, True, False, False, None ) else: kw = {} @@ -1915,6 +1929,7 @@ class DataclassArgsTest(fixtures.TestBase): True, _NoArg.NO_ARG, _NoArg.NO_ARG, + _NoArg.NO_ARG, ) else: kw = {} diff --git a/test/orm/declarative/test_tm_future_annotations_sync.py b/test/orm/declarative/test_tm_future_annotations_sync.py index f0b3e81fd7..d55f9f80b5 100644 --- a/test/orm/declarative/test_tm_future_annotations_sync.py +++ b/test/orm/declarative/test_tm_future_annotations_sync.py @@ -1457,6 +1457,13 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL): "Argument 'hash' is a dataclass argument" ), ), + ( + "dataclass_metadata", + {}, + exc.SADeprecationWarning( + "Argument 'dataclass_metadata' is a dataclass argument" + ), + ), argnames="argname, argument, assertion", ) @testing.variation("use_annotated", [True, False, "control"]) @@ -1481,6 +1488,7 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL): "compare", "default_factory", "hash", + "dataclass_metadata", ) if is_dataclass: diff --git a/test/orm/declarative/test_typed_mapping.py b/test/orm/declarative/test_typed_mapping.py index 748ad03f7a..c8c8fec9cd 100644 --- a/test/orm/declarative/test_typed_mapping.py +++ b/test/orm/declarative/test_typed_mapping.py @@ -1448,6 +1448,13 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL): "Argument 'hash' is a dataclass argument" ), ), + ( + "dataclass_metadata", + {}, + exc.SADeprecationWarning( + "Argument 'dataclass_metadata' is a dataclass argument" + ), + ), argnames="argname, argument, assertion", ) @testing.variation("use_annotated", [True, False, "control"]) @@ -1472,6 +1479,7 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL): "compare", "default_factory", "hash", + "dataclass_metadata", ) if is_dataclass: