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(
788 value # type: ignore[arg-type]
789 )
790 else:
791 raise sa_exc.ArgumentError(
792 "Can't UPDATE composite attribute %s to %r"
793 % (self.prop, value)
794 )
795
796 return list(zip(self._comparable_elements, values))
797
798 @util.memoized_property
799 def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]:
800 if self._adapt_to_entity:
801 return [
802 getattr(self._adapt_to_entity.entity, prop.key)
803 for prop in self.prop._comparable_elements
804 ]
805 else:
806 return self.prop._comparable_elements
807
808 def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
809 return self._compare(operators.eq, other)
810
811 def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
812 return self._compare(operators.ne, other)
813
814 def __lt__(self, other: Any) -> ColumnElement[bool]:
815 return self._compare(operators.lt, other)
816
817 def __gt__(self, other: Any) -> ColumnElement[bool]:
818 return self._compare(operators.gt, other)
819
820 def __le__(self, other: Any) -> ColumnElement[bool]:
821 return self._compare(operators.le, other)
822
823 def __ge__(self, other: Any) -> ColumnElement[bool]:
824 return self._compare(operators.ge, other)
825
826 # what might be interesting would be if we create
827 # an instance of the composite class itself with
828 # the columns as data members, then use "hybrid style" comparison
829 # to create these comparisons. then your Point.__eq__() method could
830 # be where comparison behavior is defined for SQL also. Likely
831 # not a good choice for default behavior though, not clear how it would
832 # work w/ dataclasses, etc. also no demand for any of this anyway.
833 def _compare(
834 self, operator: OperatorType, other: Any
835 ) -> ColumnElement[bool]:
836 values: Sequence[Any]
837 if other is None:
838 values = [None] * len(self.prop._comparable_elements)
839 else:
840 values = self.prop._composite_values_from_instance(other)
841 comparisons = [
842 operator(a, b)
843 for a, b in zip(self.prop._comparable_elements, values)
844 ]
845 if self._adapt_to_entity:
846 assert self.adapter is not None
847 comparisons = [self.adapter(x) for x in comparisons]
848 return sql.and_(*comparisons)
849
850 def __str__(self) -> str:
851 return str(self.parent.class_.__name__) + "." + self.key
852
853
854class Composite(CompositeProperty[_T], _DeclarativeMapped[_T]):
855 """Declarative-compatible front-end for the :class:`.CompositeProperty`
856 class.
857
858 Public constructor is the :func:`_orm.composite` function.
859
860 .. versionchanged:: 2.0 Added :class:`_orm.Composite` as a Declarative
861 compatible subclass of :class:`_orm.CompositeProperty`.
862
863 .. seealso::
864
865 :ref:`mapper_composite`
866
867 """
868
869 inherit_cache = True
870 """:meta private:"""
871
872
873class ConcreteInheritedProperty(DescriptorProperty[_T]):
874 """A 'do nothing' :class:`.MapperProperty` that disables
875 an attribute on a concrete subclass that is only present
876 on the inherited mapper, not the concrete classes' mapper.
877
878 Cases where this occurs include:
879
880 * When the superclass mapper is mapped against a
881 "polymorphic union", which includes all attributes from
882 all subclasses.
883 * When a relationship() is configured on an inherited mapper,
884 but not on the subclass mapper. Concrete mappers require
885 that relationship() is configured explicitly on each
886 subclass.
887
888 """
889
890 def _comparator_factory(
891 self, mapper: Mapper[Any]
892 ) -> Type[PropComparator[_T]]:
893 comparator_callable = None
894
895 for m in self.parent.iterate_to_root():
896 p = m._props[self.key]
897 if getattr(p, "comparator_factory", None) is not None:
898 comparator_callable = p.comparator_factory
899 break
900 assert comparator_callable is not None
901 return comparator_callable(p, mapper) # type: ignore
902
903 def __init__(self) -> None:
904 super().__init__()
905
906 def warn() -> NoReturn:
907 raise AttributeError(
908 "Concrete %s does not implement "
909 "attribute %r at the instance level. Add "
910 "this property explicitly to %s."
911 % (self.parent, self.key, self.parent)
912 )
913
914 class NoninheritedConcreteProp:
915 def __set__(s: Any, obj: Any, value: Any) -> NoReturn:
916 warn()
917
918 def __delete__(s: Any, obj: Any) -> NoReturn:
919 warn()
920
921 def __get__(s: Any, obj: Any, owner: Any) -> Any:
922 if obj is None:
923 return self.descriptor
924 warn()
925
926 self.descriptor = NoninheritedConcreteProp()
927
928
929class SynonymProperty(DescriptorProperty[_T]):
930 """Denote an attribute name as a synonym to a mapped property,
931 in that the attribute will mirror the value and expression behavior
932 of another attribute.
933
934 :class:`.Synonym` is constructed using the :func:`_orm.synonym`
935 function.
936
937 .. seealso::
938
939 :ref:`synonyms` - Overview of synonyms
940
941 """
942
943 comparator_factory: Optional[Type[PropComparator[_T]]]
944
945 def __init__(
946 self,
947 name: str,
948 map_column: Optional[bool] = None,
949 descriptor: Optional[Any] = None,
950 comparator_factory: Optional[Type[PropComparator[_T]]] = None,
951 attribute_options: Optional[_AttributeOptions] = None,
952 info: Optional[_InfoType] = None,
953 doc: Optional[str] = None,
954 ):
955 super().__init__(attribute_options=attribute_options)
956
957 self.name = name
958 self.map_column = map_column
959 self.descriptor = descriptor
960 self.comparator_factory = comparator_factory
961 if doc:
962 self.doc = doc
963 elif descriptor and descriptor.__doc__:
964 self.doc = descriptor.__doc__
965 else:
966 self.doc = None
967 if info:
968 self.info.update(info)
969
970 util.set_creation_order(self)
971
972 if not TYPE_CHECKING:
973
974 @property
975 def uses_objects(self) -> bool:
976 return getattr(self.parent.class_, self.name).impl.uses_objects
977
978 # TODO: when initialized, check _proxied_object,
979 # emit a warning if its not a column-based property
980
981 @util.memoized_property
982 def _proxied_object(
983 self,
984 ) -> Union[MapperProperty[_T], SQLORMOperations[_T]]:
985 attr = getattr(self.parent.class_, self.name)
986 if not hasattr(attr, "property") or not isinstance(
987 attr.property, MapperProperty
988 ):
989 # attribute is a non-MapperProprerty proxy such as
990 # hybrid or association proxy
991 if isinstance(attr, attributes.QueryableAttribute):
992 return attr.comparator
993 elif isinstance(attr, SQLORMOperations):
994 # assocaition proxy comes here
995 return attr
996
997 raise sa_exc.InvalidRequestError(
998 """synonym() attribute "%s.%s" only supports """
999 """ORM mapped attributes, got %r"""
1000 % (self.parent.class_.__name__, self.name, attr)
1001 )
1002 return attr.property
1003
1004 def _comparator_factory(self, mapper: Mapper[Any]) -> SQLORMOperations[_T]:
1005 prop = self._proxied_object
1006
1007 if isinstance(prop, MapperProperty):
1008 if self.comparator_factory:
1009 comp = self.comparator_factory(prop, mapper)
1010 else:
1011 comp = prop.comparator_factory(prop, mapper)
1012 return comp
1013 else:
1014 return prop
1015
1016 def get_history(
1017 self,
1018 state: InstanceState[Any],
1019 dict_: _InstanceDict,
1020 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
1021 ) -> History:
1022 attr: QueryableAttribute[Any] = getattr(self.parent.class_, self.name)
1023 return attr.impl.get_history(state, dict_, passive=passive)
1024
1025 @util.preload_module("sqlalchemy.orm.properties")
1026 def set_parent(self, parent: Mapper[Any], init: bool) -> None:
1027 properties = util.preloaded.orm_properties
1028
1029 if self.map_column:
1030 # implement the 'map_column' option.
1031 if self.key not in parent.persist_selectable.c:
1032 raise sa_exc.ArgumentError(
1033 "Can't compile synonym '%s': no column on table "
1034 "'%s' named '%s'"
1035 % (
1036 self.name,
1037 parent.persist_selectable.description,
1038 self.key,
1039 )
1040 )
1041 elif (
1042 parent.persist_selectable.c[self.key]
1043 in parent._columntoproperty
1044 and parent._columntoproperty[
1045 parent.persist_selectable.c[self.key]
1046 ].key
1047 == self.name
1048 ):
1049 raise sa_exc.ArgumentError(
1050 "Can't call map_column=True for synonym %r=%r, "
1051 "a ColumnProperty already exists keyed to the name "
1052 "%r for column %r"
1053 % (self.key, self.name, self.name, self.key)
1054 )
1055 p: ColumnProperty[Any] = properties.ColumnProperty(
1056 parent.persist_selectable.c[self.key]
1057 )
1058 parent._configure_property(self.name, p, init=init, setparent=True)
1059 p._mapped_by_synonym = self.key
1060
1061 self.parent = parent
1062
1063
1064class Synonym(SynonymProperty[_T], _DeclarativeMapped[_T]):
1065 """Declarative front-end for the :class:`.SynonymProperty` class.
1066
1067 Public constructor is the :func:`_orm.synonym` function.
1068
1069 .. versionchanged:: 2.0 Added :class:`_orm.Synonym` as a Declarative
1070 compatible subclass for :class:`_orm.SynonymProperty`
1071
1072 .. seealso::
1073
1074 :ref:`synonyms` - Overview of synonyms
1075
1076 """
1077
1078 inherit_cache = True
1079 """:meta private:"""