]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add ``dataclass_metadata`` parameter to orm cols
authorSigmund Lahn <s@lahn.no>
Thu, 3 Jul 2025 16:14:13 +0000 (12:14 -0400)
committerFederico Caselli <cfederico87@gmail.com>
Sun, 20 Jul 2025 09:09:42 +0000 (11:09 +0200)
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
(cherry picked from commit aeb7830d22cdb4ac387242d46833cb2f67e0a952)

doc/build/changelog/unreleased_20/10674.rst [new file with mode: 0644]
lib/sqlalchemy/ext/associationproxy.py
lib/sqlalchemy/orm/_orm_constructors.py
lib/sqlalchemy/orm/decl_base.py
lib/sqlalchemy/orm/interfaces.py
test/orm/declarative/test_dc_transforms.py
test/orm/declarative/test_dc_transforms_future_anno_sync.py
test/orm/declarative/test_tm_future_annotations_sync.py
test/orm/declarative/test_typed_mapping.py

diff --git a/doc/build/changelog/unreleased_20/10674.rst b/doc/build/changelog/unreleased_20/10674.rst
new file mode 100644 (file)
index 0000000..8c2c04f
--- /dev/null
@@ -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
index 8f2c19b8764a599b109202cc7c3912873ebf1055..d72cfc3eddf622ae73c4bd9a52b490bc9f3d3bbe 100644 (file)
@@ -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
@@ -206,6 +207,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.
 
@@ -245,7 +252,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,
         ),
     )
 
index 30fc794672a494e828646a1cccbec8d04f249ac4..9c07bf1800063a0e372b0b2d25be0eed0627268c 100644 (file)
@@ -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
@@ -134,6 +135,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
@@ -339,6 +341,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`.
 
@@ -353,7 +361,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,
@@ -459,6 +474,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.
 
@@ -581,6 +597,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,
@@ -593,6 +615,7 @@ def column_property(
             compare,
             kw_only,
             hash,
+            dataclass_metadata,
         ),
         group=group,
         deferred=deferred,
@@ -624,6 +647,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]: ...
 
@@ -691,6 +715,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.
@@ -769,6 +794,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()
@@ -777,7 +809,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,
@@ -1031,6 +1070,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.
@@ -1841,6 +1881,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(
@@ -1858,7 +1905,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,
@@ -1896,6 +1950,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
@@ -2009,7 +2064,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,
@@ -2144,6 +2206,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.
@@ -2174,7 +2237,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,
@@ -2218,6 +2288,7 @@ def query_expression(
             compare,
             _NoArg.NO_ARG,
             _NoArg.NO_ARG,
+            _NoArg.NO_ARG,
         ),
         expire_on_flush=expire_on_flush,
         info=info,
index 77b03a0385256aed0b5f695fbd17601b2c486105..418e312787cb51aa0bbe35d09063cfe08ab13746 100644 (file)
@@ -1600,9 +1600,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
index b4462e54593fa3d3a4f5d4de1e1a4c1902087398..dfcd13058d9d2413ba49ee9895fa3621dfa2acd3 100644 (file)
@@ -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
@@ -207,6 +208,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) -> Any:
         """Return a ``dataclasses.Field`` object given these arguments."""
@@ -226,6 +228,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
@@ -263,7 +267,7 @@ class _AttributeOptions(NamedTuple):
         key: str,
         annotation: _AnnotationScanType,
         mapped_container: Optional[Any],
-        elem: _T,
+        elem: Any,
     ) -> Union[
         Tuple[str, _AnnotationScanType],
         Tuple[str, _AnnotationScanType, dataclasses.Field[Any]],
@@ -306,6 +310,7 @@ _DEFAULT_ATTRIBUTE_OPTIONS = _AttributeOptions(
     _NoArg.NO_ARG,
     _NoArg.NO_ARG,
     _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
 )
 
 _DEFAULT_READONLY_ATTRIBUTE_OPTIONS = _AttributeOptions(
@@ -316,6 +321,7 @@ _DEFAULT_READONLY_ATTRIBUTE_OPTIONS = _AttributeOptions(
     _NoArg.NO_ARG,
     _NoArg.NO_ARG,
     _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
 )
 
 
index 53a9366c3a75770573e23decce9b4977f42290c3..e2613cf237c423f5b924e386e8388fdf34a2e2a4 100644 (file)
@@ -867,6 +867,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):
     def test_list(self, dc_decl_base: Type[MappedAsDataclass]):
@@ -1851,9 +1864,10 @@ class DataclassArgsTest(fixtures.TestBase):
                 "compare": True,
                 "kw_only": False,
                 "hash": False,
+                "dataclass_metadata": None,
             }
             exp = interfaces._AttributeOptions(
-                False, False, False, list, True, False, False
+                False, False, False, list, True, False, False, None
             )
         else:
             kw = {}
@@ -1882,6 +1896,7 @@ class DataclassArgsTest(fixtures.TestBase):
                 True,
                 _NoArg.NO_ARG,
                 _NoArg.NO_ARG,
+                _NoArg.NO_ARG,
             )
         else:
             kw = {}
index 8701990526fa3f4ceaea0bb2ef5a2e3139039ed2..6ebd0855bb1b05bd2c7434a58cab8ee64efa44c5 100644 (file)
@@ -880,6 +880,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):
     def test_list(self, dc_decl_base: Type[MappedAsDataclass]):
@@ -1870,9 +1883,10 @@ class DataclassArgsTest(fixtures.TestBase):
                 "compare": True,
                 "kw_only": False,
                 "hash": False,
+                "dataclass_metadata": None,
             }
             exp = interfaces._AttributeOptions(
-                False, False, False, list, True, False, False
+                False, False, False, list, True, False, False, None
             )
         else:
             kw = {}
@@ -1901,6 +1915,7 @@ class DataclassArgsTest(fixtures.TestBase):
                 True,
                 _NoArg.NO_ARG,
                 _NoArg.NO_ARG,
+                _NoArg.NO_ARG,
             )
         else:
             kw = {}
index 5b17e3e6e54a4b31ee946f1f07400b61cc693844..3a84853099dbcf39a5ab75145f9aaf8cab961c1e 100644 (file)
@@ -1502,6 +1502,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"])
@@ -1526,6 +1533,7 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             "compare",
             "default_factory",
             "hash",
+            "dataclass_metadata",
         )
 
         if is_dataclass:
index acc07ba7d4ce6d1e747a717dd4cdfffc956bd131..750a1b34f52d62b2a13c3840d327d878cbb36a7f 100644 (file)
@@ -1493,6 +1493,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"])
@@ -1517,6 +1524,7 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             "compare",
             "default_factory",
             "hash",
+            "dataclass_metadata",
         )
 
         if is_dataclass: