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