]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add `dataclass_metadata` parameter to `mapped_column` and friends.
authorSigmund Lahn <s@lahn.no>
Thu, 22 May 2025 16:43:12 +0000 (18:43 +0200)
committerSigmund Lahn <s@lahn.no>
Fri, 23 May 2025 09:39:50 +0000 (11:39 +0200)
This parameter is then passed to `dataclasses.field()` if mapped as a
dataclass.

Fixes: #10674
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_tm_future_annotations_sync.py
test/orm/declarative/test_typed_mapping.py

index f96018e51e05d3ed1601c2e98603d9f62c6d4a49..0164a87668865c9e766242517f7aaf380681e9a8 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
@@ -243,7 +244,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 5dad065396094e7ea39edfbeb2ab3ec9f2192a66..b6f3e7d286cb3aaf0ccffed0fea9b29a9416f9ee 100644 (file)
@@ -8,7 +8,7 @@
 from __future__ import annotations
 
 import typing
-from typing import Any
+from typing import Any, Mapping
 from typing import Callable
 from typing import Collection
 from typing import Iterable
@@ -136,6 +136,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
@@ -355,7 +356,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 +469,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.
 
@@ -595,6 +604,7 @@ def column_property(
             compare,
             kw_only,
             hash,
+            dataclass_metadata,
         ),
         group=group,
         deferred=deferred,
@@ -627,6 +637,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 +708,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.
@@ -783,7 +795,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 +1056,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.
@@ -1870,7 +1890,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 +1935,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 +2049,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 +2191,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 +2222,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 +2271,7 @@ def query_expression(
             compare,
             _NoArg.NO_ARG,
             _NoArg.NO_ARG,
+            _NoArg.NO_ARG,
         ),
         expire_on_flush=expire_on_flush,
         info=info,
index 55f5236ce3c32391556ebbee21d89fa19efaf690..b064377666f12d266d4c3232d6356b2a04d13edb 100644 (file)
@@ -1580,9 +1580,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 9045e09a7c8e1259b9715b628fd471e4f08da696..d3297ade58bdae69d7dcb9e4f14bdebcc68d2376 100644 (file)
@@ -21,7 +21,7 @@ from __future__ import annotations
 import collections
 import dataclasses
 import typing
-from typing import Any
+from typing import Any, Mapping
 from typing import Callable
 from typing import cast
 from typing import ClassVar
@@ -227,6 +227,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 +249,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
@@ -335,6 +338,7 @@ _DEFAULT_ATTRIBUTE_OPTIONS = _AttributeOptions(
     _NoArg.NO_ARG,
     _NoArg.NO_ARG,
     _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
 )
 
 _DEFAULT_READONLY_ATTRIBUTE_OPTIONS = _AttributeOptions(
@@ -345,6 +349,7 @@ _DEFAULT_READONLY_ATTRIBUTE_OPTIONS = _AttributeOptions(
     _NoArg.NO_ARG,
     _NoArg.NO_ARG,
     _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
 )
 
 
index 004a119acde082fa64725c4b4fc9793c4283908f..e19a42df398e433b1796d2dc6698a4c55b0ab012 100644 (file)
@@ -55,6 +55,7 @@ from sqlalchemy.testing import expect_deprecated
 from sqlalchemy.testing import expect_raises
 from sqlalchemy.testing import expect_raises_message
 from sqlalchemy.testing import fixtures
+from sqlalchemy.testing import in_
 from sqlalchemy.testing import is_
 from sqlalchemy.testing import is_false
 from sqlalchemy.testing import is_true
@@ -900,6 +901,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]):
@@ -1877,9 +1891,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 = {}
@@ -1908,6 +1923,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: