1# orm/descriptor_props.py
2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8"""Descriptor properties are more "auxiliary" properties
9that exist as configurational elements, but don't participate
10as actively in the load/persist ORM loop.
11
12"""
13from __future__ import annotations
14
15from dataclasses import is_dataclass
16import inspect
17import itertools
18import operator
19import typing
20from typing import Any
21from typing import Callable
22from typing import Dict
23from typing import List
24from typing import NoReturn
25from typing import Optional
26from typing import Sequence
27from typing import Tuple
28from typing import Type
29from typing import TYPE_CHECKING
30from typing import TypeVar
31from typing import Union
32import weakref
33
34from . import attributes
35from . import util as orm_util
36from .base import _DeclarativeMapped
37from .base import DONT_SET
38from .base import LoaderCallableStatus
39from .base import Mapped
40from .base import PassiveFlag
41from .base import SQLORMOperations
42from .interfaces import _AttributeOptions
43from .interfaces import _IntrospectsAnnotations
44from .interfaces import _MapsColumns
45from .interfaces import MapperProperty
46from .interfaces import PropComparator
47from .util import _none_set
48from .util import de_stringify_annotation
49from .. import event
50from .. import exc as sa_exc
51from .. import schema
52from .. import sql
53from .. import util
54from ..sql import expression
55from ..sql import operators
56from ..sql.base import _NoArg
57from ..sql.elements import BindParameter
58from ..util.typing import get_args
59from ..util.typing import is_fwd_ref
60from ..util.typing import is_pep593
61from ..util.typing import TupleAny
62from ..util.typing import Unpack
63
64
65if typing.TYPE_CHECKING:
66 from ._typing import _InstanceDict
67 from ._typing import _RegistryType
68 from .attributes import History
69 from .attributes import InstrumentedAttribute
70 from .attributes import QueryableAttribute
71 from .context import _ORMCompileState
72 from .decl_base import _ClassScanMapperConfig
73 from .interfaces import _DataclassArguments
74 from .mapper import Mapper
75 from .properties import ColumnProperty
76 from .properties import MappedColumn
77 from .state import InstanceState
78 from ..engine.base import Connection
79 from ..engine.row import Row
80 from ..sql._typing import _DMLColumnArgument
81 from ..sql._typing import _InfoType
82 from ..sql.elements import ClauseList
83 from ..sql.elements import ColumnElement
84 from ..sql.operators import OperatorType
85 from ..sql.schema import Column
86 from ..sql.selectable import Select
87 from ..util.typing import _AnnotationScanType
88 from ..util.typing import CallableReference
89 from ..util.typing import DescriptorReference
90 from ..util.typing import RODescriptorReference
91
92_T = TypeVar("_T", bound=Any)
93_PT = TypeVar("_PT", bound=Any)
94
95
96class DescriptorProperty(MapperProperty[_T]):
97 """:class:`.MapperProperty` which proxies access to a
98 user-defined descriptor."""
99
100 doc: Optional[str] = None
101
102 uses_objects = False
103 _links_to_entity = False
104
105 descriptor: DescriptorReference[Any]
106
107 def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
108 raise NotImplementedError(
109 "This MapperProperty does not implement column loader strategies"
110 )
111
112 def get_history(
113 self,
114 state: InstanceState[Any],
115 dict_: _InstanceDict,
116 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
117 ) -> History:
118 raise NotImplementedError()
119
120 def instrument_class(self, mapper: Mapper[Any]) -> None:
121 prop = self
122
123 class _ProxyImpl(attributes._AttributeImpl):
124 accepts_scalar_loader = False
125 load_on_unexpire = True
126 collection = False
127
128 @property
129 def uses_objects(self) -> bool: # type: ignore
130 return prop.uses_objects
131
132 def __init__(self, key: str):
133 self.key = key
134
135 def get_history(
136 self,
137 state: InstanceState[Any],
138 dict_: _InstanceDict,
139 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
140 ) -> History:
141 return prop.get_history(state, dict_, passive)
142
143 if self.descriptor is None:
144 desc = getattr(mapper.class_, self.key, None)
145 if mapper._is_userland_descriptor(self.key, desc):
146 self.descriptor = desc
147
148 if self.descriptor is None:
149
150 def fset(obj: Any, value: Any) -> None:
151 setattr(obj, self.name, value)
152
153 def fdel(obj: Any) -> None:
154 delattr(obj, self.name)
155
156 def fget(obj: Any) -> Any:
157 return getattr(obj, self.name)
158
159 self.descriptor = property(fget=fget, fset=fset, fdel=fdel)
160
161 proxy_attr = attributes._create_proxied_attribute(self.descriptor)(
162 self.parent.class_,
163 self.key,
164 self.descriptor,
165 lambda: self._comparator_factory(mapper),
166 doc=self.doc,
167 original_property=self,
168 )
169
170 proxy_attr.impl = _ProxyImpl(self.key)
171 mapper.class_manager.instrument_attribute(self.key, proxy_attr)
172
173
174_CompositeAttrType = Union[
175 str,
176 "Column[_T]",
177 "MappedColumn[_T]",
178 "InstrumentedAttribute[_T]",
179 "Mapped[_T]",
180]
181
182
183_CC = TypeVar("_CC", bound=Any)
184
185
186_composite_getters: weakref.WeakKeyDictionary[
187 Type[Any], Callable[[Any], Tuple[Any, ...]]
188] = weakref.WeakKeyDictionary()
189
190
191class CompositeProperty(
192 _MapsColumns[_CC], _IntrospectsAnnotations, DescriptorProperty[_CC]
193):
194 """Defines a "composite" mapped attribute, representing a collection
195 of columns as one attribute.
196
197 :class:`.CompositeProperty` is constructed using the :func:`.composite`
198 function.
199
200 .. seealso::
201
202 :ref:`mapper_composite`
203
204 """
205
206 composite_class: Union[Type[_CC], Callable[..., _CC]]
207 attrs: Tuple[_CompositeAttrType[Any], ...]
208
209 _generated_composite_accessor: CallableReference[
210 Optional[Callable[[_CC], Tuple[Any, ...]]]
211 ]
212
213 comparator_factory: Type[Comparator[_CC]]
214
215 def __init__(
216 self,
217 _class_or_attr: Union[
218 None, Type[_CC], Callable[..., _CC], _CompositeAttrType[Any]
219 ] = None,
220 *attrs: _CompositeAttrType[Any],
221 attribute_options: Optional[_AttributeOptions] = None,
222 active_history: bool = False,
223 deferred: bool = False,
224 group: Optional[str] = None,
225 comparator_factory: Optional[Type[Comparator[_CC]]] = None,
226 info: Optional[_InfoType] = None,
227 **kwargs: Any,
228 ):
229 super().__init__(attribute_options=attribute_options)
230
231 if isinstance(_class_or_attr, (Mapped, str, sql.ColumnElement)):
232 self.attrs = (_class_or_attr,) + attrs
233 # will initialize within declarative_scan
234 self.composite_class = None # type: ignore
235 else:
236 self.composite_class = _class_or_attr # type: ignore
237 self.attrs = attrs
238
239 self.active_history = active_history
240 self.deferred = deferred
241 self.group = group
242 self.comparator_factory = (
243 comparator_factory
244 if comparator_factory is not None
245 else self.__class__.Comparator
246 )
247 self._generated_composite_accessor = None
248 if info is not None:
249 self.info.update(info)
250
251 util.set_creation_order(self)
252 self._create_descriptor()
253 self._init_accessor()
254
255 def instrument_class(self, mapper: Mapper[Any]) -> None:
256 super().instrument_class(mapper)
257 self._setup_event_handlers()
258
259 def _composite_values_from_instance(self, value: _CC) -> Tuple[Any, ...]:
260 if self._generated_composite_accessor:
261 return self._generated_composite_accessor(value)
262 else:
263 try:
264 accessor = value.__composite_values__
265 except AttributeError as ae:
266 raise sa_exc.InvalidRequestError(
267 f"Composite class {self.composite_class.__name__} is not "
268 f"a dataclass and does not define a __composite_values__()"
269 " method; can't get state"
270 ) from ae
271 else:
272 return accessor() # type: ignore
273
274 def do_init(self) -> None:
275 """Initialization which occurs after the :class:`.Composite`
276 has been associated with its parent mapper.
277
278 """
279 self._setup_arguments_on_columns()
280
281 _COMPOSITE_FGET = object()
282
283 def _create_descriptor(self) -> None:
284 """Create the Python descriptor that will serve as
285 the access point on instances of the mapped class.
286
287 """
288
289 def fget(instance: Any) -> Any:
290 dict_ = attributes.instance_dict(instance)
291 state = attributes.instance_state(instance)
292
293 if self.key not in dict_:
294 # key not present. Iterate through related
295 # attributes, retrieve their values. This
296 # ensures they all load.
297 values = [
298 getattr(instance, key) for key in self._attribute_keys
299 ]
300
301 # current expected behavior here is that the composite is
302 # created on access if the object is persistent or if
303 # col attributes have non-None. This would be better
304 # if the composite were created unconditionally,
305 # but that would be a behavioral change.
306 if self.key not in dict_ and (
307 state.key is not None or not _none_set.issuperset(values)
308 ):
309 dict_[self.key] = self.composite_class(*values)
310 state.manager.dispatch.refresh(
311 state, self._COMPOSITE_FGET, [self.key]
312 )
313
314 return dict_.get(self.key, None)
315
316 def fset(instance: Any, value: Any) -> None:
317 if value is LoaderCallableStatus.DONT_SET:
318 return
319
320 dict_ = attributes.instance_dict(instance)
321 state = attributes.instance_state(instance)
322 attr = state.manager[self.key]
323
324 if attr.dispatch._active_history:
325 previous = fget(instance)
326 else:
327 previous = dict_.get(self.key, LoaderCallableStatus.NO_VALUE)
328
329 for fn in attr.dispatch.set:
330 value = fn(state, value, previous, attr.impl)
331 dict_[self.key] = value
332 if value is None:
333 for key in self._attribute_keys:
334 setattr(instance, key, None)
335 else:
336 for key, value in zip(
337 self._attribute_keys,
338 self._composite_values_from_instance(value),
339 ):
340 setattr(instance, key, value)
341
342 def fdel(instance: Any) -> None:
343 state = attributes.instance_state(instance)
344 dict_ = attributes.instance_dict(instance)
345 attr = state.manager[self.key]
346
347 if attr.dispatch._active_history:
348 previous = fget(instance)
349 dict_.pop(self.key, None)
350 else:
351 previous = dict_.pop(self.key, LoaderCallableStatus.NO_VALUE)
352
353 attr = state.manager[self.key]
354 attr.dispatch.remove(state, previous, attr.impl)
355 for key in self._attribute_keys:
356 setattr(instance, key, None)
357
358 self.descriptor = property(fget, fset, fdel)
359
360 @util.preload_module("sqlalchemy.orm.properties")
361 def declarative_scan(
362 self,
363 decl_scan: _ClassScanMapperConfig,
364 registry: _RegistryType,
365 cls: Type[Any],
366 originating_module: Optional[str],
367 key: str,
368 mapped_container: Optional[Type[Mapped[Any]]],
369 annotation: Optional[_AnnotationScanType],
370 extracted_mapped_annotation: Optional[_AnnotationScanType],
371 is_dataclass_field: bool,
372 ) -> None:
373 MappedColumn = util.preloaded.orm_properties.MappedColumn
374 if (
375 self.composite_class is None
376 and extracted_mapped_annotation is None
377 ):
378 self._raise_for_required(key, cls)
379 argument = extracted_mapped_annotation
380
381 if is_pep593(argument):
382 argument = get_args(argument)[0]
383
384 if argument and self.composite_class is None:
385 if isinstance(argument, str) or is_fwd_ref(
386 argument, check_generic=True
387 ):
388 if originating_module is None:
389 str_arg = (
390 argument.__forward_arg__
391 if hasattr(argument, "__forward_arg__")
392 else str(argument)
393 )
394 raise sa_exc.ArgumentError(
395 f"Can't use forward ref {argument} for composite "
396 f"class argument; set up the type as Mapped[{str_arg}]"
397 )
398 argument = de_stringify_annotation(
399 cls, argument, originating_module, include_generic=True
400 )
401
402 self.composite_class = argument
403
404 if is_dataclass(self.composite_class):
405 self._setup_for_dataclass(registry, cls, originating_module, key)
406 else:
407 for attr in self.attrs:
408 if (
409 isinstance(attr, (MappedColumn, schema.Column))
410 and attr.name is None
411 ):
412 raise sa_exc.ArgumentError(
413 "Composite class column arguments must be named "
414 "unless a dataclass is used"
415 )
416 self._init_accessor()
417
418 def _init_accessor(self) -> None:
419 if is_dataclass(self.composite_class) and not hasattr(
420 self.composite_class, "__composite_values__"
421 ):
422 insp = inspect.signature(self.composite_class)
423 getter = operator.attrgetter(
424 *[p.name for p in insp.parameters.values()]
425 )
426 if len(insp.parameters) == 1:
427 self._generated_composite_accessor = lambda obj: (getter(obj),)
428 else:
429 self._generated_composite_accessor = getter
430
431 if (
432 self.composite_class is not None
433 and isinstance(self.composite_class, type)
434 and self.composite_class not in _composite_getters
435 ):
436 if self._generated_composite_accessor is not None:
437 _composite_getters[self.composite_class] = (
438 self._generated_composite_accessor
439 )
440 elif hasattr(self.composite_class, "__composite_values__"):
441 _composite_getters[self.composite_class] = (
442 lambda obj: obj.__composite_values__()
443 )
444
445 @util.preload_module("sqlalchemy.orm.properties")
446 @util.preload_module("sqlalchemy.orm.decl_base")
447 def _setup_for_dataclass(
448 self,
449 registry: _RegistryType,
450 cls: Type[Any],
451 originating_module: Optional[str],
452 key: str,
453 ) -> None:
454 MappedColumn = util.preloaded.orm_properties.MappedColumn
455
456 decl_base = util.preloaded.orm_decl_base
457
458 insp = inspect.signature(self.composite_class)
459 for param, attr in itertools.zip_longest(
460 insp.parameters.values(), self.attrs
461 ):
462 if param is None:
463 raise sa_exc.ArgumentError(
464 f"number of composite attributes "
465 f"{len(self.attrs)} exceeds "
466 f"that of the number of attributes in class "
467 f"{self.composite_class.__name__} {len(insp.parameters)}"
468 )
469 if attr is None:
470 # fill in missing attr spots with empty MappedColumn
471 attr = MappedColumn()
472 self.attrs += (attr,)
473
474 if isinstance(attr, MappedColumn):
475 attr.declarative_scan_for_composite(
476 registry,
477 cls,
478 originating_module,
479 key,
480 param.name,
481 param.annotation,
482 )
483 elif isinstance(attr, schema.Column):
484 decl_base._undefer_column_name(param.name, attr)
485
486 @util.memoized_property
487 def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]:
488 return [getattr(self.parent.class_, prop.key) for prop in self.props]
489
490 @util.memoized_property
491 @util.preload_module("orm.properties")
492 def props(self) -> Sequence[MapperProperty[Any]]:
493 props = []
494 MappedColumn = util.preloaded.orm_properties.MappedColumn
495
496 for attr in self.attrs:
497 if isinstance(attr, str):
498 prop = self.parent.get_property(attr, _configure_mappers=False)
499 elif isinstance(attr, schema.Column):
500 prop = self.parent._columntoproperty[attr]
501 elif isinstance(attr, MappedColumn):
502 prop = self.parent._columntoproperty[attr.column]
503 elif isinstance(attr, attributes.InstrumentedAttribute):
504 prop = attr.property
505 else:
506 prop = None
507
508 if not isinstance(prop, MapperProperty):
509 raise sa_exc.ArgumentError(
510 "Composite expects Column objects or mapped "
511 f"attributes/attribute names as arguments, got: {attr!r}"
512 )
513
514 props.append(prop)
515 return props
516
517 def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
518 return self._comparable_elements
519
520 @util.non_memoized_property
521 @util.preload_module("orm.properties")
522 def columns(self) -> Sequence[Column[Any]]:
523 MappedColumn = util.preloaded.orm_properties.MappedColumn
524 return [
525 a.column if isinstance(a, MappedColumn) else a
526 for a in self.attrs
527 if isinstance(a, (schema.Column, MappedColumn))
528 ]
529
530 @property
531 def mapper_property_to_assign(self) -> Optional[MapperProperty[_CC]]:
532 return self
533
534 @property
535 def columns_to_assign(self) -> List[Tuple[schema.Column[Any], int]]:
536 return [(c, 0) for c in self.columns if c.table is None]
537
538 @util.preload_module("orm.properties")
539 def _setup_arguments_on_columns(self) -> None:
540 """Propagate configuration arguments made on this composite
541 to the target columns, for those that apply.
542
543 """
544 ColumnProperty = util.preloaded.orm_properties.ColumnProperty
545
546 for prop in self.props:
547 if not isinstance(prop, ColumnProperty):
548 continue
549 else:
550 cprop = prop
551
552 cprop.active_history = self.active_history
553 if self.deferred:
554 cprop.deferred = self.deferred
555 cprop.strategy_key = (("deferred", True), ("instrument", True))
556 cprop.group = self.group
557
558 def _setup_event_handlers(self) -> None:
559 """Establish events that populate/expire the composite attribute."""
560
561 def load_handler(
562 state: InstanceState[Any], context: _ORMCompileState
563 ) -> None:
564 _load_refresh_handler(state, context, None, is_refresh=False)
565
566 def refresh_handler(
567 state: InstanceState[Any],
568 context: _ORMCompileState,
569 to_load: Optional[Sequence[str]],
570 ) -> None:
571 # note this corresponds to sqlalchemy.ext.mutable load_attrs()
572
573 if not to_load or (
574 {self.key}.union(self._attribute_keys)
575 ).intersection(to_load):
576 _load_refresh_handler(state, context, to_load, is_refresh=True)
577
578 def _load_refresh_handler(
579 state: InstanceState[Any],
580 context: _ORMCompileState,
581 to_load: Optional[Sequence[str]],
582 is_refresh: bool,
583 ) -> None:
584 dict_ = state.dict
585
586 # if context indicates we are coming from the
587 # fget() handler, this already set the value; skip the
588 # handler here. (other handlers like mutablecomposite will still
589 # want to catch it)
590 # there's an insufficiency here in that the fget() handler
591 # really should not be using the refresh event and there should
592 # be some other event that mutablecomposite can subscribe
593 # towards for this.
594
595 if (
596 not is_refresh or context is self._COMPOSITE_FGET
597 ) and self.key in dict_:
598 return
599
600 # if column elements aren't loaded, skip.
601 # __get__() will initiate a load for those
602 # columns
603 for k in self._attribute_keys:
604 if k not in dict_:
605 return
606
607 dict_[self.key] = self.composite_class(
608 *[state.dict[key] for key in self._attribute_keys]
609 )
610
611 def expire_handler(
612 state: InstanceState[Any], keys: Optional[Sequence[str]]
613 ) -> None:
614 if keys is None or set(self._attribute_keys).intersection(keys):
615 state.dict.pop(self.key, None)
616
617 def insert_update_handler(
618 mapper: Mapper[Any],
619 connection: Connection,
620 state: InstanceState[Any],
621 ) -> None:
622 """After an insert or update, some columns may be expired due
623 to server side defaults, or re-populated due to client side
624 defaults. Pop out the composite value here so that it
625 recreates.
626
627 """
628
629 state.dict.pop(self.key, None)
630
631 event.listen(
632 self.parent, "after_insert", insert_update_handler, raw=True
633 )
634 event.listen(
635 self.parent, "after_update", insert_update_handler, raw=True
636 )
637 event.listen(
638 self.parent, "load", load_handler, raw=True, propagate=True
639 )
640 event.listen(
641 self.parent, "refresh", refresh_handler, raw=True, propagate=True
642 )
643 event.listen(
644 self.parent, "expire", expire_handler, raw=True, propagate=True
645 )
646
647 proxy_attr = self.parent.class_manager[self.key]
648 proxy_attr.impl.dispatch = proxy_attr.dispatch # type: ignore
649 proxy_attr.impl.dispatch._active_history = self.active_history
650
651 # TODO: need a deserialize hook here
652
653 @util.memoized_property
654 def _attribute_keys(self) -> Sequence[str]:
655 return [prop.key for prop in self.props]
656
657 def _populate_composite_bulk_save_mappings_fn(
658 self,
659 ) -> Callable[[Dict[str, Any]], None]:
660 if self._generated_composite_accessor:
661 get_values = self._generated_composite_accessor
662 else:
663
664 def get_values(val: Any) -> Tuple[Any]:
665 return val.__composite_values__() # type: ignore
666
667 attrs = [prop.key for prop in self.props]
668
669 def populate(dest_dict: Dict[str, Any]) -> None:
670 dest_dict.update(
671 {
672 key: val
673 for key, val in zip(
674 attrs, get_values(dest_dict.pop(self.key))
675 )
676 }
677 )
678
679 return populate
680
681 def get_history(
682 self,
683 state: InstanceState[Any],
684 dict_: _InstanceDict,
685 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
686 ) -> History:
687 """Provided for userland code that uses attributes.get_history()."""
688
689 added: List[Any] = []
690 deleted: List[Any] = []
691
692 has_history = False
693 for prop in self.props:
694 key = prop.key
695 hist = state.manager[key].impl.get_history(state, dict_)
696 if hist.has_changes():
697 has_history = True
698
699 non_deleted = hist.non_deleted()
700 if non_deleted:
701 added.extend(non_deleted)
702 else:
703 added.append(None)
704 if hist.deleted:
705 deleted.extend(hist.deleted)
706 else:
707 deleted.append(None)
708
709 if has_history:
710 return attributes.History(
711 [self.composite_class(*added)],
712 (),
713 [self.composite_class(*deleted)],
714 )
715 else:
716 return attributes.History((), [self.composite_class(*added)], ())
717
718 def _comparator_factory(
719 self, mapper: Mapper[Any]
720 ) -> Composite.Comparator[_CC]:
721 return self.comparator_factory(self, mapper)
722
723 class CompositeBundle(orm_util.Bundle[_T]):
724 def __init__(
725 self,
726 property_: Composite[_T],
727 expr: ClauseList,
728 ):
729 self.property = property_
730 super().__init__(property_.key, *expr)
731
732 def create_row_processor(
733 self,
734 query: Select[Unpack[TupleAny]],
735 procs: Sequence[Callable[[Row[Unpack[TupleAny]]], Any]],
736 labels: Sequence[str],
737 ) -> Callable[[Row[Unpack[TupleAny]]], Any]:
738 def proc(row: Row[Unpack[TupleAny]]) -> Any:
739 return self.property.composite_class(
740 *[proc(row) for proc in procs]
741 )
742
743 return proc
744
745 class Comparator(PropComparator[_PT]):
746 """Produce boolean, comparison, and other operators for
747 :class:`.Composite` attributes.
748
749 See the example in :ref:`composite_operations` for an overview
750 of usage , as well as the documentation for :class:`.PropComparator`.
751
752 .. seealso::
753
754 :class:`.PropComparator`
755
756 :class:`.ColumnOperators`
757
758 :ref:`types_operators`
759
760 :attr:`.TypeEngine.comparator_factory`
761
762 """
763
764 # https://github.com/python/mypy/issues/4266
765 __hash__ = None # type: ignore
766
767 prop: RODescriptorReference[Composite[_PT]]
768
769 @util.memoized_property
770 def clauses(self) -> ClauseList:
771 return expression.ClauseList(
772 group=False, *self._comparable_elements
773 )
774
775 def __clause_element__(self) -> CompositeProperty.CompositeBundle[_PT]:
776 return self.expression
777
778 @util.memoized_property
779 def expression(self) -> CompositeProperty.CompositeBundle[_PT]:
780 clauses = self.clauses._annotate(
781 {
782 "parententity": self._parententity,
783 "parentmapper": self._parententity,
784 "proxy_key": self.prop.key,
785 }
786 )
787 return CompositeProperty.CompositeBundle(self.prop, clauses)
788
789 def _bulk_update_tuples(
790 self, value: Any
791 ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
792 if isinstance(value, BindParameter):
793 value = value.value
794
795 values: Sequence[Any]
796
797 if value is None:
798 values = [None for key in self.prop._attribute_keys]
799 elif isinstance(self.prop.composite_class, type) and isinstance(
800 value, self.prop.composite_class
801 ):
802 values = self.prop._composite_values_from_instance(
803 value # type: ignore[arg-type]
804 )
805 else:
806 raise sa_exc.ArgumentError(
807 "Can't UPDATE composite attribute %s to %r"
808 % (self.prop, value)
809 )
810
811 return list(zip(self._comparable_elements, values))
812
813 @util.memoized_property
814 def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]:
815 if self._adapt_to_entity:
816 return [
817 getattr(self._adapt_to_entity.entity, prop.key)
818 for prop in self.prop._comparable_elements
819 ]
820 else:
821 return self.prop._comparable_elements
822
823 def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
824 return self._compare(operators.eq, other)
825
826 def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
827 return self._compare(operators.ne, other)
828
829 def __lt__(self, other: Any) -> ColumnElement[bool]:
830 return self._compare(operators.lt, other)
831
832 def __gt__(self, other: Any) -> ColumnElement[bool]:
833 return self._compare(operators.gt, other)
834
835 def __le__(self, other: Any) -> ColumnElement[bool]:
836 return self._compare(operators.le, other)
837
838 def __ge__(self, other: Any) -> ColumnElement[bool]:
839 return self._compare(operators.ge, other)
840
841 # what might be interesting would be if we create
842 # an instance of the composite class itself with
843 # the columns as data members, then use "hybrid style" comparison
844 # to create these comparisons. then your Point.__eq__() method could
845 # be where comparison behavior is defined for SQL also. Likely
846 # not a good choice for default behavior though, not clear how it would
847 # work w/ dataclasses, etc. also no demand for any of this anyway.
848 def _compare(
849 self, operator: OperatorType, other: Any
850 ) -> ColumnElement[bool]:
851 values: Sequence[Any]
852 if other is None:
853 values = [None] * len(self.prop._comparable_elements)
854 else:
855 values = self.prop._composite_values_from_instance(other)
856 comparisons = [
857 operator(a, b)
858 for a, b in zip(self.prop._comparable_elements, values)
859 ]
860 if self._adapt_to_entity:
861 assert self.adapter is not None
862 comparisons = [self.adapter(x) for x in comparisons]
863 return sql.and_(*comparisons)
864
865 def __str__(self) -> str:
866 return str(self.parent.class_.__name__) + "." + self.key
867
868
869class Composite(CompositeProperty[_T], _DeclarativeMapped[_T]):
870 """Declarative-compatible front-end for the :class:`.CompositeProperty`
871 class.
872
873 Public constructor is the :func:`_orm.composite` function.
874
875 .. versionchanged:: 2.0 Added :class:`_orm.Composite` as a Declarative
876 compatible subclass of :class:`_orm.CompositeProperty`.
877
878 .. seealso::
879
880 :ref:`mapper_composite`
881
882 """
883
884 inherit_cache = True
885 """:meta private:"""
886
887
888class ConcreteInheritedProperty(DescriptorProperty[_T]):
889 """A 'do nothing' :class:`.MapperProperty` that disables
890 an attribute on a concrete subclass that is only present
891 on the inherited mapper, not the concrete classes' mapper.
892
893 Cases where this occurs include:
894
895 * When the superclass mapper is mapped against a
896 "polymorphic union", which includes all attributes from
897 all subclasses.
898 * When a relationship() is configured on an inherited mapper,
899 but not on the subclass mapper. Concrete mappers require
900 that relationship() is configured explicitly on each
901 subclass.
902
903 """
904
905 def _comparator_factory(
906 self, mapper: Mapper[Any]
907 ) -> Type[PropComparator[_T]]:
908 comparator_callable = None
909
910 for m in self.parent.iterate_to_root():
911 p = m._props[self.key]
912 if getattr(p, "comparator_factory", None) is not None:
913 comparator_callable = p.comparator_factory
914 break
915 assert comparator_callable is not None
916 return comparator_callable(p, mapper) # type: ignore
917
918 def __init__(self) -> None:
919 super().__init__()
920
921 def warn() -> NoReturn:
922 raise AttributeError(
923 "Concrete %s does not implement "
924 "attribute %r at the instance level. Add "
925 "this property explicitly to %s."
926 % (self.parent, self.key, self.parent)
927 )
928
929 class NoninheritedConcreteProp:
930 def __set__(s: Any, obj: Any, value: Any) -> NoReturn:
931 warn()
932
933 def __delete__(s: Any, obj: Any) -> NoReturn:
934 warn()
935
936 def __get__(s: Any, obj: Any, owner: Any) -> Any:
937 if obj is None:
938 return self.descriptor
939 warn()
940
941 self.descriptor = NoninheritedConcreteProp()
942
943
944class SynonymProperty(DescriptorProperty[_T]):
945 """Denote an attribute name as a synonym to a mapped property,
946 in that the attribute will mirror the value and expression behavior
947 of another attribute.
948
949 :class:`.Synonym` is constructed using the :func:`_orm.synonym`
950 function.
951
952 .. seealso::
953
954 :ref:`synonyms` - Overview of synonyms
955
956 """
957
958 comparator_factory: Optional[Type[PropComparator[_T]]]
959
960 def __init__(
961 self,
962 name: str,
963 map_column: Optional[bool] = None,
964 descriptor: Optional[Any] = None,
965 comparator_factory: Optional[Type[PropComparator[_T]]] = None,
966 attribute_options: Optional[_AttributeOptions] = None,
967 info: Optional[_InfoType] = None,
968 doc: Optional[str] = None,
969 ):
970 super().__init__(attribute_options=attribute_options)
971
972 self.name = name
973 self.map_column = map_column
974 self.descriptor = descriptor
975 self.comparator_factory = comparator_factory
976 if doc:
977 self.doc = doc
978 elif descriptor and descriptor.__doc__:
979 self.doc = descriptor.__doc__
980 else:
981 self.doc = None
982 if info:
983 self.info.update(info)
984
985 util.set_creation_order(self)
986
987 if not TYPE_CHECKING:
988
989 @property
990 def uses_objects(self) -> bool:
991 return getattr(self.parent.class_, self.name).impl.uses_objects
992
993 # TODO: when initialized, check _proxied_object,
994 # emit a warning if its not a column-based property
995
996 @util.memoized_property
997 def _proxied_object(
998 self,
999 ) -> Union[MapperProperty[_T], SQLORMOperations[_T]]:
1000 attr = getattr(self.parent.class_, self.name)
1001 if not hasattr(attr, "property") or not isinstance(
1002 attr.property, MapperProperty
1003 ):
1004 # attribute is a non-MapperProprerty proxy such as
1005 # hybrid or association proxy
1006 if isinstance(attr, attributes.QueryableAttribute):
1007 return attr.comparator
1008 elif isinstance(attr, SQLORMOperations):
1009 # assocaition proxy comes here
1010 return attr
1011
1012 raise sa_exc.InvalidRequestError(
1013 """synonym() attribute "%s.%s" only supports """
1014 """ORM mapped attributes, got %r"""
1015 % (self.parent.class_.__name__, self.name, attr)
1016 )
1017 return attr.property
1018
1019 def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
1020 return (getattr(self.parent.class_, self.name),)
1021
1022 def _comparator_factory(self, mapper: Mapper[Any]) -> SQLORMOperations[_T]:
1023 prop = self._proxied_object
1024
1025 if isinstance(prop, MapperProperty):
1026 if self.comparator_factory:
1027 comp = self.comparator_factory(prop, mapper)
1028 else:
1029 comp = prop.comparator_factory(prop, mapper)
1030 return comp
1031 else:
1032 return prop
1033
1034 def get_history(
1035 self,
1036 state: InstanceState[Any],
1037 dict_: _InstanceDict,
1038 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
1039 ) -> History:
1040 attr: QueryableAttribute[Any] = getattr(self.parent.class_, self.name)
1041 return attr.impl.get_history(state, dict_, passive=passive)
1042
1043 def _get_dataclass_setup_options(
1044 self,
1045 decl_scan: _ClassScanMapperConfig,
1046 key: str,
1047 dataclass_setup_arguments: _DataclassArguments,
1048 ) -> _AttributeOptions:
1049 dataclasses_default = self._attribute_options.dataclasses_default
1050 if (
1051 dataclasses_default is not _NoArg.NO_ARG
1052 and not callable(dataclasses_default)
1053 and not getattr(
1054 decl_scan.cls, "_sa_disable_descriptor_defaults", False
1055 )
1056 ):
1057 proxied = decl_scan.collected_attributes[self.name]
1058 proxied_default = proxied._attribute_options.dataclasses_default
1059 if proxied_default != dataclasses_default:
1060 raise sa_exc.ArgumentError(
1061 f"Synonym {key!r} default argument "
1062 f"{dataclasses_default!r} must match the dataclasses "
1063 f"default value of proxied object {self.name!r}, "
1064 f"""currently {
1065 repr(proxied_default)
1066 if proxied_default is not _NoArg.NO_ARG
1067 else 'not set'}"""
1068 )
1069 self._default_scalar_value = dataclasses_default
1070 return self._attribute_options._replace(
1071 dataclasses_default=DONT_SET
1072 )
1073
1074 return self._attribute_options
1075
1076 @util.preload_module("sqlalchemy.orm.properties")
1077 def set_parent(self, parent: Mapper[Any], init: bool) -> None:
1078 properties = util.preloaded.orm_properties
1079
1080 if self.map_column:
1081 # implement the 'map_column' option.
1082 if self.key not in parent.persist_selectable.c:
1083 raise sa_exc.ArgumentError(
1084 "Can't compile synonym '%s': no column on table "
1085 "'%s' named '%s'"
1086 % (
1087 self.name,
1088 parent.persist_selectable.description,
1089 self.key,
1090 )
1091 )
1092 elif (
1093 parent.persist_selectable.c[self.key]
1094 in parent._columntoproperty
1095 and parent._columntoproperty[
1096 parent.persist_selectable.c[self.key]
1097 ].key
1098 == self.name
1099 ):
1100 raise sa_exc.ArgumentError(
1101 "Can't call map_column=True for synonym %r=%r, "
1102 "a ColumnProperty already exists keyed to the name "
1103 "%r for column %r"
1104 % (self.key, self.name, self.name, self.key)
1105 )
1106 p: ColumnProperty[Any] = properties.ColumnProperty(
1107 parent.persist_selectable.c[self.key]
1108 )
1109 parent._configure_property(self.name, p, init=init, setparent=True)
1110 p._mapped_by_synonym = self.key
1111
1112 self.parent = parent
1113
1114
1115class Synonym(SynonymProperty[_T], _DeclarativeMapped[_T]):
1116 """Declarative front-end for the :class:`.SynonymProperty` class.
1117
1118 Public constructor is the :func:`_orm.synonym` function.
1119
1120 .. versionchanged:: 2.0 Added :class:`_orm.Synonym` as a Declarative
1121 compatible subclass for :class:`_orm.SynonymProperty`
1122
1123 .. seealso::
1124
1125 :ref:`synonyms` - Overview of synonyms
1126
1127 """
1128
1129 inherit_cache = True
1130 """:meta private:"""