From: Simon Schiele Date: Wed, 30 Nov 2022 13:40:50 +0000 (-0500) Subject: Add "compare" on dataclass fields X-Git-Tag: rel_2_0_0b4~22 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0042076f5db0cdeceee51b5ce32d76cc54c2af69;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add "compare" on dataclass fields 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 --- diff --git a/doc/build/changelog/unreleased_20/8905.rst b/doc/build/changelog/unreleased_20/8905.rst new file mode 100644 index 0000000000..7f88f7fcd1 --- /dev/null +++ b/doc/build/changelog/unreleased_20/8905.rst @@ -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. diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py index 15193e563b..4d38ac5368 100644 --- a/lib/sqlalchemy/ext/associationproxy.py +++ b/lib/sqlalchemy/ext/associationproxy.py @@ -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 ), ) diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py index 2450d1e836..fe5df21054 100644 --- a/lib/sqlalchemy/orm/_orm_constructors.py +++ b/lib/sqlalchemy/orm/_orm_constructors.py @@ -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, diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 3d2f9708fc..48d0689f89 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -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, ) diff --git a/test/orm/declarative/test_dc_transforms.py b/test/orm/declarative/test_dc_transforms.py index c9c2e69c87..202eaef4ad 100644 --- a/test/orm/declarative/test_dc_transforms.py +++ b/test/orm/declarative/test_dc_transforms.py @@ -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):