summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorFederico Caselli <cfederico87@gmail.com>2023-04-12 21:21:36 +0000
committerGerrit Code Review <gerrit@bbpush.zzzcomputing.com>2023-04-12 21:21:36 +0000
commit53be3fc70a44d34fda1ffddcf9d67473d3de50b4 (patch)
treefbadb6bb7d078ad6041224270f11973232952b24 /lib/sqlalchemy
parent107ec58bdfbcbb09f40d92590f8197ffa683a925 (diff)
parent9f43b10e9014e694cb89fe2899dc52f602bf2197 (diff)
downloadsqlalchemy-53be3fc70a44d34fda1ffddcf9d67473d3de50b4.tar.gz
Merge "establish column_property and query_expression as readonly from a dc perspective" into main
Diffstat (limited to 'lib/sqlalchemy')
-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
8 files changed, 139 insertions, 55 deletions
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(