summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2023-04-10 12:56:47 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2023-04-12 15:11:03 -0400
commit9f43b10e9014e694cb89fe2899dc52f602bf2197 (patch)
tree03c54cbc80bd98e9d358b6a21a029b8524142696
parent6e5ed192c6435ec107eae524bb2c6959c38bc654 (diff)
downloadsqlalchemy-9f43b10e9014e694cb89fe2899dc52f602bf2197.tar.gz
establish column_property and query_expression as readonly from a dc perspective
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
-rw-r--r--doc/build/changelog/unreleased_20/9628.rst29
-rw-r--r--doc/build/orm/declarative_tables.rst4
-rw-r--r--lib/sqlalchemy/ext/mypy/names.py16
-rw-r--r--lib/sqlalchemy/orm/_orm_constructors.py116
-rw-r--r--lib/sqlalchemy/orm/decl_api.py9
-rw-r--r--lib/sqlalchemy/orm/interfaces.py26
-rw-r--r--lib/sqlalchemy/orm/properties.py15
-rw-r--r--lib/sqlalchemy/sql/base.py3
-rw-r--r--lib/sqlalchemy/util/deprecations.py6
-rw-r--r--lib/sqlalchemy/util/langhelpers.py3
-rw-r--r--test/ext/mypy/plain_files/dataclass_transforms_one.py50
-rw-r--r--test/ext/mypy/plain_files/pep681.py32
-rw-r--r--test/orm/declarative/test_basic.py20
-rw-r--r--test/orm/declarative/test_dc_transforms.py106
-rw-r--r--test/orm/test_deferred.py8
-rw-r--r--test/orm/test_deprecations.py61
16 files changed, 407 insertions, 97 deletions
diff --git a/doc/build/changelog/unreleased_20/9628.rst b/doc/build/changelog/unreleased_20/9628.rst
new file mode 100644
index 000000000..0d9368349
--- /dev/null
+++ b/doc/build/changelog/unreleased_20/9628.rst
@@ -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.
diff --git a/doc/build/orm/declarative_tables.rst b/doc/build/orm/declarative_tables.rst
index d9a11087d..0ee40cd07 100644
--- a/doc/build/orm/declarative_tables.rst
+++ b/doc/build/orm/declarative_tables.rst
@@ -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
diff --git a/lib/sqlalchemy/ext/mypy/names.py b/lib/sqlalchemy/ext/mypy/names.py
index fac6bf5b1..989f25592 100644
--- a/lib/sqlalchemy/ext/mypy/names.py
+++ b/lib/sqlalchemy/ext/mypy/names.py
@@ -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
diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py
index 57acc5706..1a1780158 100644
--- a/lib/sqlalchemy/orm/_orm_constructors.py
+++ b/lib/sqlalchemy/orm/_orm_constructors.py
@@ -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),)
diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py
index 60d2fbc2b..ed001023b 100644
--- a/lib/sqlalchemy/orm/decl_api.py
+++ b/lib/sqlalchemy/orm/decl_api.py
@@ -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
diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py
index 866749139..2af883da2 100644
--- a/lib/sqlalchemy/orm/interfaces.py
+++ b/lib/sqlalchemy/orm/interfaces.py
@@ -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
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index 2f7b85d88..f00775874 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -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
diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py
index 1752a4dc1..8186f6ade 100644
--- a/lib/sqlalchemy/sql/base.py
+++ b/lib/sqlalchemy/sql/base.py
@@ -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
diff --git a/lib/sqlalchemy/util/deprecations.py b/lib/sqlalchemy/util/deprecations.py
index 097150712..e32ab9e0d 100644
--- a/lib/sqlalchemy/util/deprecations.py
+++ b/lib/sqlalchemy/util/deprecations.py
@@ -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)
diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py
index 6ff069c4e..903d8bdeb 100644
--- a/lib/sqlalchemy/util/langhelpers.py
+++ b/lib/sqlalchemy/util/langhelpers.py
@@ -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
index 000000000..b7b88590f
--- /dev/null
+++ b/test/ext/mypy/plain_files/dataclass_transforms_one.py
@@ -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
index caa219d78..000000000
--- a/test/ext/mypy/plain_files/pep681.py
+++ /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"
diff --git a/test/orm/declarative/test_basic.py b/test/orm/declarative/test_basic.py
index 2d712c823..698b66db1 100644
--- a/test/orm/declarative/test_basic.py
+++ b/test/orm/declarative/test_basic.py
@@ -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):
diff --git a/test/orm/declarative/test_dc_transforms.py b/test/orm/declarative/test_dc_transforms.py
index 031aad5d5..576ee7fbf 100644
--- a/test/orm/declarative/test_dc_transforms.py
+++ b/test/orm/declarative/test_dc_transforms.py
@@ -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)
diff --git a/test/orm/test_deferred.py b/test/orm/test_deferred.py
index 5ce7475f2..e1eb6e9e1 100644
--- a/test/orm/test_deferred.py
+++ b/test/orm/test_deferred.py
@@ -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
diff --git a/test/orm/test_deprecations.py b/test/orm/test_deprecations.py
index 722563964..a3e2f4ef7 100644
--- a/test/orm/test_deprecations.py
+++ b/test/orm/test_deprecations.py
@@ -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"