diff options
author | Federico Caselli <cfederico87@gmail.com> | 2023-04-12 21:21:36 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@bbpush.zzzcomputing.com> | 2023-04-12 21:21:36 +0000 |
commit | 53be3fc70a44d34fda1ffddcf9d67473d3de50b4 (patch) | |
tree | fbadb6bb7d078ad6041224270f11973232952b24 /lib/sqlalchemy | |
parent | 107ec58bdfbcbb09f40d92590f8197ffa683a925 (diff) | |
parent | 9f43b10e9014e694cb89fe2899dc52f602bf2197 (diff) | |
download | sqlalchemy-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.py | 16 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/_orm_constructors.py | 116 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/decl_api.py | 9 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 26 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/properties.py | 15 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/base.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/util/deprecations.py | 6 | ||||
-rw-r--r-- | lib/sqlalchemy/util/langhelpers.py | 3 |
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( |