]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
establish column_property and query_expression as readonly from a dc perspective
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 10 Apr 2023 16:56:47 +0000 (12:56 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 12 Apr 2023 19:11:03 +0000 (15:11 -0400)
Fixed bug in ORM Declarative Dataclasses where the
:func:`_orm.queryable_attribute` and :func:`_orm.column_property`
constructs, which are documented as read-only constructs in the context of
a Declarative mapping, could not be used with a
:class:`_orm.MappedAsDataclass` class without adding ``init=False``, which
in the case of :func:`_orm.queryable_attribute` was not possible as no
``init`` parameter was included. These constructs have been modified from a
dataclass perspective to be assumed to be "read only", setting
``init=False`` by default and no longer including them in the pep-681
constructor. The dataclass parameters for :func:`_orm.column_property`
``init``, ``default``, ``default_factory``, ``kw_only`` are now deprecated;
these fields don't apply to :func:`_orm.column_property` as used in a
Declarative dataclasses configuration where the construct would be
read-only. Also added read-specific parameter
:paramref:`_orm.queryable_attribute.compare` to
:func:`_orm.queryable_attribute`; :paramref:`_orm.queryable_attribute.repr`
was already present.

Added missing :paramref:`_orm.mapped_column.active_history` parameter
to :func:`_orm.mapped_column` construct.

Fixes: #9628
Change-Id: I2ab44d6b763b20410bd1ebb5ac949a6d223f1ce2

16 files changed:
doc/build/changelog/unreleased_20/9628.rst [new file with mode: 0644]
doc/build/orm/declarative_tables.rst
lib/sqlalchemy/ext/mypy/names.py
lib/sqlalchemy/orm/_orm_constructors.py
lib/sqlalchemy/orm/decl_api.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/sql/base.py
lib/sqlalchemy/util/deprecations.py
lib/sqlalchemy/util/langhelpers.py
test/ext/mypy/plain_files/dataclass_transforms_one.py [new file with mode: 0644]
test/ext/mypy/plain_files/pep681.py [deleted file]
test/orm/declarative/test_basic.py
test/orm/declarative/test_dc_transforms.py
test/orm/test_deferred.py
test/orm/test_deprecations.py

diff --git a/doc/build/changelog/unreleased_20/9628.rst b/doc/build/changelog/unreleased_20/9628.rst
new file mode 100644 (file)
index 0000000..0d93683
--- /dev/null
@@ -0,0 +1,29 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 9628
+
+    Fixed bug in ORM Declarative Dataclasses where the
+    :func:`_orm.queryable_attribute` and :func:`_orm.column_property`
+    constructs, which are documented as read-only constructs in the context of
+    a Declarative mapping, could not be used with a
+    :class:`_orm.MappedAsDataclass` class without adding ``init=False``, which
+    in the case of :func:`_orm.queryable_attribute` was not possible as no
+    ``init`` parameter was included. These constructs have been modified from a
+    dataclass perspective to be assumed to be "read only", setting
+    ``init=False`` by default and no longer including them in the pep-681
+    constructor. The dataclass parameters for :func:`_orm.column_property`
+    ``init``, ``default``, ``default_factory``, ``kw_only`` are now deprecated;
+    these fields don't apply to :func:`_orm.column_property` as used in a
+    Declarative dataclasses configuration where the construct would be
+    read-only. Also added read-specific parameter
+    :paramref:`_orm.queryable_attribute.compare` to
+    :func:`_orm.queryable_attribute`; :paramref:`_orm.queryable_attribute.repr`
+    was already present.
+
+
+
+.. change::
+    :tags: bug, orm
+
+    Added missing :paramref:`_orm.mapped_column.active_history` parameter
+    to :func:`_orm.mapped_column` construct.
index d9a11087d6b34501c181e0e4ded622fbf4c0b358..0ee40cd07f9fcae2e72eb603233c948626b0d78b 100644 (file)
@@ -1336,8 +1336,8 @@ declaration, typing tools will be able to match the attribute to the
 
 .. _orm_imperative_table_column_options:
 
-Applying Load, Persistence and Mapping Options for Mapped Table Columns
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Applying Load, Persistence and Mapping Options for Imperative Table Columns
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 The section :ref:`orm_declarative_column_options` reviewed how to set load
 and persistence options when using the :func:`_orm.mapped_column` construct
index fac6bf5b14f5d4d6a744cc8afbd6e79444a52f2c..989f25592397c5408ffb95ee63a66ea4ba26f35c 100644 (file)
@@ -17,6 +17,7 @@ from typing import Union
 from mypy.nodes import ARG_POS
 from mypy.nodes import CallExpr
 from mypy.nodes import ClassDef
+from mypy.nodes import Decorator
 from mypy.nodes import Expression
 from mypy.nodes import FuncDef
 from mypy.nodes import MemberExpr
@@ -261,7 +262,20 @@ def type_id_for_unbound_type(
 
 def type_id_for_callee(callee: Expression) -> Optional[int]:
     if isinstance(callee, (MemberExpr, NameExpr)):
-        if isinstance(callee.node, OverloadedFuncDef):
+        if isinstance(callee.node, Decorator) and isinstance(
+            callee.node.func, FuncDef
+        ):
+            if callee.node.func.type and isinstance(
+                callee.node.func.type, CallableType
+            ):
+                ret_type = get_proper_type(callee.node.func.type.ret_type)
+
+                if isinstance(ret_type, Instance):
+                    return type_id_for_fullname(ret_type.type.fullname)
+
+            return None
+
+        elif isinstance(callee.node, OverloadedFuncDef):
             if (
                 callee.node.impl
                 and callee.node.impl.type
index 57acc57061d815c25fa0dfb55bd12e8472504221..1a1780158123a0c2adc5e114c6ccb9ba21d55e38 100644 (file)
@@ -126,6 +126,7 @@ def mapped_column(
     insert_default: Optional[Any] = _NoArg.NO_ARG,
     server_default: Optional[_ServerDefaultType] = None,
     server_onupdate: Optional[FetchedValue] = None,
+    active_history: bool = False,
     quote: Optional[bool] = None,
     system: bool = False,
     comment: Optional[str] = None,
@@ -258,6 +259,20 @@ def mapped_column(
 
      .. versionadded:: 2.0.4
 
+    :param active_history=False:
+
+        When ``True``, indicates that the "previous" value for a
+        scalar attribute should be loaded when replaced, if not
+        already loaded. Normally, history tracking logic for
+        simple non-primary-key scalar values only needs to be
+        aware of the "new" value in order to perform a flush. This
+        flag is available for applications that make use of
+        :func:`.attributes.get_history` or :meth:`.Session.is_modified`
+        which also need to know the "previous" value of the attribute.
+
+        .. versionadded:: 2.0.10
+
+
     :param init: Specific to :ref:`orm_declarative_native_dataclasses`,
      specifies if the mapped attribute should be part of the ``__init__()``
      method as generated by the dataclass process.
@@ -301,6 +316,7 @@ def mapped_column(
         index=index,
         unique=unique,
         info=info,
+        active_history=active_history,
         nullable=nullable,
         onupdate=onupdate,
         primary_key=primary_key,
@@ -318,6 +334,19 @@ def mapped_column(
     )
 
 
+@util.deprecated_params(
+    **{
+        arg: (
+            "2.0",
+            f"The :paramref:`_orm.column_property.{arg}` parameter is "
+            "deprecated for :func:`_orm.column_property`.  This parameter "
+            "applies to a writeable-attribute in a Declarative Dataclasses "
+            "configuration only, and :func:`_orm.column_property` is treated "
+            "as a read-only attribute in this context.",
+        )
+        for arg in ("init", "kw_only", "default", "default_factory")
+    }
+)
 def column_property(
     column: _ORMColumnExprArgument[_T],
     *additional_columns: _ORMColumnExprArgument[Any],
@@ -325,7 +354,7 @@ def column_property(
     deferred: bool = False,
     raiseload: bool = False,
     comparator_factory: Optional[Type[PropComparator[_T]]] = None,
-    init: Union[_NoArg, bool] = _NoArg.NO_ARG,
+    init: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     default: Optional[Any] = _NoArg.NO_ARG,
     default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
@@ -338,49 +367,58 @@ def column_property(
 ) -> MappedSQLExpression[_T]:
     r"""Provide a column-level property for use with a mapping.
 
-    Column-based properties can normally be applied to the mapper's
-    ``properties`` dictionary using the :class:`_schema.Column`
-    element directly.
-    Use this function when the given column is not directly present within
-    the mapper's selectable; examples include SQL expressions, functions,
-    and scalar SELECT queries.
+    With Declarative mappings, :func:`_orm.column_property` is used to
+    map read-only SQL expressions to a mapped class.
+
+    When using Imperative mappings, :func:`_orm.column_property` also
+    takes on the role of mapping table columns with additional features.
+    When using fully Declarative mappings, the :func:`_orm.mapped_column`
+    construct should be used for this purpose.
+
+    With Declarative Dataclass mappings, :func:`_orm.column_property`
+    is considered to be **read only**, and will not be included in the
+    Dataclass ``__init__()`` constructor.
 
     The :func:`_orm.column_property` function returns an instance of
     :class:`.ColumnProperty`.
 
-    Columns that aren't present in the mapper's selectable won't be
-    persisted by the mapper and are effectively "read-only" attributes.
+    .. seealso::
+
+        :ref:`mapper_column_property_sql_expressions` - general use of
+        :func:`_orm.column_property` to map SQL expressions
+
+        :ref:`orm_imperative_table_column_options` - usage of
+        :func:`_orm.column_property` with Imperative Table mappings to apply
+        additional options to a plain :class:`_schema.Column` object
 
     :param \*cols:
-          list of Column objects to be mapped.
+        list of Column objects to be mapped.
 
     :param active_history=False:
-      When ``True``, indicates that the "previous" value for a
-      scalar attribute should be loaded when replaced, if not
-      already loaded. Normally, history tracking logic for
-      simple non-primary-key scalar values only needs to be
-      aware of the "new" value in order to perform a flush. This
-      flag is available for applications that make use of
-      :func:`.attributes.get_history` or :meth:`.Session.is_modified`
-      which also need to know
-      the "previous" value of the attribute.
+
+        Used only for Imperative Table mappings, or legacy-style Declarative
+        mappings (i.e. which have not been upgraded to
+        :func:`_orm.mapped_column`), for column-based attributes that are
+        expected to be writeable; use :func:`_orm.mapped_column` with
+        :paramref:`_orm.mapped_column.active_history` for Declarative mappings.
+        See that parameter for functional details.
 
     :param comparator_factory: a class which extends
-       :class:`.ColumnProperty.Comparator` which provides custom SQL
-       clause generation for comparison operations.
+        :class:`.ColumnProperty.Comparator` which provides custom SQL
+        clause generation for comparison operations.
 
     :param group:
         a group name for this property when marked as deferred.
 
     :param deferred:
-          when True, the column property is "deferred", meaning that
-          it does not load immediately, and is instead loaded when the
-          attribute is first accessed on an instance.  See also
-          :func:`~sqlalchemy.orm.deferred`.
+        when True, the column property is "deferred", meaning that
+        it does not load immediately, and is instead loaded when the
+        attribute is first accessed on an instance.  See also
+        :func:`~sqlalchemy.orm.deferred`.
 
     :param doc:
-          optional string that will be applied as the doc on the
-          class-bound descriptor.
+        optional string that will be applied as the doc on the
+        class-bound descriptor.
 
     :param expire_on_flush=True:
         Disable expiry on flush.   A column_property() which refers
@@ -410,20 +448,25 @@ def column_property(
 
             :ref:`orm_queryguide_deferred_raiseload`
 
-    .. seealso::
+    :param init:
+
+    :param default:
 
-        :ref:`column_property_options` - to map columns while including
-        mapping options
+    :param default_factory:
 
-        :ref:`mapper_column_property_sql_expressions` - to map SQL
-        expressions
+    :param kw_only:
 
     """
     return MappedSQLExpression(
         column,
         *additional_columns,
         attribute_options=_AttributeOptions(
-            init, repr, default, default_factory, compare, kw_only
+            False if init is _NoArg.NO_ARG else init,
+            repr,
+            default,
+            default_factory,
+            compare,
+            kw_only,
         ),
         group=group,
         deferred=deferred,
@@ -433,6 +476,7 @@ def column_property(
         expire_on_flush=expire_on_flush,
         info=info,
         doc=doc,
+        _assume_readonly_dc_attributes=True,
     )
 
 
@@ -2017,6 +2061,7 @@ def query_expression(
     default_expr: _ORMColumnExprArgument[_T] = sql.null(),
     *,
     repr: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
+    compare: Union[_NoArg, bool] = _NoArg.NO_ARG,  # noqa: A002
     expire_on_flush: bool = True,
     info: Optional[_InfoType] = None,
     doc: Optional[str] = None,
@@ -2036,16 +2081,17 @@ def query_expression(
     prop = MappedSQLExpression(
         default_expr,
         attribute_options=_AttributeOptions(
-            _NoArg.NO_ARG,
+            False,
             repr,
             _NoArg.NO_ARG,
             _NoArg.NO_ARG,
-            _NoArg.NO_ARG,
+            compare,
             _NoArg.NO_ARG,
         ),
         expire_on_flush=expire_on_flush,
         info=info,
         doc=doc,
+        _assume_readonly_dc_attributes=True,
     )
 
     prop.strategy_key = (("query_expression", True),)
index 60d2fbc2b76ed5b26d72b01a5fa5ae1084164ff2..ed001023b5fdc8573fb38a9be1171d207c8e22d6 100644 (file)
@@ -37,11 +37,9 @@ from . import clsregistry
 from . import instrumentation
 from . import interfaces
 from . import mapperlib
-from ._orm_constructors import column_property
 from ._orm_constructors import composite
 from ._orm_constructors import deferred
 from ._orm_constructors import mapped_column
-from ._orm_constructors import query_expression
 from ._orm_constructors import relationship
 from ._orm_constructors import synonym
 from .attributes import InstrumentedAttribute
@@ -59,7 +57,6 @@ from .descriptor_props import Composite
 from .descriptor_props import Synonym
 from .descriptor_props import Synonym as _orm_synonym
 from .mapper import Mapper
-from .properties import ColumnProperty
 from .properties import MappedColumn
 from .relationships import RelationshipProperty
 from .state import InstanceState
@@ -153,15 +150,12 @@ class DeclarativeAttributeIntercept(
         MappedColumn,
         RelationshipProperty,
         Composite,
-        ColumnProperty,
         Synonym,
         mapped_column,
         relationship,
         composite,
-        column_property,
         synonym,
         deferred,
-        query_expression,
     ),
 )
 class DCTransformDeclarative(DeclarativeAttributeIntercept):
@@ -1549,15 +1543,12 @@ class registry:
             MappedColumn,
             RelationshipProperty,
             Composite,
-            ColumnProperty,
             Synonym,
             mapped_column,
             relationship,
             composite,
-            column_property,
             synonym,
             deferred,
-            query_expression,
         ),
     )
     @overload
index 8667491398f94e9f5e1bea13ece54b370042bfc0..2af883da2d76f6bd80c3da72b69367069180f4e3 100644 (file)
@@ -269,6 +269,15 @@ _DEFAULT_ATTRIBUTE_OPTIONS = _AttributeOptions(
     _NoArg.NO_ARG,
 )
 
+_DEFAULT_READONLY_ATTRIBUTE_OPTIONS = _AttributeOptions(
+    False,
+    _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
+    _NoArg.NO_ARG,
+)
+
 
 class _DCAttributeOptions:
     """mixin for descriptors or configurational objects that include dataclass
@@ -519,19 +528,24 @@ class MapperProperty(
         """
 
     def __init__(
-        self, attribute_options: Optional[_AttributeOptions] = None
+        self,
+        attribute_options: Optional[_AttributeOptions] = None,
+        _assume_readonly_dc_attributes: bool = False,
     ) -> None:
         self._configure_started = False
         self._configure_finished = False
-        if (
-            attribute_options
-            and attribute_options != _DEFAULT_ATTRIBUTE_OPTIONS
-        ):
+
+        if _assume_readonly_dc_attributes:
+            default_attrs = _DEFAULT_READONLY_ATTRIBUTE_OPTIONS
+        else:
+            default_attrs = _DEFAULT_ATTRIBUTE_OPTIONS
+
+        if attribute_options and attribute_options != default_attrs:
             self._has_dataclass_arguments = True
             self._attribute_options = attribute_options
         else:
             self._has_dataclass_arguments = False
-            self._attribute_options = _DEFAULT_ATTRIBUTE_OPTIONS
+            self._attribute_options = default_attrs
 
     def init(self) -> None:
         """Called after all mappers are created to assemble
index 2f7b85d88fc13e69de2fb862f920200730a85f04..f007758742eb329a221911e5be9cf07ecc42a813 100644 (file)
@@ -151,8 +151,12 @@ class ColumnProperty(
         info: Optional[_InfoType] = None,
         doc: Optional[str] = None,
         _instrument: bool = True,
+        _assume_readonly_dc_attributes: bool = False,
     ):
-        super().__init__(attribute_options=attribute_options)
+        super().__init__(
+            attribute_options=attribute_options,
+            _assume_readonly_dc_attributes=_assume_readonly_dc_attributes,
+        )
         columns = (column,) + additional_columns
         self.columns = [
             coercions.expect(roles.LabeledColumnExprRole, c) for c in columns
@@ -532,6 +536,7 @@ class MappedColumn(
         "deferred",
         "deferred_group",
         "deferred_raiseload",
+        "active_history",
         "_attribute_options",
         "_has_dataclass_arguments",
         "_use_existing_column",
@@ -579,7 +584,7 @@ class MappedColumn(
             self.deferred = bool(
                 self.deferred_group or self.deferred_raiseload
             )
-
+        self.active_history = kw.pop("active_history", False)
         self._sort_order = kw.pop("sort_order", 0)
         self.column = cast("Column[_T]", Column(*arg, **kw))
         self.foreign_keys = self.column.foreign_keys
@@ -597,6 +602,7 @@ class MappedColumn(
         new.deferred_group = self.deferred_group
         new.deferred_raiseload = self.deferred_raiseload
         new.foreign_keys = new.column.foreign_keys
+        new.active_history = self.active_history
         new._has_nullable = self._has_nullable
         new._attribute_options = self._attribute_options
         new._has_insert_default = self._has_insert_default
@@ -612,13 +618,14 @@ class MappedColumn(
 
     @property
     def mapper_property_to_assign(self) -> Optional[MapperProperty[_T]]:
-        if self.deferred:
+        if self.deferred or self.active_history:
             return ColumnProperty(
                 self.column,
-                deferred=True,
+                deferred=self.deferred,
                 group=self.deferred_group,
                 raiseload=self.deferred_raiseload,
                 attribute_options=self._attribute_options,
+                active_history=self.active_history,
             )
         else:
             return None
index 1752a4dc1ab68856a31d924b36ef774d2f48d5b5..8186f6ade3bc5127af5a83dff767f8b255af8d40 100644 (file)
@@ -101,6 +101,9 @@ if not TYPE_CHECKING:
 class _NoArg(Enum):
     NO_ARG = 0
 
+    def __repr__(self):
+        return f"_NoArg.{self.name}"
+
 
 NO_ARG = _NoArg.NO_ARG
 
index 097150712a9ea4b5c3e17265fc9949148eeedfad..e32ab9e0d01081f56b78145ded96888275b724ee 100644 (file)
@@ -226,6 +226,7 @@ def deprecated_params(**specs: Tuple[str, str]) -> Callable[[_F], _F]:
 
         check_defaults: Union[Set[str], Tuple[()]]
         if spec.defaults is not None:
+
             defaults = dict(
                 zip(
                     spec.args[(len(spec.args) - len(spec.defaults)) :],
@@ -234,6 +235,11 @@ def deprecated_params(**specs: Tuple[str, str]) -> Callable[[_F], _F]:
             )
             check_defaults = set(defaults).intersection(messages)
             check_kw = set(messages).difference(defaults)
+        elif spec.kwonlydefaults is not None:
+
+            defaults = spec.kwonlydefaults
+            check_defaults = set(defaults).intersection(messages)
+            check_kw = set(messages).difference(defaults)
         else:
             check_defaults = ()
             check_kw = set(messages)
index 6ff069c4e65932cb6f374948044e556fa184266f..903d8bdeb42d81596efe0bba48ed959683439bdf 100644 (file)
@@ -290,6 +290,9 @@ def %(name)s%(grouped_args)s:
 """
                 % metadata
             )
+
+        mod = sys.modules[fn.__module__]
+        env.update(vars(mod))
         env.update({targ_name: target, fn_name: fn, "__name__": fn.__module__})
 
         decorated = cast(
diff --git a/test/ext/mypy/plain_files/dataclass_transforms_one.py b/test/ext/mypy/plain_files/dataclass_transforms_one.py
new file mode 100644 (file)
index 0000000..b7b8859
--- /dev/null
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+from typing import Optional
+
+from sqlalchemy.orm import column_property
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+from sqlalchemy.orm import MappedAsDataclass
+from sqlalchemy.orm import query_expression
+
+
+class Base(DeclarativeBase):
+    pass
+
+
+class TestInitialSupport(Base):
+    __tablename__ = "a"
+
+    id: Mapped[int] = mapped_column(primary_key=True, init=False)
+    data: Mapped[str]
+    x: Mapped[Optional[int]] = mapped_column(default=None)
+    y: Mapped[Optional[int]] = mapped_column(kw_only=True)
+
+
+tis = TestInitialSupport(data="some data", y=5)
+
+# EXPECTED_TYPE: str
+reveal_type(tis.data)
+
+# EXPECTED_RE_TYPE: .*Union\[builtins.int, None\]
+reveal_type(tis.y)
+
+tis.data = "some other data"
+
+
+class TestTicket9628(MappedAsDataclass, Base):
+    __tablename__ = "ticket_9628"
+
+    id: Mapped[int] = mapped_column(primary_key=True, init=False)
+    data: Mapped[str] = mapped_column()
+
+    d2: Mapped[str] = column_property(data + "Asdf")
+    d3: Mapped[str] = query_expression(data + "Asdf")
+
+
+# d2 and d3 are not required, as these have init=False.  We omit
+# them from dataclass transforms entirely as these are never intended
+# to be writeable fields in a 2.0 declarative mapping
+t9628 = TestTicket9628(data="asf")
diff --git a/test/ext/mypy/plain_files/pep681.py b/test/ext/mypy/plain_files/pep681.py
deleted file mode 100644 (file)
index caa219d..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-from __future__ import annotations
-
-from typing import Optional
-
-from sqlalchemy.orm import DeclarativeBase
-from sqlalchemy.orm import Mapped
-from sqlalchemy.orm import mapped_column
-from sqlalchemy.orm import MappedAsDataclass
-
-
-class Base(MappedAsDataclass, DeclarativeBase):
-    pass
-
-
-class A(Base):
-    __tablename__ = "a"
-
-    id: Mapped[int] = mapped_column(primary_key=True, init=False)
-    data: Mapped[str]
-    x: Mapped[Optional[int]] = mapped_column(default=None)
-    y: Mapped[Optional[int]] = mapped_column(kw_only=True)
-
-
-a1 = A(data="some data", y=5)
-
-# EXPECTED_TYPE: str
-reveal_type(a1.data)
-
-# EXPECTED_RE_TYPE: .*Union\[builtins.int, None\]
-reveal_type(a1.y)
-
-a1.data = "some other data"
index 2d712c823e9f0897b622fc0f5f7c54ec197104f8..698b66db19f54bfbe4949b7a1099dbab883103a2 100644 (file)
@@ -2593,6 +2593,26 @@ class DeclarativeMultiBaseTest(
         sess.expunge_all()
         eq_(sess.query(User).all(), [User(name="u1", a="a", b="b")])
 
+    def test_active_history_columns(self):
+        class Foo(Base):
+            __tablename__ = "foo"
+
+            id = Column(
+                Integer, primary_key=True, test_needs_autoincrement=True
+            )
+            a = column_property(Column(String), active_history=True)
+            b = mapped_column(String, active_history=True)
+            c = column_property(Column(String))
+            d = mapped_column(String)
+
+        self.assert_compile(
+            select(Foo), "SELECT foo.id, foo.a, foo.b, foo.c, foo.d FROM foo"
+        )
+        eq_(Foo.a.impl.active_history, True)
+        eq_(Foo.b.impl.active_history, True)
+        eq_(Foo.c.impl.active_history, False)
+        eq_(Foo.d.impl.active_history, False)
+
     def test_column_properties(self):
         class Address(Base, fixtures.ComparableEntity):
 
index 031aad5d52513754399dc437c3184735473b5d24..576ee7fbfeb6cd73d2627762bd109493fea78ec0 100644 (file)
@@ -39,11 +39,13 @@ from sqlalchemy.orm import Mapped
 from sqlalchemy.orm import mapped_column
 from sqlalchemy.orm import MappedAsDataclass
 from sqlalchemy.orm import MappedColumn
+from sqlalchemy.orm import query_expression
 from sqlalchemy.orm import registry
 from sqlalchemy.orm import registry as _RegistryType
 from sqlalchemy.orm import relationship
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import synonym
+from sqlalchemy.sql.base import _NoArg
 from sqlalchemy.testing import AssertsCompiledSQL
 from sqlalchemy.testing import eq_
 from sqlalchemy.testing import eq_regex
@@ -1355,9 +1357,7 @@ class DataclassArgsTest(fixtures.TestBase):
         else:
             return args, args
 
-    @testing.fixture(
-        params=["mapped_column", "synonym", "deferred", "column_property"]
-    )
+    @testing.fixture(params=["mapped_column", "synonym", "deferred"])
     def mapped_expr_constructor(self, request):
         name = request.param
 
@@ -1367,8 +1367,6 @@ class DataclassArgsTest(fixtures.TestBase):
             yield synonym("some_int", default=7, init=True)
         elif name == "deferred":
             yield deferred(Column(Integer), default=7, init=True)
-        elif name == "column_property":
-            yield column_property(Column(Integer), default=7, init=True)
 
     def test_attrs_rejected_if_not_a_dc(
         self, mapped_expr_constructor, decl_base: Type[DeclarativeBase]
@@ -1725,7 +1723,6 @@ class DataclassArgsTest(fixtures.TestBase):
     @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),
@@ -1752,6 +1749,28 @@ class DataclassArgsTest(fixtures.TestBase):
         prop = construct(**kw)
         eq_(prop._attribute_options, exp)
 
+    @testing.variation("use_arguments", [True, False])
+    @testing.combinations(
+        lambda **kw: column_property(Column(Integer), **kw),
+        lambda **kw: query_expression(**kw),
+        argnames="construct",
+    )
+    def test_ro_attribute_options(self, use_arguments, construct):
+        if use_arguments:
+            kw = {
+                "repr": False,
+                "compare": True,
+            }
+            exp = interfaces._AttributeOptions(
+                False, False, _NoArg.NO_ARG, _NoArg.NO_ARG, True, _NoArg.NO_ARG
+            )
+        else:
+            kw = {}
+            exp = interfaces._DEFAULT_READONLY_ATTRIBUTE_OPTIONS
+
+        prop = construct(**kw)
+        eq_(prop._attribute_options, exp)
+
 
 class MixinColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
     """tests for #8718"""
@@ -1978,3 +1997,78 @@ class CompositeTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             "state='NY', zip_='12345'))",
         )
         eq_(repr(u2), "mymodule.User(name='u2', address=None)")
+
+
+class ReadOnlyAttrTest(fixtures.TestBase, testing.AssertsCompiledSQL):
+    """tests related to #9628"""
+
+    __dialect__ = "default"
+
+    @testing.combinations(
+        (query_expression,), (column_property,), argnames="construct"
+    )
+    def test_default_behavior(
+        self, dc_decl_base: Type[MappedAsDataclass], construct
+    ):
+        class MyClass(dc_decl_base):
+            __tablename__ = "a"
+
+            id: Mapped[int] = mapped_column(primary_key=True, init=False)
+            data: Mapped[str] = mapped_column()
+
+            const: Mapped[str] = construct(data + "asdf")
+
+        m1 = MyClass(data="foo")
+        eq_(m1, MyClass(data="foo"))
+        ne_(m1, MyClass(data="bar"))
+
+        eq_regex(
+            repr(m1),
+            r".*MyClass\(id=None, data='foo', const=None\)",
+        )
+
+    @testing.combinations(
+        (query_expression,), (column_property,), argnames="construct"
+    )
+    def test_no_repr_behavior(
+        self, dc_decl_base: Type[MappedAsDataclass], construct
+    ):
+        class MyClass(dc_decl_base):
+            __tablename__ = "a"
+
+            id: Mapped[int] = mapped_column(primary_key=True, init=False)
+            data: Mapped[str] = mapped_column()
+
+            const: Mapped[str] = construct(data + "asdf", repr=False)
+
+        m1 = MyClass(data="foo")
+
+        eq_regex(
+            repr(m1),
+            r".*MyClass\(id=None, data='foo'\)",
+        )
+
+    @testing.combinations(
+        (query_expression,), (column_property,), argnames="construct"
+    )
+    def test_enable_compare(
+        self, dc_decl_base: Type[MappedAsDataclass], construct
+    ):
+        class MyClass(dc_decl_base):
+            __tablename__ = "a"
+
+            id: Mapped[int] = mapped_column(primary_key=True, init=False)
+            data: Mapped[str] = mapped_column()
+
+            const: Mapped[str] = construct(data + "asdf", compare=True)
+
+        m1 = MyClass(data="foo")
+        eq_(m1, MyClass(data="foo"))
+        ne_(m1, MyClass(data="bar"))
+
+        m2 = MyClass(data="foo")
+        m2.const = "some const"
+        ne_(m2, MyClass(data="foo"))
+        m3 = MyClass(data="foo")
+        m3.const = "some const"
+        eq_(m2, m3)
index 5ce7475f2b41d9a4443fa118fbbe6a20fdafd531..e1eb6e9e1d12940c9179650a33f0d0873c3d625e 100644 (file)
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from typing import Union
+
 import sqlalchemy as sa
 from sqlalchemy import ForeignKey
 from sqlalchemy import func
@@ -132,8 +136,8 @@ class DeferredTest(AssertsCompiledSQL, _fixtures.FixtureTest):
             ],
         )
 
-    @testing.combinations(True, False, None, "deferred_parameter")
-    def test_group_defer_newstyle(self, deferred_parameter):
+    @testing.combinations(True, False, None, argnames="deferred_parameter")
+    def test_group_defer_newstyle(self, deferred_parameter: Union[bool, None]):
         class Base(DeclarativeBase):
             pass
 
index 72256396410396b478df8fa691c9b15b5dae59dd..a3e2f4ef720b54001638a40176dfdae1a7337b55 100644 (file)
@@ -3,6 +3,7 @@ from unittest.mock import Mock
 
 import sqlalchemy as sa
 from sqlalchemy import cast
+from sqlalchemy import column
 from sqlalchemy import desc
 from sqlalchemy import event
 from sqlalchemy import exc as sa_exc
@@ -31,6 +32,8 @@ from sqlalchemy.orm import deferred
 from sqlalchemy.orm import foreign
 from sqlalchemy.orm import instrumentation
 from sqlalchemy.orm import joinedload
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
 from sqlalchemy.orm import relationship
 from sqlalchemy.orm import scoped_session
 from sqlalchemy.orm import Session
@@ -48,6 +51,7 @@ from sqlalchemy.testing import AssertsCompiledSQL
 from sqlalchemy.testing import eq_
 from sqlalchemy.testing import eq_ignore_whitespace
 from sqlalchemy.testing import expect_deprecated
+from sqlalchemy.testing import expect_raises_message
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import is_
 from sqlalchemy.testing import is_true
@@ -399,6 +403,63 @@ class MiscDeprecationsTest(fixtures.TestBase):
 
         is_(EvaluatorCompiler, _EvaluatorCompiler)
 
+    @testing.combinations(
+        ("init", True),
+        ("kw_only", True, testing.requires.python310),
+        ("default", 5),
+        ("default_factory", lambda: 10),
+        argnames="paramname, value",
+    )
+    def test_column_property_dc_attributes(self, paramname, value):
+        with expect_deprecated(
+            rf"The column_property.{paramname} parameter is deprecated "
+            r"for column_property\(\)",
+            raise_on_any_unexpected=True,
+        ):
+            column_property(column("q"), **{paramname: value})
+
+    @testing.requires.python310
+    def test_column_property_dc_attributes_still_function(self, dc_decl_base):
+        with expect_deprecated(
+            r"The column_property.init parameter is deprecated "
+            r"for column_property\(\)",
+            r"The column_property.default parameter is deprecated "
+            r"for column_property\(\)",
+            r"The column_property.default_factory parameter is deprecated "
+            r"for column_property\(\)",
+            r"The column_property.kw_only parameter is deprecated "
+            r"for column_property\(\)",
+            raise_on_any_unexpected=True,
+        ):
+
+            class MyClass(dc_decl_base):
+                __tablename__ = "a"
+
+                id: Mapped[int] = mapped_column(primary_key=True, init=False)
+                data: Mapped[str] = mapped_column()
+
+                const1: Mapped[str] = column_property(
+                    data + "asdf", init=True, default="foobar"
+                )
+                const2: Mapped[str] = column_property(
+                    data + "asdf",
+                    init=True,
+                    default_factory=lambda: "factory_foo",
+                )
+                const3: Mapped[str] = column_property(
+                    data + "asdf", init=True, kw_only=True
+                )
+
+            m1 = MyClass(data="d1", const3="c3")
+            eq_(m1.const1, "foobar")
+            eq_(m1.const2, "factory_foo")
+            eq_(m1.const3, "c3")
+
+        with expect_raises_message(
+            TypeError, "missing 1 required keyword-only argument: 'const3'"
+        ):
+            MyClass(data="d1")
+
 
 class DeprecatedQueryTest(_fixtures.FixtureTest, AssertsCompiledSQL):
     __dialect__ = "default"