]> 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:08:44 +0000 (11:08 +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

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 f96018e51e05d3ed1601c2e98603d9f62c6d4a49..22d2bb570d76d72e9e511588a92d09fcb481a891 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
@@ -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,
         ),
     )
 
index d2f526309bc608c89cd124bf359bb8905fbf6a58..bcd21fc964951ca010a50a8061d6fc95ecd67f28 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
@@ -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,
index 646df0540a07dc4c9f2b0598c0f1556a41d8262c..9d9538bdf07cd902ac56f0e0394435e1fdd6278f 100644 (file)
@@ -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
index 266acbc4723b12864aea61d3b69b2a73e35a76ea..49f4b94fae64888c0f334bb3670d22ad070f6df5 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
@@ -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,
 )
 
 
index 2d20c102aa96885b4398c8ba416518a8d4d53774..3bc260077ca3f3307b96af0e294f4cae8d82c3e4 100644 (file)
@@ -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 = {}
index 455d63428b083447eb2d28b337c3b93050d61d20..035abc19c124b8b87783beb403323d7499672bf3 100644 (file)
@@ -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 = {}
index f0b3e81fd755c0874d5ee3fcfadcd80e78fce901..d55f9f80b56ee75043ae3c1b7380fd59606de12f 100644 (file)
@@ -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:
index 748ad03f7ab73305bffa28a28697c681718caf3c..c8c8fec9cd9171c00fb1b4d560a4624e0211e73e 100644 (file)
@@ -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: