]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add "compare" on dataclass fields
authorSimon Schiele <simon.schiele@sony.com>
Wed, 30 Nov 2022 13:40:50 +0000 (08:40 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 30 Nov 2022 21:04:24 +0000 (16:04 -0500)
Added :paramref:`_orm.mapped_column.compare` parameter to relevant ORM
attribute constructs including :func:`_orm.mapped_column`,
:func:`_orm.relationship` etc. to provide for the Python dataclasses
``compare`` parameter on ``field()``, when using the
:ref:`orm_declarative_native_dataclasses` feature. Pull request courtesy
Simon Schiele.

Added an additional case for associationproxy into
test_dc_transforms.py -> test_attribute_options

Fixes: #8905
Closes: #8906
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/8906
Pull-request-sha: ea9a53d2ca60befdd0c570013c0e57a78c11dd4a

Change-Id: I390d043b06c1d668242325ef86e2f7b7dbfac442

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

diff --git a/doc/build/changelog/unreleased_20/8905.rst b/doc/build/changelog/unreleased_20/8905.rst
new file mode 100644 (file)
index 0000000..7f88f7f
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: usecase, orm
+    :tickets: 8905
+
+    Added :paramref:`_orm.mapped_column.compare` parameter to relevant ORM
+    attribute constructs including :func:`_orm.mapped_column`,
+    :func:`_orm.relationship` etc. to provide for the Python dataclasses
+    ``compare`` parameter on ``field()``, when using the
+    :ref:`orm_declarative_native_dataclasses` feature. Pull request courtesy
+    Simon Schiele.
index 15193e563bdbfdf3d1bf0762be3af3cb9ebab8f2..4d38ac536854a534cf4b7fd2d42cabbb1d26545f 100644 (file)
@@ -94,6 +94,7 @@ def association_proxy(
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Optional[Any] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
     kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
 ) -> AssociationProxy[Any]:
     r"""Return a Python property implementing a view of a target
@@ -174,6 +175,13 @@ def association_proxy(
 
      .. versionadded:: 2.0.0b4
 
+    :param compare: Specific to
+     :ref:`orm_declarative_native_dataclasses`, indicates if this field
+     should be included in comparison operations when generating the
+     ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+     .. versionadded:: 2.0.0b4
+
     :param kw_only: Specific to :ref:`orm_declarative_native_dataclasses`,
      indicates if this field should be marked as keyword-only when generating
      the ``__init__()`` method as generated by the dataclass process.
@@ -218,7 +226,7 @@ def association_proxy(
         info=info,
         cascade_scalar_deletes=cascade_scalar_deletes,
         attribute_options=_AttributeOptions(
-            init, repr, default, default_factory, kw_only
+            init, repr, default, default_factory, compare, kw_only
         ),
     )
 
index 2450d1e8364f284a4d183565100ca9f1ef58def8..fe5df21054ea1c95cc1e2f7f03c396e9ace158c7 100644 (file)
@@ -104,6 +104,7 @@ def mapped_column(
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Optional[Any] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
     kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
     nullable: Optional[
         Union[bool, Literal[SchemaConst.NULL_UNSPECIFIED]]
@@ -245,6 +246,13 @@ def mapped_column(
      specifies a default-value generation function that will take place
      as part of the ``__init__()``
      method as generated by the dataclass process.
+    :param compare: Specific to
+     :ref:`orm_declarative_native_dataclasses`, indicates if this field
+     should be included in comparison operations when generating the
+     ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+     .. versionadded:: 2.0.0b4
+
     :param kw_only: Specific to
      :ref:`orm_declarative_native_dataclasses`, indicates if this field
      should be marked as keyword-only when generating the ``__init__()``.
@@ -263,7 +271,7 @@ def mapped_column(
         autoincrement=autoincrement,
         insert_default=insert_default,
         attribute_options=_AttributeOptions(
-            init, repr, default, default_factory, kw_only
+            init, repr, default, default_factory, compare, kw_only
         ),
         doc=doc,
         key=key,
@@ -296,6 +304,7 @@ def column_property(
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Optional[Any] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
     kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
     active_history: bool = False,
     expire_on_flush: bool = True,
@@ -389,7 +398,7 @@ def column_property(
         column,
         *additional_columns,
         attribute_options=_AttributeOptions(
-            init, repr, default, default_factory, kw_only
+            init, repr, default, default_factory, compare, kw_only
         ),
         group=group,
         deferred=deferred,
@@ -415,6 +424,7 @@ def composite(
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Optional[Any] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
     kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
     info: Optional[_InfoType] = None,
     doc: Optional[str] = None,
@@ -436,6 +446,7 @@ def composite(
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Optional[Any] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
     kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
     info: Optional[_InfoType] = None,
     doc: Optional[str] = None,
@@ -458,6 +469,7 @@ def composite(
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Optional[Any] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
     kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
     info: Optional[_InfoType] = None,
     doc: Optional[str] = None,
@@ -521,6 +533,14 @@ def composite(
      specifies a default-value generation function that will take place
      as part of the ``__init__()``
      method as generated by the dataclass process.
+
+    :param compare: Specific to
+     :ref:`orm_declarative_native_dataclasses`, indicates if this field
+     should be included in comparison operations when generating the
+     ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+     .. versionadded:: 2.0.0b4
+
     :param kw_only: Specific to
      :ref:`orm_declarative_native_dataclasses`, indicates if this field
      should be marked as keyword-only when generating the ``__init__()``.
@@ -533,7 +553,7 @@ def composite(
         _class_or_attr,
         *attrs,
         attribute_options=_AttributeOptions(
-            init, repr, default, default_factory, kw_only
+            init, repr, default, default_factory, compare, kw_only
         ),
         group=group,
         deferred=deferred,
@@ -756,6 +776,7 @@ def relationship(
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Union[_NoArg, _T] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
     kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
     lazy: _LazyLoadArgumentType = "select",
     passive_deletes: Union[Literal["all"], bool] = False,
@@ -1593,6 +1614,13 @@ def relationship(
      specifies a default-value generation function that will take place
      as part of the ``__init__()``
      method as generated by the dataclass process.
+    :param compare: Specific to
+     :ref:`orm_declarative_native_dataclasses`, indicates if this field
+     should be included in comparison operations when generating the
+     ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+     .. versionadded:: 2.0.0b4
+
     :param kw_only: Specific to
      :ref:`orm_declarative_native_dataclasses`, indicates if this field
      should be marked as keyword-only when generating the ``__init__()``.
@@ -1615,7 +1643,7 @@ def relationship(
         cascade=cascade,
         viewonly=viewonly,
         attribute_options=_AttributeOptions(
-            init, repr, default, default_factory, kw_only
+            init, repr, default, default_factory, compare, kw_only
         ),
         lazy=lazy,
         passive_deletes=passive_deletes,
@@ -1648,6 +1676,7 @@ def synonym(
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Union[_NoArg, _T] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
     kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
     info: Optional[_InfoType] = None,
     doc: Optional[str] = None,
@@ -1761,7 +1790,7 @@ def synonym(
         descriptor=descriptor,
         comparator_factory=comparator_factory,
         attribute_options=_AttributeOptions(
-            init, repr, default, default_factory, kw_only
+            init, repr, default, default_factory, compare, kw_only
         ),
         doc=doc,
         info=info,
@@ -1890,6 +1919,7 @@ def deferred(
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Optional[Any] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
     kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
     active_history: bool = False,
     expire_on_flush: bool = True,
@@ -1925,7 +1955,7 @@ def deferred(
         column,
         *additional_columns,
         attribute_options=_AttributeOptions(
-            init, repr, default, default_factory, kw_only
+            init, repr, default, default_factory, compare, kw_only
         ),
         group=group,
         deferred=True,
@@ -1966,6 +1996,7 @@ def query_expression(
             _NoArg.NO_ARG,
             _NoArg.NO_ARG,
             _NoArg.NO_ARG,
+            _NoArg.NO_ARG,
         ),
         expire_on_flush=expire_on_flush,
         info=info,
index 3d2f9708fc5c852f04f839e448e5edcb4f3a8fe9..48d0689f8940565e1eef2258638e9ef0debbf86c 100644 (file)
@@ -195,6 +195,7 @@ class _AttributeOptions(NamedTuple):
     dataclasses_repr: Union[_NoArg, bool]
     dataclasses_default: Union[_NoArg, Any]
     dataclasses_default_factory: Union[_NoArg, Callable[[], Any]]
+    dataclasses_compare: Union[_NoArg, bool]
     dataclasses_kw_only: Union[_NoArg, bool]
 
     def _as_dataclass_field(self) -> Any:
@@ -209,6 +210,8 @@ class _AttributeOptions(NamedTuple):
             kw["init"] = self.dataclasses_init
         if self.dataclasses_repr is not _NoArg.NO_ARG:
             kw["repr"] = self.dataclasses_repr
+        if self.dataclasses_compare is not _NoArg.NO_ARG:
+            kw["compare"] = self.dataclasses_compare
         if self.dataclasses_kw_only is not _NoArg.NO_ARG:
             kw["kw_only"] = self.dataclasses_kw_only
 
@@ -256,7 +259,12 @@ class _AttributeOptions(NamedTuple):
 
 
 _DEFAULT_ATTRIBUTE_OPTIONS = _AttributeOptions(
-    _NoArg.NO_ARG, _NoArg.NO_ARG, _NoArg.NO_ARG, _NoArg.NO_ARG, _NoArg.NO_ARG
+    _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
 )
 
 
index c9c2e69c874f4d9e714f4f923a23e5e3b04fe372..202eaef4ad19cbf4648bc0329d1e0eeea54409dd 100644 (file)
@@ -25,6 +25,7 @@ from sqlalchemy import JSON
 from sqlalchemy import select
 from sqlalchemy import String
 from sqlalchemy import testing
+from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.orm import column_property
 from sqlalchemy.orm import composite
 from sqlalchemy.orm import DeclarativeBase
@@ -570,6 +571,17 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase):
             ),
         )
 
+    def test_compare(self, dc_decl_base: Type[MappedAsDataclass]):
+        class A(dc_decl_base):
+            __tablename__ = "a"
+
+            id: Mapped[int] = mapped_column(primary_key=True, compare=False)
+            data: Mapped[str]
+
+        a1 = A(id=0, data="foo")
+        a2 = A(id=1, data="foo")
+        eq_(a1, a2)
+
     @testing.only_if(lambda: compat.py310, "python 3.10 is required")
     def test_kw_only(self, dc_decl_base: Type[MappedAsDataclass]):
         class A(dc_decl_base):
@@ -1192,30 +1204,36 @@ class DataclassArgsTest(fixtures.TestBase):
 
                 id: Mapped[int] = mapped_column(primary_key=True, init=False)
 
-    @testing.combinations(True, False)
-    def test_attribute_options(self, args):
-        if args:
+    @testing.variation("use_arguments", [True, False])
+    @testing.combinations(
+        mapped_column,
+        lambda **kw: synonym("some_int", **kw),
+        lambda **kw: column_property(Column(Integer), **kw),
+        lambda **kw: deferred(Column(Integer), **kw),
+        lambda **kw: composite("foo", **kw),
+        lambda **kw: relationship("Foo", **kw),
+        lambda **kw: association_proxy("foo", "bar", **kw),
+        argnames="construct",
+    )
+    def test_attribute_options(self, use_arguments, construct):
+        if use_arguments:
             kw = {
-                "init": True,
-                "repr": True,
-                "default": True,
+                "init": False,
+                "repr": False,
+                "default": False,
                 "default_factory": list,
-                "kw_only": True,
+                "compare": True,
+                "kw_only": False,
             }
-            exp = interfaces._AttributeOptions(True, True, True, list, True)
+            exp = interfaces._AttributeOptions(
+                False, False, False, list, True, False
+            )
         else:
             kw = {}
             exp = interfaces._DEFAULT_ATTRIBUTE_OPTIONS
 
-        for prop in [
-            mapped_column(**kw),
-            synonym("some_int", **kw),
-            column_property(Column(Integer), **kw),
-            deferred(Column(Integer), **kw),
-            composite("foo", **kw),
-            relationship("Foo", **kw),
-        ]:
-            eq_(prop._attribute_options, exp)
+        prop = construct(**kw)
+        eq_(prop._attribute_options, exp)
 
 
 class MixinColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):