1# orm/attributes.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# mypy: allow-untyped-defs, allow-untyped-calls
8
9"""Defines instrumentation for class attributes and their interaction
10with instances.
11
12This module is usually not directly visible to user applications, but
13defines a large part of the ORM's interactivity.
14
15
16"""
17
18from __future__ import annotations
19
20import dataclasses
21import operator
22from typing import Any
23from typing import Callable
24from typing import cast
25from typing import ClassVar
26from typing import Dict
27from typing import Iterable
28from typing import List
29from typing import NamedTuple
30from typing import Optional
31from typing import overload
32from typing import Sequence
33from typing import Tuple
34from typing import Type
35from typing import TYPE_CHECKING
36from typing import TypeVar
37from typing import Union
38
39from . import collections
40from . import exc as orm_exc
41from . import interfaces
42from ._typing import insp_is_aliased_class
43from .base import _DeclarativeMapped
44from .base import ATTR_EMPTY
45from .base import ATTR_WAS_SET
46from .base import CALLABLES_OK
47from .base import DEFERRED_HISTORY_LOAD
48from .base import INCLUDE_PENDING_MUTATIONS # noqa
49from .base import INIT_OK
50from .base import instance_dict as instance_dict
51from .base import instance_state as instance_state
52from .base import instance_str
53from .base import LOAD_AGAINST_COMMITTED
54from .base import LoaderCallableStatus
55from .base import manager_of_class as manager_of_class
56from .base import Mapped as Mapped # noqa
57from .base import NEVER_SET # noqa
58from .base import NO_AUTOFLUSH
59from .base import NO_CHANGE # noqa
60from .base import NO_KEY
61from .base import NO_RAISE
62from .base import NO_VALUE
63from .base import NON_PERSISTENT_OK # noqa
64from .base import opt_manager_of_class as opt_manager_of_class
65from .base import PASSIVE_CLASS_MISMATCH # noqa
66from .base import PASSIVE_NO_FETCH
67from .base import PASSIVE_NO_FETCH_RELATED # noqa
68from .base import PASSIVE_NO_INITIALIZE
69from .base import PASSIVE_NO_RESULT
70from .base import PASSIVE_OFF
71from .base import PASSIVE_ONLY_PERSISTENT
72from .base import PASSIVE_RETURN_NO_VALUE
73from .base import PassiveFlag
74from .base import RELATED_OBJECT_OK # noqa
75from .base import SQL_OK # noqa
76from .base import SQLORMExpression
77from .base import state_str
78from .. import event
79from .. import exc
80from .. import inspection
81from .. import util
82from ..event import dispatcher
83from ..event import EventTarget
84from ..sql import base as sql_base
85from ..sql import cache_key
86from ..sql import coercions
87from ..sql import roles
88from ..sql import visitors
89from ..sql.cache_key import HasCacheKey
90from ..sql.visitors import _TraverseInternalsType
91from ..sql.visitors import InternalTraversal
92from ..util.typing import Literal
93from ..util.typing import Self
94from ..util.typing import TypeGuard
95
96if TYPE_CHECKING:
97 from ._typing import _EntityType
98 from ._typing import _ExternalEntityType
99 from ._typing import _InstanceDict
100 from ._typing import _InternalEntityType
101 from ._typing import _LoaderCallable
102 from ._typing import _O
103 from .collections import _AdaptedCollectionProtocol
104 from .collections import CollectionAdapter
105 from .interfaces import MapperProperty
106 from .relationships import RelationshipProperty
107 from .state import InstanceState
108 from .util import AliasedInsp
109 from .writeonly import WriteOnlyAttributeImpl
110 from ..event.base import _Dispatch
111 from ..sql._typing import _ColumnExpressionArgument
112 from ..sql._typing import _DMLColumnArgument
113 from ..sql._typing import _InfoType
114 from ..sql._typing import _PropagateAttrsType
115 from ..sql.annotation import _AnnotationDict
116 from ..sql.elements import ColumnElement
117 from ..sql.elements import Label
118 from ..sql.operators import OperatorType
119 from ..sql.selectable import FromClause
120
121
122_T = TypeVar("_T")
123_T_co = TypeVar("_T_co", bound=Any, covariant=True)
124
125
126_AllPendingType = Sequence[
127 Tuple[Optional["InstanceState[Any]"], Optional[object]]
128]
129
130
131_UNKNOWN_ATTR_KEY = object()
132
133
134@inspection._self_inspects
135class QueryableAttribute(
136 _DeclarativeMapped[_T_co],
137 SQLORMExpression[_T_co],
138 interfaces.InspectionAttr,
139 interfaces.PropComparator[_T_co],
140 roles.JoinTargetRole,
141 roles.OnClauseRole,
142 sql_base.Immutable,
143 cache_key.SlotsMemoizedHasCacheKey,
144 util.MemoizedSlots,
145 EventTarget,
146):
147 """Base class for :term:`descriptor` objects that intercept
148 attribute events on behalf of a :class:`.MapperProperty`
149 object. The actual :class:`.MapperProperty` is accessible
150 via the :attr:`.QueryableAttribute.property`
151 attribute.
152
153
154 .. seealso::
155
156 :class:`.InstrumentedAttribute`
157
158 :class:`.MapperProperty`
159
160 :attr:`_orm.Mapper.all_orm_descriptors`
161
162 :attr:`_orm.Mapper.attrs`
163 """
164
165 __slots__ = (
166 "class_",
167 "key",
168 "impl",
169 "comparator",
170 "property",
171 "parent",
172 "expression",
173 "_of_type",
174 "_extra_criteria",
175 "_slots_dispatch",
176 "_propagate_attrs",
177 "_doc",
178 )
179
180 is_attribute = True
181
182 dispatch: dispatcher[QueryableAttribute[_T_co]]
183
184 class_: _ExternalEntityType[Any]
185 key: str
186 parententity: _InternalEntityType[Any]
187 impl: AttributeImpl
188 comparator: interfaces.PropComparator[_T_co]
189 _of_type: Optional[_InternalEntityType[Any]]
190 _extra_criteria: Tuple[ColumnElement[bool], ...]
191 _doc: Optional[str]
192
193 # PropComparator has a __visit_name__ to participate within
194 # traversals. Disambiguate the attribute vs. a comparator.
195 __visit_name__ = "orm_instrumented_attribute"
196
197 def __init__(
198 self,
199 class_: _ExternalEntityType[_O],
200 key: str,
201 parententity: _InternalEntityType[_O],
202 comparator: interfaces.PropComparator[_T_co],
203 impl: Optional[AttributeImpl] = None,
204 of_type: Optional[_InternalEntityType[Any]] = None,
205 extra_criteria: Tuple[ColumnElement[bool], ...] = (),
206 ):
207 self.class_ = class_
208 self.key = key
209
210 self._parententity = self.parent = parententity
211
212 # this attribute is non-None after mappers are set up, however in the
213 # interim class manager setup, there's a check for None to see if it
214 # needs to be populated, so we assign None here leaving the attribute
215 # in a temporarily not-type-correct state
216 self.impl = impl # type: ignore
217
218 assert comparator is not None
219 self.comparator = comparator
220 self._of_type = of_type
221 self._extra_criteria = extra_criteria
222 self._doc = None
223
224 manager = opt_manager_of_class(class_)
225 # manager is None in the case of AliasedClass
226 if manager:
227 # propagate existing event listeners from
228 # immediate superclass
229 for base in manager._bases:
230 if key in base:
231 self.dispatch._update(base[key].dispatch)
232 if base[key].dispatch._active_history:
233 self.dispatch._active_history = True # type: ignore
234
235 _cache_key_traversal = [
236 ("key", visitors.ExtendedInternalTraversal.dp_string),
237 ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
238 ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
239 ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
240 ]
241
242 def __reduce__(self) -> Any:
243 # this method is only used in terms of the
244 # sqlalchemy.ext.serializer extension
245 return (
246 _queryable_attribute_unreduce,
247 (
248 self.key,
249 self._parententity.mapper.class_,
250 self._parententity,
251 self._parententity.entity,
252 ),
253 )
254
255 @property
256 def _impl_uses_objects(self) -> bool:
257 return self.impl.uses_objects
258
259 def get_history(
260 self, instance: Any, passive: PassiveFlag = PASSIVE_OFF
261 ) -> History:
262 return self.impl.get_history(
263 instance_state(instance), instance_dict(instance), passive
264 )
265
266 @property
267 def info(self) -> _InfoType:
268 """Return the 'info' dictionary for the underlying SQL element.
269
270 The behavior here is as follows:
271
272 * If the attribute is a column-mapped property, i.e.
273 :class:`.ColumnProperty`, which is mapped directly
274 to a schema-level :class:`_schema.Column` object, this attribute
275 will return the :attr:`.SchemaItem.info` dictionary associated
276 with the core-level :class:`_schema.Column` object.
277
278 * If the attribute is a :class:`.ColumnProperty` but is mapped to
279 any other kind of SQL expression other than a
280 :class:`_schema.Column`,
281 the attribute will refer to the :attr:`.MapperProperty.info`
282 dictionary associated directly with the :class:`.ColumnProperty`,
283 assuming the SQL expression itself does not have its own ``.info``
284 attribute (which should be the case, unless a user-defined SQL
285 construct has defined one).
286
287 * If the attribute refers to any other kind of
288 :class:`.MapperProperty`, including :class:`.Relationship`,
289 the attribute will refer to the :attr:`.MapperProperty.info`
290 dictionary associated with that :class:`.MapperProperty`.
291
292 * To access the :attr:`.MapperProperty.info` dictionary of the
293 :class:`.MapperProperty` unconditionally, including for a
294 :class:`.ColumnProperty` that's associated directly with a
295 :class:`_schema.Column`, the attribute can be referred to using
296 :attr:`.QueryableAttribute.property` attribute, as
297 ``MyClass.someattribute.property.info``.
298
299 .. seealso::
300
301 :attr:`.SchemaItem.info`
302
303 :attr:`.MapperProperty.info`
304
305 """
306 return self.comparator.info
307
308 parent: _InternalEntityType[Any]
309 """Return an inspection instance representing the parent.
310
311 This will be either an instance of :class:`_orm.Mapper`
312 or :class:`.AliasedInsp`, depending upon the nature
313 of the parent entity which this attribute is associated
314 with.
315
316 """
317
318 expression: ColumnElement[_T_co]
319 """The SQL expression object represented by this
320 :class:`.QueryableAttribute`.
321
322 This will typically be an instance of a :class:`_sql.ColumnElement`
323 subclass representing a column expression.
324
325 """
326
327 def _memoized_attr_expression(self) -> ColumnElement[_T]:
328 annotations: _AnnotationDict
329
330 # applies only to Proxy() as used by hybrid.
331 # currently is an exception to typing rather than feeding through
332 # non-string keys.
333 # ideally Proxy() would have a separate set of methods to deal
334 # with this case.
335 entity_namespace = self._entity_namespace
336 assert isinstance(entity_namespace, HasCacheKey)
337
338 if self.key is _UNKNOWN_ATTR_KEY:
339 annotations = {"entity_namespace": entity_namespace}
340 else:
341 annotations = {
342 "proxy_key": self.key,
343 "proxy_owner": self._parententity,
344 "entity_namespace": entity_namespace,
345 }
346
347 ce = self.comparator.__clause_element__()
348 try:
349 if TYPE_CHECKING:
350 assert isinstance(ce, ColumnElement)
351 anno = ce._annotate
352 except AttributeError as ae:
353 raise exc.InvalidRequestError(
354 'When interpreting attribute "%s" as a SQL expression, '
355 "expected __clause_element__() to return "
356 "a ClauseElement object, got: %r" % (self, ce)
357 ) from ae
358 else:
359 return anno(annotations)
360
361 def _memoized_attr__propagate_attrs(self) -> _PropagateAttrsType:
362 # this suits the case in coercions where we don't actually
363 # call ``__clause_element__()`` but still need to get
364 # resolved._propagate_attrs. See #6558.
365 return util.immutabledict(
366 {
367 "compile_state_plugin": "orm",
368 "plugin_subject": self._parentmapper,
369 }
370 )
371
372 @property
373 def _entity_namespace(self) -> _InternalEntityType[Any]:
374 return self._parententity
375
376 @property
377 def _annotations(self) -> _AnnotationDict:
378 return self.__clause_element__()._annotations
379
380 def __clause_element__(self) -> ColumnElement[_T_co]:
381 return self.expression
382
383 @property
384 def _from_objects(self) -> List[FromClause]:
385 return self.expression._from_objects
386
387 def _bulk_update_tuples(
388 self, value: Any
389 ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
390 """Return setter tuples for a bulk UPDATE."""
391
392 return self.comparator._bulk_update_tuples(value)
393
394 def adapt_to_entity(self, adapt_to_entity: AliasedInsp[Any]) -> Self:
395 assert not self._of_type
396 return self.__class__(
397 adapt_to_entity.entity,
398 self.key,
399 impl=self.impl,
400 comparator=self.comparator.adapt_to_entity(adapt_to_entity),
401 parententity=adapt_to_entity,
402 )
403
404 def of_type(self, entity: _EntityType[_T]) -> QueryableAttribute[_T]:
405 return QueryableAttribute(
406 self.class_,
407 self.key,
408 self._parententity,
409 impl=self.impl,
410 comparator=self.comparator.of_type(entity),
411 of_type=inspection.inspect(entity),
412 extra_criteria=self._extra_criteria,
413 )
414
415 def and_(
416 self, *clauses: _ColumnExpressionArgument[bool]
417 ) -> QueryableAttribute[bool]:
418 if TYPE_CHECKING:
419 assert isinstance(self.comparator, RelationshipProperty.Comparator)
420
421 exprs = tuple(
422 coercions.expect(roles.WhereHavingRole, clause)
423 for clause in util.coerce_generator_arg(clauses)
424 )
425
426 return QueryableAttribute(
427 self.class_,
428 self.key,
429 self._parententity,
430 impl=self.impl,
431 comparator=self.comparator.and_(*exprs),
432 of_type=self._of_type,
433 extra_criteria=self._extra_criteria + exprs,
434 )
435
436 def _clone(self, **kw: Any) -> QueryableAttribute[_T]:
437 return QueryableAttribute(
438 self.class_,
439 self.key,
440 self._parententity,
441 impl=self.impl,
442 comparator=self.comparator,
443 of_type=self._of_type,
444 extra_criteria=self._extra_criteria,
445 )
446
447 def label(self, name: Optional[str]) -> Label[_T_co]:
448 return self.__clause_element__().label(name)
449
450 def operate(
451 self, op: OperatorType, *other: Any, **kwargs: Any
452 ) -> ColumnElement[Any]:
453 return op(self.comparator, *other, **kwargs) # type: ignore[no-any-return] # noqa: E501
454
455 def reverse_operate(
456 self, op: OperatorType, other: Any, **kwargs: Any
457 ) -> ColumnElement[Any]:
458 return op(other, self.comparator, **kwargs) # type: ignore[no-any-return] # noqa: E501
459
460 def hasparent(
461 self, state: InstanceState[Any], optimistic: bool = False
462 ) -> bool:
463 return self.impl.hasparent(state, optimistic=optimistic) is not False
464
465 def __getattr__(self, key: str) -> Any:
466 try:
467 return util.MemoizedSlots.__getattr__(self, key)
468 except AttributeError:
469 pass
470
471 try:
472 return getattr(self.comparator, key)
473 except AttributeError as err:
474 raise AttributeError(
475 "Neither %r object nor %r object associated with %s "
476 "has an attribute %r"
477 % (
478 type(self).__name__,
479 type(self.comparator).__name__,
480 self,
481 key,
482 )
483 ) from err
484
485 def __str__(self) -> str:
486 return f"{self.class_.__name__}.{self.key}"
487
488 def _memoized_attr_property(self) -> Optional[MapperProperty[Any]]:
489 return self.comparator.property
490
491
492def _queryable_attribute_unreduce(
493 key: str,
494 mapped_class: Type[_O],
495 parententity: _InternalEntityType[_O],
496 entity: _ExternalEntityType[Any],
497) -> Any:
498 # this method is only used in terms of the
499 # sqlalchemy.ext.serializer extension
500 if insp_is_aliased_class(parententity):
501 return entity._get_from_serialized(key, mapped_class, parententity)
502 else:
503 return getattr(entity, key)
504
505
506class InstrumentedAttribute(QueryableAttribute[_T_co]):
507 """Class bound instrumented attribute which adds basic
508 :term:`descriptor` methods.
509
510 See :class:`.QueryableAttribute` for a description of most features.
511
512
513 """
514
515 __slots__ = ()
516
517 inherit_cache = True
518 """:meta private:"""
519
520 # hack to make __doc__ writeable on instances of
521 # InstrumentedAttribute, while still keeping classlevel
522 # __doc__ correct
523
524 @util.rw_hybridproperty
525 def __doc__(self) -> Optional[str]:
526 return self._doc
527
528 @__doc__.setter # type: ignore
529 def __doc__(self, value: Optional[str]) -> None:
530 self._doc = value
531
532 @__doc__.classlevel # type: ignore
533 def __doc__(cls) -> Optional[str]:
534 return super().__doc__
535
536 def __set__(self, instance: object, value: Any) -> None:
537 self.impl.set(
538 instance_state(instance), instance_dict(instance), value, None
539 )
540
541 def __delete__(self, instance: object) -> None:
542 self.impl.delete(instance_state(instance), instance_dict(instance))
543
544 @overload
545 def __get__(
546 self, instance: None, owner: Any
547 ) -> InstrumentedAttribute[_T_co]: ...
548
549 @overload
550 def __get__(self, instance: object, owner: Any) -> _T_co: ...
551
552 def __get__(
553 self, instance: Optional[object], owner: Any
554 ) -> Union[InstrumentedAttribute[_T_co], _T_co]:
555 if instance is None:
556 return self
557
558 dict_ = instance_dict(instance)
559 if self.impl.supports_population and self.key in dict_:
560 return dict_[self.key] # type: ignore[no-any-return]
561 else:
562 try:
563 state = instance_state(instance)
564 except AttributeError as err:
565 raise orm_exc.UnmappedInstanceError(instance) from err
566 return self.impl.get(state, dict_) # type: ignore[no-any-return]
567
568
569@dataclasses.dataclass(frozen=True)
570class AdHocHasEntityNamespace(HasCacheKey):
571 _traverse_internals: ClassVar[_TraverseInternalsType] = [
572 ("_entity_namespace", InternalTraversal.dp_has_cache_key),
573 ]
574
575 # py37 compat, no slots=True on dataclass
576 __slots__ = ("_entity_namespace",)
577 _entity_namespace: _InternalEntityType[Any]
578 is_mapper: ClassVar[bool] = False
579 is_aliased_class: ClassVar[bool] = False
580
581 @property
582 def entity_namespace(self):
583 return self._entity_namespace.entity_namespace
584
585
586def create_proxied_attribute(
587 descriptor: Any,
588) -> Callable[..., QueryableAttribute[Any]]:
589 """Create an QueryableAttribute / user descriptor hybrid.
590
591 Returns a new QueryableAttribute type that delegates descriptor
592 behavior and getattr() to the given descriptor.
593 """
594
595 # TODO: can move this to descriptor_props if the need for this
596 # function is removed from ext/hybrid.py
597
598 class Proxy(QueryableAttribute[Any]):
599 """Presents the :class:`.QueryableAttribute` interface as a
600 proxy on top of a Python descriptor / :class:`.PropComparator`
601 combination.
602
603 """
604
605 _extra_criteria = ()
606
607 # the attribute error catches inside of __getattr__ basically create a
608 # singularity if you try putting slots on this too
609 # __slots__ = ("descriptor", "original_property", "_comparator")
610
611 def __init__(
612 self,
613 class_,
614 key,
615 descriptor,
616 comparator,
617 adapt_to_entity=None,
618 doc=None,
619 original_property=None,
620 ):
621 self.class_ = class_
622 self.key = key
623 self.descriptor = descriptor
624 self.original_property = original_property
625 self._comparator = comparator
626 self._adapt_to_entity = adapt_to_entity
627 self._doc = self.__doc__ = doc
628
629 @property
630 def _parententity(self):
631 return inspection.inspect(self.class_, raiseerr=False)
632
633 @property
634 def parent(self):
635 return inspection.inspect(self.class_, raiseerr=False)
636
637 _is_internal_proxy = True
638
639 _cache_key_traversal = [
640 ("key", visitors.ExtendedInternalTraversal.dp_string),
641 ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
642 ]
643
644 @property
645 def _impl_uses_objects(self):
646 return (
647 self.original_property is not None
648 and getattr(self.class_, self.key).impl.uses_objects
649 )
650
651 @property
652 def _entity_namespace(self):
653 if hasattr(self._comparator, "_parententity"):
654 return self._comparator._parententity
655 else:
656 # used by hybrid attributes which try to remain
657 # agnostic of any ORM concepts like mappers
658 return AdHocHasEntityNamespace(self._parententity)
659
660 @property
661 def property(self):
662 return self.comparator.property
663
664 @util.memoized_property
665 def comparator(self):
666 if callable(self._comparator):
667 self._comparator = self._comparator()
668 if self._adapt_to_entity:
669 self._comparator = self._comparator.adapt_to_entity(
670 self._adapt_to_entity
671 )
672 return self._comparator
673
674 def adapt_to_entity(self, adapt_to_entity):
675 return self.__class__(
676 adapt_to_entity.entity,
677 self.key,
678 self.descriptor,
679 self._comparator,
680 adapt_to_entity,
681 )
682
683 def _clone(self, **kw):
684 return self.__class__(
685 self.class_,
686 self.key,
687 self.descriptor,
688 self._comparator,
689 adapt_to_entity=self._adapt_to_entity,
690 original_property=self.original_property,
691 )
692
693 def __get__(self, instance, owner):
694 retval = self.descriptor.__get__(instance, owner)
695 # detect if this is a plain Python @property, which just returns
696 # itself for class level access. If so, then return us.
697 # Otherwise, return the object returned by the descriptor.
698 if retval is self.descriptor and instance is None:
699 return self
700 else:
701 return retval
702
703 def __str__(self) -> str:
704 return f"{self.class_.__name__}.{self.key}"
705
706 def __getattr__(self, attribute):
707 """Delegate __getattr__ to the original descriptor and/or
708 comparator."""
709
710 # this is unfortunately very complicated, and is easily prone
711 # to recursion overflows when implementations of related
712 # __getattr__ schemes are changed
713
714 try:
715 return util.MemoizedSlots.__getattr__(self, attribute)
716 except AttributeError:
717 pass
718
719 try:
720 return getattr(descriptor, attribute)
721 except AttributeError as err:
722 if attribute == "comparator":
723 raise AttributeError("comparator") from err
724 try:
725 # comparator itself might be unreachable
726 comparator = self.comparator
727 except AttributeError as err2:
728 raise AttributeError(
729 "Neither %r object nor unconfigured comparator "
730 "object associated with %s has an attribute %r"
731 % (type(descriptor).__name__, self, attribute)
732 ) from err2
733 else:
734 try:
735 return getattr(comparator, attribute)
736 except AttributeError as err3:
737 raise AttributeError(
738 "Neither %r object nor %r object "
739 "associated with %s has an attribute %r"
740 % (
741 type(descriptor).__name__,
742 type(comparator).__name__,
743 self,
744 attribute,
745 )
746 ) from err3
747
748 Proxy.__name__ = type(descriptor).__name__ + "Proxy"
749
750 util.monkeypatch_proxied_specials(
751 Proxy, type(descriptor), name="descriptor", from_instance=descriptor
752 )
753 return Proxy
754
755
756OP_REMOVE = util.symbol("REMOVE")
757OP_APPEND = util.symbol("APPEND")
758OP_REPLACE = util.symbol("REPLACE")
759OP_BULK_REPLACE = util.symbol("BULK_REPLACE")
760OP_MODIFIED = util.symbol("MODIFIED")
761
762
763class AttributeEventToken:
764 """A token propagated throughout the course of a chain of attribute
765 events.
766
767 Serves as an indicator of the source of the event and also provides
768 a means of controlling propagation across a chain of attribute
769 operations.
770
771 The :class:`.Event` object is sent as the ``initiator`` argument
772 when dealing with events such as :meth:`.AttributeEvents.append`,
773 :meth:`.AttributeEvents.set`,
774 and :meth:`.AttributeEvents.remove`.
775
776 The :class:`.Event` object is currently interpreted by the backref
777 event handlers, and is used to control the propagation of operations
778 across two mutually-dependent attributes.
779
780 .. versionchanged:: 2.0 Changed the name from ``AttributeEvent``
781 to ``AttributeEventToken``.
782
783 :attribute impl: The :class:`.AttributeImpl` which is the current event
784 initiator.
785
786 :attribute op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE`,
787 :attr:`.OP_REPLACE`, or :attr:`.OP_BULK_REPLACE`, indicating the
788 source operation.
789
790 """
791
792 __slots__ = "impl", "op", "parent_token"
793
794 def __init__(self, attribute_impl: AttributeImpl, op: util.symbol):
795 self.impl = attribute_impl
796 self.op = op
797 self.parent_token = self.impl.parent_token
798
799 def __eq__(self, other):
800 return (
801 isinstance(other, AttributeEventToken)
802 and other.impl is self.impl
803 and other.op == self.op
804 )
805
806 @property
807 def key(self):
808 return self.impl.key
809
810 def hasparent(self, state):
811 return self.impl.hasparent(state)
812
813
814AttributeEvent = AttributeEventToken # legacy
815Event = AttributeEventToken # legacy
816
817
818class AttributeImpl:
819 """internal implementation for instrumented attributes."""
820
821 collection: bool
822 default_accepts_scalar_loader: bool
823 uses_objects: bool
824 supports_population: bool
825 dynamic: bool
826
827 _is_has_collection_adapter = False
828
829 _replace_token: AttributeEventToken
830 _remove_token: AttributeEventToken
831 _append_token: AttributeEventToken
832
833 def __init__(
834 self,
835 class_: _ExternalEntityType[_O],
836 key: str,
837 callable_: Optional[_LoaderCallable],
838 dispatch: _Dispatch[QueryableAttribute[Any]],
839 trackparent: bool = False,
840 compare_function: Optional[Callable[..., bool]] = None,
841 active_history: bool = False,
842 parent_token: Optional[AttributeEventToken] = None,
843 load_on_unexpire: bool = True,
844 send_modified_events: bool = True,
845 accepts_scalar_loader: Optional[bool] = None,
846 **kwargs: Any,
847 ):
848 r"""Construct an AttributeImpl.
849
850 :param \class_: associated class
851
852 :param key: string name of the attribute
853
854 :param \callable_:
855 optional function which generates a callable based on a parent
856 instance, which produces the "default" values for a scalar or
857 collection attribute when it's first accessed, if not present
858 already.
859
860 :param trackparent:
861 if True, attempt to track if an instance has a parent attached
862 to it via this attribute.
863
864 :param compare_function:
865 a function that compares two values which are normally
866 assignable to this attribute.
867
868 :param active_history:
869 indicates that get_history() should always return the "old" value,
870 even if it means executing a lazy callable upon attribute change.
871
872 :param parent_token:
873 Usually references the MapperProperty, used as a key for
874 the hasparent() function to identify an "owning" attribute.
875 Allows multiple AttributeImpls to all match a single
876 owner attribute.
877
878 :param load_on_unexpire:
879 if False, don't include this attribute in a load-on-expired
880 operation, i.e. the "expired_attribute_loader" process.
881 The attribute can still be in the "expired" list and be
882 considered to be "expired". Previously, this flag was called
883 "expire_missing" and is only used by a deferred column
884 attribute.
885
886 :param send_modified_events:
887 if False, the InstanceState._modified_event method will have no
888 effect; this means the attribute will never show up as changed in a
889 history entry.
890
891 """
892 self.class_ = class_
893 self.key = key
894 self.callable_ = callable_
895 self.dispatch = dispatch
896 self.trackparent = trackparent
897 self.parent_token = parent_token or self
898 self.send_modified_events = send_modified_events
899 if compare_function is None:
900 self.is_equal = operator.eq
901 else:
902 self.is_equal = compare_function
903
904 if accepts_scalar_loader is not None:
905 self.accepts_scalar_loader = accepts_scalar_loader
906 else:
907 self.accepts_scalar_loader = self.default_accepts_scalar_loader
908
909 _deferred_history = kwargs.pop("_deferred_history", False)
910 self._deferred_history = _deferred_history
911
912 if active_history:
913 self.dispatch._active_history = True
914
915 self.load_on_unexpire = load_on_unexpire
916 self._modified_token = AttributeEventToken(self, OP_MODIFIED)
917
918 __slots__ = (
919 "class_",
920 "key",
921 "callable_",
922 "dispatch",
923 "trackparent",
924 "parent_token",
925 "send_modified_events",
926 "is_equal",
927 "load_on_unexpire",
928 "_modified_token",
929 "accepts_scalar_loader",
930 "_deferred_history",
931 )
932
933 def __str__(self) -> str:
934 return f"{self.class_.__name__}.{self.key}"
935
936 def _get_active_history(self):
937 """Backwards compat for impl.active_history"""
938
939 return self.dispatch._active_history
940
941 def _set_active_history(self, value):
942 self.dispatch._active_history = value
943
944 active_history = property(_get_active_history, _set_active_history)
945
946 def hasparent(
947 self, state: InstanceState[Any], optimistic: bool = False
948 ) -> bool:
949 """Return the boolean value of a `hasparent` flag attached to
950 the given state.
951
952 The `optimistic` flag determines what the default return value
953 should be if no `hasparent` flag can be located.
954
955 As this function is used to determine if an instance is an
956 *orphan*, instances that were loaded from storage should be
957 assumed to not be orphans, until a True/False value for this
958 flag is set.
959
960 An instance attribute that is loaded by a callable function
961 will also not have a `hasparent` flag.
962
963 """
964 msg = "This AttributeImpl is not configured to track parents."
965 assert self.trackparent, msg
966
967 return (
968 state.parents.get(id(self.parent_token), optimistic) is not False
969 )
970
971 def sethasparent(
972 self,
973 state: InstanceState[Any],
974 parent_state: InstanceState[Any],
975 value: bool,
976 ) -> None:
977 """Set a boolean flag on the given item corresponding to
978 whether or not it is attached to a parent object via the
979 attribute represented by this ``InstrumentedAttribute``.
980
981 """
982 msg = "This AttributeImpl is not configured to track parents."
983 assert self.trackparent, msg
984
985 id_ = id(self.parent_token)
986 if value:
987 state.parents[id_] = parent_state
988 else:
989 if id_ in state.parents:
990 last_parent = state.parents[id_]
991
992 if (
993 last_parent is not False
994 and last_parent.key != parent_state.key
995 ):
996 if last_parent.obj() is None:
997 raise orm_exc.StaleDataError(
998 "Removing state %s from parent "
999 "state %s along attribute '%s', "
1000 "but the parent record "
1001 "has gone stale, can't be sure this "
1002 "is the most recent parent."
1003 % (
1004 state_str(state),
1005 state_str(parent_state),
1006 self.key,
1007 )
1008 )
1009
1010 return
1011
1012 state.parents[id_] = False
1013
1014 def get_history(
1015 self,
1016 state: InstanceState[Any],
1017 dict_: _InstanceDict,
1018 passive: PassiveFlag = PASSIVE_OFF,
1019 ) -> History:
1020 raise NotImplementedError()
1021
1022 def get_all_pending(
1023 self,
1024 state: InstanceState[Any],
1025 dict_: _InstanceDict,
1026 passive: PassiveFlag = PASSIVE_NO_INITIALIZE,
1027 ) -> _AllPendingType:
1028 """Return a list of tuples of (state, obj)
1029 for all objects in this attribute's current state
1030 + history.
1031
1032 Only applies to object-based attributes.
1033
1034 This is an inlining of existing functionality
1035 which roughly corresponds to:
1036
1037 get_state_history(
1038 state,
1039 key,
1040 passive=PASSIVE_NO_INITIALIZE).sum()
1041
1042 """
1043 raise NotImplementedError()
1044
1045 def _default_value(
1046 self, state: InstanceState[Any], dict_: _InstanceDict
1047 ) -> Any:
1048 """Produce an empty value for an uninitialized scalar attribute."""
1049
1050 assert self.key not in dict_, (
1051 "_default_value should only be invoked for an "
1052 "uninitialized or expired attribute"
1053 )
1054
1055 value = None
1056 for fn in self.dispatch.init_scalar:
1057 ret = fn(state, value, dict_)
1058 if ret is not ATTR_EMPTY:
1059 value = ret
1060
1061 return value
1062
1063 def get(
1064 self,
1065 state: InstanceState[Any],
1066 dict_: _InstanceDict,
1067 passive: PassiveFlag = PASSIVE_OFF,
1068 ) -> Any:
1069 """Retrieve a value from the given object.
1070 If a callable is assembled on this object's attribute, and
1071 passive is False, the callable will be executed and the
1072 resulting value will be set as the new value for this attribute.
1073 """
1074 if self.key in dict_:
1075 return dict_[self.key]
1076 else:
1077 # if history present, don't load
1078 key = self.key
1079 if (
1080 key not in state.committed_state
1081 or state.committed_state[key] is NO_VALUE
1082 ):
1083 if not passive & CALLABLES_OK:
1084 return PASSIVE_NO_RESULT
1085
1086 value = self._fire_loader_callables(state, key, passive)
1087
1088 if value is PASSIVE_NO_RESULT or value is NO_VALUE:
1089 return value
1090 elif value is ATTR_WAS_SET:
1091 try:
1092 return dict_[key]
1093 except KeyError as err:
1094 # TODO: no test coverage here.
1095 raise KeyError(
1096 "Deferred loader for attribute "
1097 "%r failed to populate "
1098 "correctly" % key
1099 ) from err
1100 elif value is not ATTR_EMPTY:
1101 return self.set_committed_value(state, dict_, value)
1102
1103 if not passive & INIT_OK:
1104 return NO_VALUE
1105 else:
1106 return self._default_value(state, dict_)
1107
1108 def _fire_loader_callables(
1109 self, state: InstanceState[Any], key: str, passive: PassiveFlag
1110 ) -> Any:
1111 if (
1112 self.accepts_scalar_loader
1113 and self.load_on_unexpire
1114 and key in state.expired_attributes
1115 ):
1116 return state._load_expired(state, passive)
1117 elif key in state.callables:
1118 callable_ = state.callables[key]
1119 return callable_(state, passive)
1120 elif self.callable_:
1121 return self.callable_(state, passive)
1122 else:
1123 return ATTR_EMPTY
1124
1125 def append(
1126 self,
1127 state: InstanceState[Any],
1128 dict_: _InstanceDict,
1129 value: Any,
1130 initiator: Optional[AttributeEventToken],
1131 passive: PassiveFlag = PASSIVE_OFF,
1132 ) -> None:
1133 self.set(state, dict_, value, initiator, passive=passive)
1134
1135 def remove(
1136 self,
1137 state: InstanceState[Any],
1138 dict_: _InstanceDict,
1139 value: Any,
1140 initiator: Optional[AttributeEventToken],
1141 passive: PassiveFlag = PASSIVE_OFF,
1142 ) -> None:
1143 self.set(
1144 state, dict_, None, initiator, passive=passive, check_old=value
1145 )
1146
1147 def pop(
1148 self,
1149 state: InstanceState[Any],
1150 dict_: _InstanceDict,
1151 value: Any,
1152 initiator: Optional[AttributeEventToken],
1153 passive: PassiveFlag = PASSIVE_OFF,
1154 ) -> None:
1155 self.set(
1156 state,
1157 dict_,
1158 None,
1159 initiator,
1160 passive=passive,
1161 check_old=value,
1162 pop=True,
1163 )
1164
1165 def set(
1166 self,
1167 state: InstanceState[Any],
1168 dict_: _InstanceDict,
1169 value: Any,
1170 initiator: Optional[AttributeEventToken] = None,
1171 passive: PassiveFlag = PASSIVE_OFF,
1172 check_old: Any = None,
1173 pop: bool = False,
1174 ) -> None:
1175 raise NotImplementedError()
1176
1177 def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None:
1178 raise NotImplementedError()
1179
1180 def get_committed_value(
1181 self,
1182 state: InstanceState[Any],
1183 dict_: _InstanceDict,
1184 passive: PassiveFlag = PASSIVE_OFF,
1185 ) -> Any:
1186 """return the unchanged value of this attribute"""
1187
1188 if self.key in state.committed_state:
1189 value = state.committed_state[self.key]
1190 if value is NO_VALUE:
1191 return None
1192 else:
1193 return value
1194 else:
1195 return self.get(state, dict_, passive=passive)
1196
1197 def set_committed_value(self, state, dict_, value):
1198 """set an attribute value on the given instance and 'commit' it."""
1199
1200 dict_[self.key] = value
1201 state._commit(dict_, [self.key])
1202 return value
1203
1204
1205class ScalarAttributeImpl(AttributeImpl):
1206 """represents a scalar value-holding InstrumentedAttribute."""
1207
1208 default_accepts_scalar_loader = True
1209 uses_objects = False
1210 supports_population = True
1211 collection = False
1212 dynamic = False
1213
1214 __slots__ = "_replace_token", "_append_token", "_remove_token"
1215
1216 def __init__(self, *arg, **kw):
1217 super().__init__(*arg, **kw)
1218 self._replace_token = self._append_token = AttributeEventToken(
1219 self, OP_REPLACE
1220 )
1221 self._remove_token = AttributeEventToken(self, OP_REMOVE)
1222
1223 def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None:
1224 if self.dispatch._active_history:
1225 old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE)
1226 else:
1227 old = dict_.get(self.key, NO_VALUE)
1228
1229 if self.dispatch.remove:
1230 self.fire_remove_event(state, dict_, old, self._remove_token)
1231 state._modified_event(dict_, self, old)
1232
1233 existing = dict_.pop(self.key, NO_VALUE)
1234 if (
1235 existing is NO_VALUE
1236 and old is NO_VALUE
1237 and not state.expired
1238 and self.key not in state.expired_attributes
1239 ):
1240 raise AttributeError("%s object does not have a value" % self)
1241
1242 def get_history(
1243 self,
1244 state: InstanceState[Any],
1245 dict_: Dict[str, Any],
1246 passive: PassiveFlag = PASSIVE_OFF,
1247 ) -> History:
1248 if self.key in dict_:
1249 return History.from_scalar_attribute(self, state, dict_[self.key])
1250 elif self.key in state.committed_state:
1251 return History.from_scalar_attribute(self, state, NO_VALUE)
1252 else:
1253 if passive & INIT_OK:
1254 passive ^= INIT_OK
1255 current = self.get(state, dict_, passive=passive)
1256 if current is PASSIVE_NO_RESULT:
1257 return HISTORY_BLANK
1258 else:
1259 return History.from_scalar_attribute(self, state, current)
1260
1261 def set(
1262 self,
1263 state: InstanceState[Any],
1264 dict_: Dict[str, Any],
1265 value: Any,
1266 initiator: Optional[AttributeEventToken] = None,
1267 passive: PassiveFlag = PASSIVE_OFF,
1268 check_old: Optional[object] = None,
1269 pop: bool = False,
1270 ) -> None:
1271 if self.dispatch._active_history:
1272 old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE)
1273 else:
1274 old = dict_.get(self.key, NO_VALUE)
1275
1276 if self.dispatch.set:
1277 value = self.fire_replace_event(
1278 state, dict_, value, old, initiator
1279 )
1280 state._modified_event(dict_, self, old)
1281 dict_[self.key] = value
1282
1283 def fire_replace_event(
1284 self,
1285 state: InstanceState[Any],
1286 dict_: _InstanceDict,
1287 value: _T,
1288 previous: Any,
1289 initiator: Optional[AttributeEventToken],
1290 ) -> _T:
1291 for fn in self.dispatch.set:
1292 value = fn(
1293 state, value, previous, initiator or self._replace_token
1294 )
1295 return value
1296
1297 def fire_remove_event(
1298 self,
1299 state: InstanceState[Any],
1300 dict_: _InstanceDict,
1301 value: Any,
1302 initiator: Optional[AttributeEventToken],
1303 ) -> None:
1304 for fn in self.dispatch.remove:
1305 fn(state, value, initiator or self._remove_token)
1306
1307
1308class ScalarObjectAttributeImpl(ScalarAttributeImpl):
1309 """represents a scalar-holding InstrumentedAttribute,
1310 where the target object is also instrumented.
1311
1312 Adds events to delete/set operations.
1313
1314 """
1315
1316 default_accepts_scalar_loader = False
1317 uses_objects = True
1318 supports_population = True
1319 collection = False
1320
1321 __slots__ = ()
1322
1323 def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None:
1324 if self.dispatch._active_history:
1325 old = self.get(
1326 state,
1327 dict_,
1328 passive=PASSIVE_ONLY_PERSISTENT
1329 | NO_AUTOFLUSH
1330 | LOAD_AGAINST_COMMITTED,
1331 )
1332 else:
1333 old = self.get(
1334 state,
1335 dict_,
1336 passive=PASSIVE_NO_FETCH ^ INIT_OK
1337 | LOAD_AGAINST_COMMITTED
1338 | NO_RAISE,
1339 )
1340
1341 self.fire_remove_event(state, dict_, old, self._remove_token)
1342
1343 existing = dict_.pop(self.key, NO_VALUE)
1344
1345 # if the attribute is expired, we currently have no way to tell
1346 # that an object-attribute was expired vs. not loaded. So
1347 # for this test, we look to see if the object has a DB identity.
1348 if (
1349 existing is NO_VALUE
1350 and old is not PASSIVE_NO_RESULT
1351 and state.key is None
1352 ):
1353 raise AttributeError("%s object does not have a value" % self)
1354
1355 def get_history(
1356 self,
1357 state: InstanceState[Any],
1358 dict_: _InstanceDict,
1359 passive: PassiveFlag = PASSIVE_OFF,
1360 ) -> History:
1361 if self.key in dict_:
1362 current = dict_[self.key]
1363 else:
1364 if passive & INIT_OK:
1365 passive ^= INIT_OK
1366 current = self.get(state, dict_, passive=passive)
1367 if current is PASSIVE_NO_RESULT:
1368 return HISTORY_BLANK
1369
1370 if not self._deferred_history:
1371 return History.from_object_attribute(self, state, current)
1372 else:
1373 original = state.committed_state.get(self.key, _NO_HISTORY)
1374 if original is PASSIVE_NO_RESULT:
1375 loader_passive = passive | (
1376 PASSIVE_ONLY_PERSISTENT
1377 | NO_AUTOFLUSH
1378 | LOAD_AGAINST_COMMITTED
1379 | NO_RAISE
1380 | DEFERRED_HISTORY_LOAD
1381 )
1382 original = self._fire_loader_callables(
1383 state, self.key, loader_passive
1384 )
1385 return History.from_object_attribute(
1386 self, state, current, original=original
1387 )
1388
1389 def get_all_pending(
1390 self,
1391 state: InstanceState[Any],
1392 dict_: _InstanceDict,
1393 passive: PassiveFlag = PASSIVE_NO_INITIALIZE,
1394 ) -> _AllPendingType:
1395 if self.key in dict_:
1396 current = dict_[self.key]
1397 elif passive & CALLABLES_OK:
1398 current = self.get(state, dict_, passive=passive)
1399 else:
1400 return []
1401
1402 ret: _AllPendingType
1403
1404 # can't use __hash__(), can't use __eq__() here
1405 if (
1406 current is not None
1407 and current is not PASSIVE_NO_RESULT
1408 and current is not NO_VALUE
1409 ):
1410 ret = [(instance_state(current), current)]
1411 else:
1412 ret = [(None, None)]
1413
1414 if self.key in state.committed_state:
1415 original = state.committed_state[self.key]
1416 if (
1417 original is not None
1418 and original is not PASSIVE_NO_RESULT
1419 and original is not NO_VALUE
1420 and original is not current
1421 ):
1422 ret.append((instance_state(original), original))
1423 return ret
1424
1425 def set(
1426 self,
1427 state: InstanceState[Any],
1428 dict_: _InstanceDict,
1429 value: Any,
1430 initiator: Optional[AttributeEventToken] = None,
1431 passive: PassiveFlag = PASSIVE_OFF,
1432 check_old: Any = None,
1433 pop: bool = False,
1434 ) -> None:
1435 """Set a value on the given InstanceState."""
1436
1437 if self.dispatch._active_history:
1438 old = self.get(
1439 state,
1440 dict_,
1441 passive=PASSIVE_ONLY_PERSISTENT
1442 | NO_AUTOFLUSH
1443 | LOAD_AGAINST_COMMITTED,
1444 )
1445 else:
1446 old = self.get(
1447 state,
1448 dict_,
1449 passive=PASSIVE_NO_FETCH ^ INIT_OK
1450 | LOAD_AGAINST_COMMITTED
1451 | NO_RAISE,
1452 )
1453
1454 if (
1455 check_old is not None
1456 and old is not PASSIVE_NO_RESULT
1457 and check_old is not old
1458 ):
1459 if pop:
1460 return
1461 else:
1462 raise ValueError(
1463 "Object %s not associated with %s on attribute '%s'"
1464 % (instance_str(check_old), state_str(state), self.key)
1465 )
1466
1467 value = self.fire_replace_event(state, dict_, value, old, initiator)
1468 dict_[self.key] = value
1469
1470 def fire_remove_event(
1471 self,
1472 state: InstanceState[Any],
1473 dict_: _InstanceDict,
1474 value: Any,
1475 initiator: Optional[AttributeEventToken],
1476 ) -> None:
1477 if self.trackparent and value not in (
1478 None,
1479 PASSIVE_NO_RESULT,
1480 NO_VALUE,
1481 ):
1482 self.sethasparent(instance_state(value), state, False)
1483
1484 for fn in self.dispatch.remove:
1485 fn(state, value, initiator or self._remove_token)
1486
1487 state._modified_event(dict_, self, value)
1488
1489 def fire_replace_event(
1490 self,
1491 state: InstanceState[Any],
1492 dict_: _InstanceDict,
1493 value: _T,
1494 previous: Any,
1495 initiator: Optional[AttributeEventToken],
1496 ) -> _T:
1497 if self.trackparent:
1498 if previous is not value and previous not in (
1499 None,
1500 PASSIVE_NO_RESULT,
1501 NO_VALUE,
1502 ):
1503 self.sethasparent(instance_state(previous), state, False)
1504
1505 for fn in self.dispatch.set:
1506 value = fn(
1507 state, value, previous, initiator or self._replace_token
1508 )
1509
1510 state._modified_event(dict_, self, previous)
1511
1512 if self.trackparent:
1513 if value is not None:
1514 self.sethasparent(instance_state(value), state, True)
1515
1516 return value
1517
1518
1519class HasCollectionAdapter:
1520 __slots__ = ()
1521
1522 collection: bool
1523 _is_has_collection_adapter = True
1524
1525 def _dispose_previous_collection(
1526 self,
1527 state: InstanceState[Any],
1528 collection: _AdaptedCollectionProtocol,
1529 adapter: CollectionAdapter,
1530 fire_event: bool,
1531 ) -> None:
1532 raise NotImplementedError()
1533
1534 @overload
1535 def get_collection(
1536 self,
1537 state: InstanceState[Any],
1538 dict_: _InstanceDict,
1539 user_data: Literal[None] = ...,
1540 passive: Literal[PassiveFlag.PASSIVE_OFF] = ...,
1541 ) -> CollectionAdapter: ...
1542
1543 @overload
1544 def get_collection(
1545 self,
1546 state: InstanceState[Any],
1547 dict_: _InstanceDict,
1548 user_data: _AdaptedCollectionProtocol = ...,
1549 passive: PassiveFlag = ...,
1550 ) -> CollectionAdapter: ...
1551
1552 @overload
1553 def get_collection(
1554 self,
1555 state: InstanceState[Any],
1556 dict_: _InstanceDict,
1557 user_data: Optional[_AdaptedCollectionProtocol] = ...,
1558 passive: PassiveFlag = ...,
1559 ) -> Union[
1560 Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
1561 ]: ...
1562
1563 def get_collection(
1564 self,
1565 state: InstanceState[Any],
1566 dict_: _InstanceDict,
1567 user_data: Optional[_AdaptedCollectionProtocol] = None,
1568 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
1569 ) -> Union[
1570 Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
1571 ]:
1572 raise NotImplementedError()
1573
1574 def set(
1575 self,
1576 state: InstanceState[Any],
1577 dict_: _InstanceDict,
1578 value: Any,
1579 initiator: Optional[AttributeEventToken] = None,
1580 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
1581 check_old: Any = None,
1582 pop: bool = False,
1583 _adapt: bool = True,
1584 ) -> None:
1585 raise NotImplementedError()
1586
1587
1588if TYPE_CHECKING:
1589
1590 def _is_collection_attribute_impl(
1591 impl: AttributeImpl,
1592 ) -> TypeGuard[CollectionAttributeImpl]: ...
1593
1594else:
1595 _is_collection_attribute_impl = operator.attrgetter("collection")
1596
1597
1598class CollectionAttributeImpl(HasCollectionAdapter, AttributeImpl):
1599 """A collection-holding attribute that instruments changes in membership.
1600
1601 Only handles collections of instrumented objects.
1602
1603 InstrumentedCollectionAttribute holds an arbitrary, user-specified
1604 container object (defaulting to a list) and brokers access to the
1605 CollectionAdapter, a "view" onto that object that presents consistent bag
1606 semantics to the orm layer independent of the user data implementation.
1607
1608 """
1609
1610 uses_objects = True
1611 collection = True
1612 default_accepts_scalar_loader = False
1613 supports_population = True
1614 dynamic = False
1615
1616 _bulk_replace_token: AttributeEventToken
1617
1618 __slots__ = (
1619 "copy",
1620 "collection_factory",
1621 "_append_token",
1622 "_remove_token",
1623 "_bulk_replace_token",
1624 "_duck_typed_as",
1625 )
1626
1627 def __init__(
1628 self,
1629 class_,
1630 key,
1631 callable_,
1632 dispatch,
1633 typecallable=None,
1634 trackparent=False,
1635 copy_function=None,
1636 compare_function=None,
1637 **kwargs,
1638 ):
1639 super().__init__(
1640 class_,
1641 key,
1642 callable_,
1643 dispatch,
1644 trackparent=trackparent,
1645 compare_function=compare_function,
1646 **kwargs,
1647 )
1648
1649 if copy_function is None:
1650 copy_function = self.__copy
1651 self.copy = copy_function
1652 self.collection_factory = typecallable
1653 self._append_token = AttributeEventToken(self, OP_APPEND)
1654 self._remove_token = AttributeEventToken(self, OP_REMOVE)
1655 self._bulk_replace_token = AttributeEventToken(self, OP_BULK_REPLACE)
1656 self._duck_typed_as = util.duck_type_collection(
1657 self.collection_factory()
1658 )
1659
1660 if getattr(self.collection_factory, "_sa_linker", None):
1661
1662 @event.listens_for(self, "init_collection")
1663 def link(target, collection, collection_adapter):
1664 collection._sa_linker(collection_adapter)
1665
1666 @event.listens_for(self, "dispose_collection")
1667 def unlink(target, collection, collection_adapter):
1668 collection._sa_linker(None)
1669
1670 def __copy(self, item):
1671 return [y for y in collections.collection_adapter(item)]
1672
1673 def get_history(
1674 self,
1675 state: InstanceState[Any],
1676 dict_: _InstanceDict,
1677 passive: PassiveFlag = PASSIVE_OFF,
1678 ) -> History:
1679 current = self.get(state, dict_, passive=passive)
1680
1681 if current is PASSIVE_NO_RESULT:
1682 if (
1683 passive & PassiveFlag.INCLUDE_PENDING_MUTATIONS
1684 and self.key in state._pending_mutations
1685 ):
1686 pending = state._pending_mutations[self.key]
1687 return pending.merge_with_history(HISTORY_BLANK)
1688 else:
1689 return HISTORY_BLANK
1690 else:
1691 if passive & PassiveFlag.INCLUDE_PENDING_MUTATIONS:
1692 # this collection is loaded / present. should not be any
1693 # pending mutations
1694 assert self.key not in state._pending_mutations
1695
1696 return History.from_collection(self, state, current)
1697
1698 def get_all_pending(
1699 self,
1700 state: InstanceState[Any],
1701 dict_: _InstanceDict,
1702 passive: PassiveFlag = PASSIVE_NO_INITIALIZE,
1703 ) -> _AllPendingType:
1704 # NOTE: passive is ignored here at the moment
1705
1706 if self.key not in dict_:
1707 return []
1708
1709 current = dict_[self.key]
1710 current = getattr(current, "_sa_adapter")
1711
1712 if self.key in state.committed_state:
1713 original = state.committed_state[self.key]
1714 if original is not NO_VALUE:
1715 current_states = [
1716 ((c is not None) and instance_state(c) or None, c)
1717 for c in current
1718 ]
1719 original_states = [
1720 ((c is not None) and instance_state(c) or None, c)
1721 for c in original
1722 ]
1723
1724 current_set = dict(current_states)
1725 original_set = dict(original_states)
1726
1727 return (
1728 [
1729 (s, o)
1730 for s, o in current_states
1731 if s not in original_set
1732 ]
1733 + [(s, o) for s, o in current_states if s in original_set]
1734 + [
1735 (s, o)
1736 for s, o in original_states
1737 if s not in current_set
1738 ]
1739 )
1740
1741 return [(instance_state(o), o) for o in current]
1742
1743 def fire_append_event(
1744 self,
1745 state: InstanceState[Any],
1746 dict_: _InstanceDict,
1747 value: _T,
1748 initiator: Optional[AttributeEventToken],
1749 key: Optional[Any],
1750 ) -> _T:
1751 for fn in self.dispatch.append:
1752 value = fn(state, value, initiator or self._append_token, key=key)
1753
1754 state._modified_event(dict_, self, NO_VALUE, True)
1755
1756 if self.trackparent and value is not None:
1757 self.sethasparent(instance_state(value), state, True)
1758
1759 return value
1760
1761 def fire_append_wo_mutation_event(
1762 self,
1763 state: InstanceState[Any],
1764 dict_: _InstanceDict,
1765 value: _T,
1766 initiator: Optional[AttributeEventToken],
1767 key: Optional[Any],
1768 ) -> _T:
1769 for fn in self.dispatch.append_wo_mutation:
1770 value = fn(state, value, initiator or self._append_token, key=key)
1771
1772 return value
1773
1774 def fire_pre_remove_event(
1775 self,
1776 state: InstanceState[Any],
1777 dict_: _InstanceDict,
1778 initiator: Optional[AttributeEventToken],
1779 key: Optional[Any],
1780 ) -> None:
1781 """A special event used for pop() operations.
1782
1783 The "remove" event needs to have the item to be removed passed to
1784 it, which in the case of pop from a set, we don't have a way to access
1785 the item before the operation. the event is used for all pop()
1786 operations (even though set.pop is the one where it is really needed).
1787
1788 """
1789 state._modified_event(dict_, self, NO_VALUE, True)
1790
1791 def fire_remove_event(
1792 self,
1793 state: InstanceState[Any],
1794 dict_: _InstanceDict,
1795 value: Any,
1796 initiator: Optional[AttributeEventToken],
1797 key: Optional[Any],
1798 ) -> None:
1799 if self.trackparent and value is not None:
1800 self.sethasparent(instance_state(value), state, False)
1801
1802 for fn in self.dispatch.remove:
1803 fn(state, value, initiator or self._remove_token, key=key)
1804
1805 state._modified_event(dict_, self, NO_VALUE, True)
1806
1807 def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None:
1808 if self.key not in dict_:
1809 return
1810
1811 state._modified_event(dict_, self, NO_VALUE, True)
1812
1813 collection = self.get_collection(state, state.dict)
1814 collection.clear_with_event()
1815
1816 # key is always present because we checked above. e.g.
1817 # del is a no-op if collection not present.
1818 del dict_[self.key]
1819
1820 def _default_value(
1821 self, state: InstanceState[Any], dict_: _InstanceDict
1822 ) -> _AdaptedCollectionProtocol:
1823 """Produce an empty collection for an un-initialized attribute"""
1824
1825 assert self.key not in dict_, (
1826 "_default_value should only be invoked for an "
1827 "uninitialized or expired attribute"
1828 )
1829
1830 if self.key in state._empty_collections:
1831 return state._empty_collections[self.key]
1832
1833 adapter, user_data = self._initialize_collection(state)
1834 adapter._set_empty(user_data)
1835 return user_data
1836
1837 def _initialize_collection(
1838 self, state: InstanceState[Any]
1839 ) -> Tuple[CollectionAdapter, _AdaptedCollectionProtocol]:
1840 adapter, collection = state.manager.initialize_collection(
1841 self.key, state, self.collection_factory
1842 )
1843
1844 self.dispatch.init_collection(state, collection, adapter)
1845
1846 return adapter, collection
1847
1848 def append(
1849 self,
1850 state: InstanceState[Any],
1851 dict_: _InstanceDict,
1852 value: Any,
1853 initiator: Optional[AttributeEventToken],
1854 passive: PassiveFlag = PASSIVE_OFF,
1855 ) -> None:
1856 collection = self.get_collection(
1857 state, dict_, user_data=None, passive=passive
1858 )
1859 if collection is PASSIVE_NO_RESULT:
1860 value = self.fire_append_event(
1861 state, dict_, value, initiator, key=NO_KEY
1862 )
1863 assert (
1864 self.key not in dict_
1865 ), "Collection was loaded during event handling."
1866 state._get_pending_mutation(self.key).append(value)
1867 else:
1868 if TYPE_CHECKING:
1869 assert isinstance(collection, CollectionAdapter)
1870 collection.append_with_event(value, initiator)
1871
1872 def remove(
1873 self,
1874 state: InstanceState[Any],
1875 dict_: _InstanceDict,
1876 value: Any,
1877 initiator: Optional[AttributeEventToken],
1878 passive: PassiveFlag = PASSIVE_OFF,
1879 ) -> None:
1880 collection = self.get_collection(
1881 state, state.dict, user_data=None, passive=passive
1882 )
1883 if collection is PASSIVE_NO_RESULT:
1884 self.fire_remove_event(state, dict_, value, initiator, key=NO_KEY)
1885 assert (
1886 self.key not in dict_
1887 ), "Collection was loaded during event handling."
1888 state._get_pending_mutation(self.key).remove(value)
1889 else:
1890 if TYPE_CHECKING:
1891 assert isinstance(collection, CollectionAdapter)
1892 collection.remove_with_event(value, initiator)
1893
1894 def pop(
1895 self,
1896 state: InstanceState[Any],
1897 dict_: _InstanceDict,
1898 value: Any,
1899 initiator: Optional[AttributeEventToken],
1900 passive: PassiveFlag = PASSIVE_OFF,
1901 ) -> None:
1902 try:
1903 # TODO: better solution here would be to add
1904 # a "popper" role to collections.py to complement
1905 # "remover".
1906 self.remove(state, dict_, value, initiator, passive=passive)
1907 except (ValueError, KeyError, IndexError):
1908 pass
1909
1910 def set(
1911 self,
1912 state: InstanceState[Any],
1913 dict_: _InstanceDict,
1914 value: Any,
1915 initiator: Optional[AttributeEventToken] = None,
1916 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
1917 check_old: Any = None,
1918 pop: bool = False,
1919 _adapt: bool = True,
1920 ) -> None:
1921 iterable = orig_iterable = value
1922 new_keys = None
1923
1924 # pulling a new collection first so that an adaptation exception does
1925 # not trigger a lazy load of the old collection.
1926 new_collection, user_data = self._initialize_collection(state)
1927 if _adapt:
1928 if new_collection._converter is not None:
1929 iterable = new_collection._converter(iterable)
1930 else:
1931 setting_type = util.duck_type_collection(iterable)
1932 receiving_type = self._duck_typed_as
1933
1934 if setting_type is not receiving_type:
1935 given = (
1936 iterable is None
1937 and "None"
1938 or iterable.__class__.__name__
1939 )
1940 wanted = self._duck_typed_as.__name__
1941 raise TypeError(
1942 "Incompatible collection type: %s is not %s-like"
1943 % (given, wanted)
1944 )
1945
1946 # If the object is an adapted collection, return the (iterable)
1947 # adapter.
1948 if hasattr(iterable, "_sa_iterator"):
1949 iterable = iterable._sa_iterator()
1950 elif setting_type is dict:
1951 new_keys = list(iterable)
1952 iterable = iterable.values()
1953 else:
1954 iterable = iter(iterable)
1955 elif util.duck_type_collection(iterable) is dict:
1956 new_keys = list(value)
1957
1958 new_values = list(iterable)
1959
1960 evt = self._bulk_replace_token
1961
1962 self.dispatch.bulk_replace(state, new_values, evt, keys=new_keys)
1963
1964 # propagate NO_RAISE in passive through to the get() for the
1965 # existing object (ticket #8862)
1966 old = self.get(
1967 state,
1968 dict_,
1969 passive=PASSIVE_ONLY_PERSISTENT ^ (passive & PassiveFlag.NO_RAISE),
1970 )
1971 if old is PASSIVE_NO_RESULT:
1972 old = self._default_value(state, dict_)
1973 elif old is orig_iterable:
1974 # ignore re-assignment of the current collection, as happens
1975 # implicitly with in-place operators (foo.collection |= other)
1976 return
1977
1978 # place a copy of "old" in state.committed_state
1979 state._modified_event(dict_, self, old, True)
1980
1981 old_collection = old._sa_adapter
1982
1983 dict_[self.key] = user_data
1984
1985 collections.bulk_replace(
1986 new_values, old_collection, new_collection, initiator=evt
1987 )
1988
1989 self._dispose_previous_collection(state, old, old_collection, True)
1990
1991 def _dispose_previous_collection(
1992 self,
1993 state: InstanceState[Any],
1994 collection: _AdaptedCollectionProtocol,
1995 adapter: CollectionAdapter,
1996 fire_event: bool,
1997 ) -> None:
1998 del collection._sa_adapter
1999
2000 # discarding old collection make sure it is not referenced in empty
2001 # collections.
2002 state._empty_collections.pop(self.key, None)
2003 if fire_event:
2004 self.dispatch.dispose_collection(state, collection, adapter)
2005
2006 def _invalidate_collection(
2007 self, collection: _AdaptedCollectionProtocol
2008 ) -> None:
2009 adapter = getattr(collection, "_sa_adapter")
2010 adapter.invalidated = True
2011
2012 def set_committed_value(
2013 self, state: InstanceState[Any], dict_: _InstanceDict, value: Any
2014 ) -> _AdaptedCollectionProtocol:
2015 """Set an attribute value on the given instance and 'commit' it."""
2016
2017 collection, user_data = self._initialize_collection(state)
2018
2019 if value:
2020 collection.append_multiple_without_event(value)
2021
2022 state.dict[self.key] = user_data
2023
2024 state._commit(dict_, [self.key])
2025
2026 if self.key in state._pending_mutations:
2027 # pending items exist. issue a modified event,
2028 # add/remove new items.
2029 state._modified_event(dict_, self, user_data, True)
2030
2031 pending = state._pending_mutations.pop(self.key)
2032 added = pending.added_items
2033 removed = pending.deleted_items
2034 for item in added:
2035 collection.append_without_event(item)
2036 for item in removed:
2037 collection.remove_without_event(item)
2038
2039 return user_data
2040
2041 @overload
2042 def get_collection(
2043 self,
2044 state: InstanceState[Any],
2045 dict_: _InstanceDict,
2046 user_data: Literal[None] = ...,
2047 passive: Literal[PassiveFlag.PASSIVE_OFF] = ...,
2048 ) -> CollectionAdapter: ...
2049
2050 @overload
2051 def get_collection(
2052 self,
2053 state: InstanceState[Any],
2054 dict_: _InstanceDict,
2055 user_data: _AdaptedCollectionProtocol = ...,
2056 passive: PassiveFlag = ...,
2057 ) -> CollectionAdapter: ...
2058
2059 @overload
2060 def get_collection(
2061 self,
2062 state: InstanceState[Any],
2063 dict_: _InstanceDict,
2064 user_data: Optional[_AdaptedCollectionProtocol] = ...,
2065 passive: PassiveFlag = PASSIVE_OFF,
2066 ) -> Union[
2067 Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
2068 ]: ...
2069
2070 def get_collection(
2071 self,
2072 state: InstanceState[Any],
2073 dict_: _InstanceDict,
2074 user_data: Optional[_AdaptedCollectionProtocol] = None,
2075 passive: PassiveFlag = PASSIVE_OFF,
2076 ) -> Union[
2077 Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
2078 ]:
2079 """Retrieve the CollectionAdapter associated with the given state.
2080
2081 if user_data is None, retrieves it from the state using normal
2082 "get()" rules, which will fire lazy callables or return the "empty"
2083 collection value.
2084
2085 """
2086 if user_data is None:
2087 fetch_user_data = self.get(state, dict_, passive=passive)
2088 if fetch_user_data is LoaderCallableStatus.PASSIVE_NO_RESULT:
2089 return fetch_user_data
2090 else:
2091 user_data = cast("_AdaptedCollectionProtocol", fetch_user_data)
2092
2093 return user_data._sa_adapter
2094
2095
2096def backref_listeners(
2097 attribute: QueryableAttribute[Any], key: str, uselist: bool
2098) -> None:
2099 """Apply listeners to synchronize a two-way relationship."""
2100
2101 # use easily recognizable names for stack traces.
2102
2103 # in the sections marked "tokens to test for a recursive loop",
2104 # this is somewhat brittle and very performance-sensitive logic
2105 # that is specific to how we might arrive at each event. a marker
2106 # that can target us directly to arguments being invoked against
2107 # the impl might be simpler, but could interfere with other systems.
2108
2109 parent_token = attribute.impl.parent_token
2110 parent_impl = attribute.impl
2111
2112 def _acceptable_key_err(child_state, initiator, child_impl):
2113 raise ValueError(
2114 "Bidirectional attribute conflict detected: "
2115 'Passing object %s to attribute "%s" '
2116 'triggers a modify event on attribute "%s" '
2117 'via the backref "%s".'
2118 % (
2119 state_str(child_state),
2120 initiator.parent_token,
2121 child_impl.parent_token,
2122 attribute.impl.parent_token,
2123 )
2124 )
2125
2126 def emit_backref_from_scalar_set_event(
2127 state, child, oldchild, initiator, **kw
2128 ):
2129 if oldchild is child:
2130 return child
2131 if (
2132 oldchild is not None
2133 and oldchild is not PASSIVE_NO_RESULT
2134 and oldchild is not NO_VALUE
2135 ):
2136 # With lazy=None, there's no guarantee that the full collection is
2137 # present when updating via a backref.
2138 old_state, old_dict = (
2139 instance_state(oldchild),
2140 instance_dict(oldchild),
2141 )
2142 impl = old_state.manager[key].impl
2143
2144 # tokens to test for a recursive loop.
2145 if not impl.collection and not impl.dynamic:
2146 check_recursive_token = impl._replace_token
2147 else:
2148 check_recursive_token = impl._remove_token
2149
2150 if initiator is not check_recursive_token:
2151 impl.pop(
2152 old_state,
2153 old_dict,
2154 state.obj(),
2155 parent_impl._append_token,
2156 passive=PASSIVE_NO_FETCH,
2157 )
2158
2159 if child is not None:
2160 child_state, child_dict = (
2161 instance_state(child),
2162 instance_dict(child),
2163 )
2164 child_impl = child_state.manager[key].impl
2165
2166 if (
2167 initiator.parent_token is not parent_token
2168 and initiator.parent_token is not child_impl.parent_token
2169 ):
2170 _acceptable_key_err(state, initiator, child_impl)
2171
2172 # tokens to test for a recursive loop.
2173 check_append_token = child_impl._append_token
2174 check_bulk_replace_token = (
2175 child_impl._bulk_replace_token
2176 if _is_collection_attribute_impl(child_impl)
2177 else None
2178 )
2179
2180 if (
2181 initiator is not check_append_token
2182 and initiator is not check_bulk_replace_token
2183 ):
2184 child_impl.append(
2185 child_state,
2186 child_dict,
2187 state.obj(),
2188 initiator,
2189 passive=PASSIVE_NO_FETCH,
2190 )
2191 return child
2192
2193 def emit_backref_from_collection_append_event(
2194 state, child, initiator, **kw
2195 ):
2196 if child is None:
2197 return
2198
2199 child_state, child_dict = instance_state(child), instance_dict(child)
2200 child_impl = child_state.manager[key].impl
2201
2202 if (
2203 initiator.parent_token is not parent_token
2204 and initiator.parent_token is not child_impl.parent_token
2205 ):
2206 _acceptable_key_err(state, initiator, child_impl)
2207
2208 # tokens to test for a recursive loop.
2209 check_append_token = child_impl._append_token
2210 check_bulk_replace_token = (
2211 child_impl._bulk_replace_token
2212 if _is_collection_attribute_impl(child_impl)
2213 else None
2214 )
2215
2216 if (
2217 initiator is not check_append_token
2218 and initiator is not check_bulk_replace_token
2219 ):
2220 child_impl.append(
2221 child_state,
2222 child_dict,
2223 state.obj(),
2224 initiator,
2225 passive=PASSIVE_NO_FETCH,
2226 )
2227 return child
2228
2229 def emit_backref_from_collection_remove_event(
2230 state, child, initiator, **kw
2231 ):
2232 if (
2233 child is not None
2234 and child is not PASSIVE_NO_RESULT
2235 and child is not NO_VALUE
2236 ):
2237 child_state, child_dict = (
2238 instance_state(child),
2239 instance_dict(child),
2240 )
2241 child_impl = child_state.manager[key].impl
2242
2243 check_replace_token: Optional[AttributeEventToken]
2244
2245 # tokens to test for a recursive loop.
2246 if not child_impl.collection and not child_impl.dynamic:
2247 check_remove_token = child_impl._remove_token
2248 check_replace_token = child_impl._replace_token
2249 check_for_dupes_on_remove = uselist and not parent_impl.dynamic
2250 else:
2251 check_remove_token = child_impl._remove_token
2252 check_replace_token = (
2253 child_impl._bulk_replace_token
2254 if _is_collection_attribute_impl(child_impl)
2255 else None
2256 )
2257 check_for_dupes_on_remove = False
2258
2259 if (
2260 initiator is not check_remove_token
2261 and initiator is not check_replace_token
2262 ):
2263 if not check_for_dupes_on_remove or not util.has_dupes(
2264 # when this event is called, the item is usually
2265 # present in the list, except for a pop() operation.
2266 state.dict[parent_impl.key],
2267 child,
2268 ):
2269 child_impl.pop(
2270 child_state,
2271 child_dict,
2272 state.obj(),
2273 initiator,
2274 passive=PASSIVE_NO_FETCH,
2275 )
2276
2277 if uselist:
2278 event.listen(
2279 attribute,
2280 "append",
2281 emit_backref_from_collection_append_event,
2282 retval=True,
2283 raw=True,
2284 include_key=True,
2285 )
2286 else:
2287 event.listen(
2288 attribute,
2289 "set",
2290 emit_backref_from_scalar_set_event,
2291 retval=True,
2292 raw=True,
2293 include_key=True,
2294 )
2295 # TODO: need coverage in test/orm/ of remove event
2296 event.listen(
2297 attribute,
2298 "remove",
2299 emit_backref_from_collection_remove_event,
2300 retval=True,
2301 raw=True,
2302 include_key=True,
2303 )
2304
2305
2306_NO_HISTORY = util.symbol("NO_HISTORY")
2307_NO_STATE_SYMBOLS = frozenset([id(PASSIVE_NO_RESULT), id(NO_VALUE)])
2308
2309
2310class History(NamedTuple):
2311 """A 3-tuple of added, unchanged and deleted values,
2312 representing the changes which have occurred on an instrumented
2313 attribute.
2314
2315 The easiest way to get a :class:`.History` object for a particular
2316 attribute on an object is to use the :func:`_sa.inspect` function::
2317
2318 from sqlalchemy import inspect
2319
2320 hist = inspect(myobject).attrs.myattribute.history
2321
2322 Each tuple member is an iterable sequence:
2323
2324 * ``added`` - the collection of items added to the attribute (the first
2325 tuple element).
2326
2327 * ``unchanged`` - the collection of items that have not changed on the
2328 attribute (the second tuple element).
2329
2330 * ``deleted`` - the collection of items that have been removed from the
2331 attribute (the third tuple element).
2332
2333 """
2334
2335 added: Union[Tuple[()], List[Any]]
2336 unchanged: Union[Tuple[()], List[Any]]
2337 deleted: Union[Tuple[()], List[Any]]
2338
2339 def __bool__(self) -> bool:
2340 return self != HISTORY_BLANK
2341
2342 def empty(self) -> bool:
2343 """Return True if this :class:`.History` has no changes
2344 and no existing, unchanged state.
2345
2346 """
2347
2348 return not bool((self.added or self.deleted) or self.unchanged)
2349
2350 def sum(self) -> Sequence[Any]:
2351 """Return a collection of added + unchanged + deleted."""
2352
2353 return (
2354 (self.added or []) + (self.unchanged or []) + (self.deleted or [])
2355 )
2356
2357 def non_deleted(self) -> Sequence[Any]:
2358 """Return a collection of added + unchanged."""
2359
2360 return (self.added or []) + (self.unchanged or [])
2361
2362 def non_added(self) -> Sequence[Any]:
2363 """Return a collection of unchanged + deleted."""
2364
2365 return (self.unchanged or []) + (self.deleted or [])
2366
2367 def has_changes(self) -> bool:
2368 """Return True if this :class:`.History` has changes."""
2369
2370 return bool(self.added or self.deleted)
2371
2372 def _merge(self, added: Iterable[Any], deleted: Iterable[Any]) -> History:
2373 return History(
2374 list(self.added) + list(added),
2375 self.unchanged,
2376 list(self.deleted) + list(deleted),
2377 )
2378
2379 def as_state(self) -> History:
2380 return History(
2381 [
2382 (c is not None) and instance_state(c) or None
2383 for c in self.added
2384 ],
2385 [
2386 (c is not None) and instance_state(c) or None
2387 for c in self.unchanged
2388 ],
2389 [
2390 (c is not None) and instance_state(c) or None
2391 for c in self.deleted
2392 ],
2393 )
2394
2395 @classmethod
2396 def from_scalar_attribute(
2397 cls,
2398 attribute: ScalarAttributeImpl,
2399 state: InstanceState[Any],
2400 current: Any,
2401 ) -> History:
2402 original = state.committed_state.get(attribute.key, _NO_HISTORY)
2403
2404 deleted: Union[Tuple[()], List[Any]]
2405
2406 if original is _NO_HISTORY:
2407 if current is NO_VALUE:
2408 return cls((), (), ())
2409 else:
2410 return cls((), [current], ())
2411 # don't let ClauseElement expressions here trip things up
2412 elif (
2413 current is not NO_VALUE
2414 and attribute.is_equal(current, original) is True
2415 ):
2416 return cls((), [current], ())
2417 else:
2418 # current convention on native scalars is to not
2419 # include information
2420 # about missing previous value in "deleted", but
2421 # we do include None, which helps in some primary
2422 # key situations
2423 if id(original) in _NO_STATE_SYMBOLS:
2424 deleted = ()
2425 # indicate a "del" operation occurred when we don't have
2426 # the previous value as: ([None], (), ())
2427 if id(current) in _NO_STATE_SYMBOLS:
2428 current = None
2429 else:
2430 deleted = [original]
2431 if current is NO_VALUE:
2432 return cls((), (), deleted)
2433 else:
2434 return cls([current], (), deleted)
2435
2436 @classmethod
2437 def from_object_attribute(
2438 cls,
2439 attribute: ScalarObjectAttributeImpl,
2440 state: InstanceState[Any],
2441 current: Any,
2442 original: Any = _NO_HISTORY,
2443 ) -> History:
2444 deleted: Union[Tuple[()], List[Any]]
2445
2446 if original is _NO_HISTORY:
2447 original = state.committed_state.get(attribute.key, _NO_HISTORY)
2448
2449 if original is _NO_HISTORY:
2450 if current is NO_VALUE:
2451 return cls((), (), ())
2452 else:
2453 return cls((), [current], ())
2454 elif current is original and current is not NO_VALUE:
2455 return cls((), [current], ())
2456 else:
2457 # current convention on related objects is to not
2458 # include information
2459 # about missing previous value in "deleted", and
2460 # to also not include None - the dependency.py rules
2461 # ignore the None in any case.
2462 if id(original) in _NO_STATE_SYMBOLS or original is None:
2463 deleted = ()
2464 # indicate a "del" operation occurred when we don't have
2465 # the previous value as: ([None], (), ())
2466 if id(current) in _NO_STATE_SYMBOLS:
2467 current = None
2468 else:
2469 deleted = [original]
2470 if current is NO_VALUE:
2471 return cls((), (), deleted)
2472 else:
2473 return cls([current], (), deleted)
2474
2475 @classmethod
2476 def from_collection(
2477 cls,
2478 attribute: CollectionAttributeImpl,
2479 state: InstanceState[Any],
2480 current: Any,
2481 ) -> History:
2482 original = state.committed_state.get(attribute.key, _NO_HISTORY)
2483 if current is NO_VALUE:
2484 return cls((), (), ())
2485
2486 current = getattr(current, "_sa_adapter")
2487 if original is NO_VALUE:
2488 return cls(list(current), (), ())
2489 elif original is _NO_HISTORY:
2490 return cls((), list(current), ())
2491 else:
2492 current_states = [
2493 ((c is not None) and instance_state(c) or None, c)
2494 for c in current
2495 ]
2496 original_states = [
2497 ((c is not None) and instance_state(c) or None, c)
2498 for c in original
2499 ]
2500
2501 current_set = dict(current_states)
2502 original_set = dict(original_states)
2503
2504 return cls(
2505 [o for s, o in current_states if s not in original_set],
2506 [o for s, o in current_states if s in original_set],
2507 [o for s, o in original_states if s not in current_set],
2508 )
2509
2510
2511HISTORY_BLANK = History((), (), ())
2512
2513
2514def get_history(
2515 obj: object, key: str, passive: PassiveFlag = PASSIVE_OFF
2516) -> History:
2517 """Return a :class:`.History` record for the given object
2518 and attribute key.
2519
2520 This is the **pre-flush** history for a given attribute, which is
2521 reset each time the :class:`.Session` flushes changes to the
2522 current database transaction.
2523
2524 .. note::
2525
2526 Prefer to use the :attr:`.AttributeState.history` and
2527 :meth:`.AttributeState.load_history` accessors to retrieve the
2528 :class:`.History` for instance attributes.
2529
2530
2531 :param obj: an object whose class is instrumented by the
2532 attributes package.
2533
2534 :param key: string attribute name.
2535
2536 :param passive: indicates loading behavior for the attribute
2537 if the value is not already present. This is a
2538 bitflag attribute, which defaults to the symbol
2539 :attr:`.PASSIVE_OFF` indicating all necessary SQL
2540 should be emitted.
2541
2542 .. seealso::
2543
2544 :attr:`.AttributeState.history`
2545
2546 :meth:`.AttributeState.load_history` - retrieve history
2547 using loader callables if the value is not locally present.
2548
2549 """
2550
2551 return get_state_history(instance_state(obj), key, passive)
2552
2553
2554def get_state_history(
2555 state: InstanceState[Any], key: str, passive: PassiveFlag = PASSIVE_OFF
2556) -> History:
2557 return state.get_history(key, passive)
2558
2559
2560def has_parent(
2561 cls: Type[_O], obj: _O, key: str, optimistic: bool = False
2562) -> bool:
2563 """TODO"""
2564 manager = manager_of_class(cls)
2565 state = instance_state(obj)
2566 return manager.has_parent(state, key, optimistic)
2567
2568
2569def register_attribute(
2570 class_: Type[_O],
2571 key: str,
2572 *,
2573 comparator: interfaces.PropComparator[_T],
2574 parententity: _InternalEntityType[_O],
2575 doc: Optional[str] = None,
2576 **kw: Any,
2577) -> InstrumentedAttribute[_T]:
2578 desc = register_descriptor(
2579 class_, key, comparator=comparator, parententity=parententity, doc=doc
2580 )
2581 register_attribute_impl(class_, key, **kw)
2582 return desc
2583
2584
2585def register_attribute_impl(
2586 class_: Type[_O],
2587 key: str,
2588 uselist: bool = False,
2589 callable_: Optional[_LoaderCallable] = None,
2590 useobject: bool = False,
2591 impl_class: Optional[Type[AttributeImpl]] = None,
2592 backref: Optional[str] = None,
2593 **kw: Any,
2594) -> QueryableAttribute[Any]:
2595 manager = manager_of_class(class_)
2596 if uselist:
2597 factory = kw.pop("typecallable", None)
2598 typecallable = manager.instrument_collection_class(
2599 key, factory or list
2600 )
2601 else:
2602 typecallable = kw.pop("typecallable", None)
2603
2604 dispatch = cast(
2605 "_Dispatch[QueryableAttribute[Any]]", manager[key].dispatch
2606 ) # noqa: E501
2607
2608 impl: AttributeImpl
2609
2610 if impl_class:
2611 # TODO: this appears to be the WriteOnlyAttributeImpl /
2612 # DynamicAttributeImpl constructor which is hardcoded
2613 impl = cast("Type[WriteOnlyAttributeImpl]", impl_class)(
2614 class_, key, dispatch, **kw
2615 )
2616 elif uselist:
2617 impl = CollectionAttributeImpl(
2618 class_, key, callable_, dispatch, typecallable=typecallable, **kw
2619 )
2620 elif useobject:
2621 impl = ScalarObjectAttributeImpl(
2622 class_, key, callable_, dispatch, **kw
2623 )
2624 else:
2625 impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
2626
2627 manager[key].impl = impl
2628
2629 if backref:
2630 backref_listeners(manager[key], backref, uselist)
2631
2632 manager.post_configure_attribute(key)
2633 return manager[key]
2634
2635
2636def register_descriptor(
2637 class_: Type[Any],
2638 key: str,
2639 *,
2640 comparator: interfaces.PropComparator[_T],
2641 parententity: _InternalEntityType[Any],
2642 doc: Optional[str] = None,
2643) -> InstrumentedAttribute[_T]:
2644 manager = manager_of_class(class_)
2645
2646 descriptor = InstrumentedAttribute(
2647 class_, key, comparator=comparator, parententity=parententity
2648 )
2649
2650 descriptor.__doc__ = doc # type: ignore
2651
2652 manager.instrument_attribute(key, descriptor)
2653 return descriptor
2654
2655
2656def unregister_attribute(class_: Type[Any], key: str) -> None:
2657 manager_of_class(class_).uninstrument_attribute(key)
2658
2659
2660def init_collection(obj: object, key: str) -> CollectionAdapter:
2661 """Initialize a collection attribute and return the collection adapter.
2662
2663 This function is used to provide direct access to collection internals
2664 for a previously unloaded attribute. e.g.::
2665
2666 collection_adapter = init_collection(someobject, 'elements')
2667 for elem in values:
2668 collection_adapter.append_without_event(elem)
2669
2670 For an easier way to do the above, see
2671 :func:`~sqlalchemy.orm.attributes.set_committed_value`.
2672
2673 :param obj: a mapped object
2674
2675 :param key: string attribute name where the collection is located.
2676
2677 """
2678 state = instance_state(obj)
2679 dict_ = state.dict
2680 return init_state_collection(state, dict_, key)
2681
2682
2683def init_state_collection(
2684 state: InstanceState[Any], dict_: _InstanceDict, key: str
2685) -> CollectionAdapter:
2686 """Initialize a collection attribute and return the collection adapter.
2687
2688 Discards any existing collection which may be there.
2689
2690 """
2691 attr = state.manager[key].impl
2692
2693 if TYPE_CHECKING:
2694 assert isinstance(attr, HasCollectionAdapter)
2695
2696 old = dict_.pop(key, None) # discard old collection
2697 if old is not None:
2698 old_collection = old._sa_adapter
2699 attr._dispose_previous_collection(state, old, old_collection, False)
2700
2701 user_data = attr._default_value(state, dict_)
2702 adapter: CollectionAdapter = attr.get_collection(
2703 state, dict_, user_data, passive=PassiveFlag.PASSIVE_NO_FETCH
2704 )
2705 adapter._reset_empty()
2706
2707 return adapter
2708
2709
2710def set_committed_value(instance, key, value):
2711 """Set the value of an attribute with no history events.
2712
2713 Cancels any previous history present. The value should be
2714 a scalar value for scalar-holding attributes, or
2715 an iterable for any collection-holding attribute.
2716
2717 This is the same underlying method used when a lazy loader
2718 fires off and loads additional data from the database.
2719 In particular, this method can be used by application code
2720 which has loaded additional attributes or collections through
2721 separate queries, which can then be attached to an instance
2722 as though it were part of its original loaded state.
2723
2724 """
2725 state, dict_ = instance_state(instance), instance_dict(instance)
2726 state.manager[key].impl.set_committed_value(state, dict_, value)
2727
2728
2729def set_attribute(
2730 instance: object,
2731 key: str,
2732 value: Any,
2733 initiator: Optional[AttributeEventToken] = None,
2734) -> None:
2735 """Set the value of an attribute, firing history events.
2736
2737 This function may be used regardless of instrumentation
2738 applied directly to the class, i.e. no descriptors are required.
2739 Custom attribute management schemes will need to make usage
2740 of this method to establish attribute state as understood
2741 by SQLAlchemy.
2742
2743 :param instance: the object that will be modified
2744
2745 :param key: string name of the attribute
2746
2747 :param value: value to assign
2748
2749 :param initiator: an instance of :class:`.Event` that would have
2750 been propagated from a previous event listener. This argument
2751 is used when the :func:`.set_attribute` function is being used within
2752 an existing event listening function where an :class:`.Event` object
2753 is being supplied; the object may be used to track the origin of the
2754 chain of events.
2755
2756 .. versionadded:: 1.2.3
2757
2758 """
2759 state, dict_ = instance_state(instance), instance_dict(instance)
2760 state.manager[key].impl.set(state, dict_, value, initiator)
2761
2762
2763def get_attribute(instance: object, key: str) -> Any:
2764 """Get the value of an attribute, firing any callables required.
2765
2766 This function may be used regardless of instrumentation
2767 applied directly to the class, i.e. no descriptors are required.
2768 Custom attribute management schemes will need to make usage
2769 of this method to make usage of attribute state as understood
2770 by SQLAlchemy.
2771
2772 """
2773 state, dict_ = instance_state(instance), instance_dict(instance)
2774 return state.manager[key].impl.get(state, dict_)
2775
2776
2777def del_attribute(instance: object, key: str) -> None:
2778 """Delete the value of an attribute, firing history events.
2779
2780 This function may be used regardless of instrumentation
2781 applied directly to the class, i.e. no descriptors are required.
2782 Custom attribute management schemes will need to make usage
2783 of this method to establish attribute state as understood
2784 by SQLAlchemy.
2785
2786 """
2787 state, dict_ = instance_state(instance), instance_dict(instance)
2788 state.manager[key].impl.delete(state, dict_)
2789
2790
2791def flag_modified(instance: object, key: str) -> None:
2792 """Mark an attribute on an instance as 'modified'.
2793
2794 This sets the 'modified' flag on the instance and
2795 establishes an unconditional change event for the given attribute.
2796 The attribute must have a value present, else an
2797 :class:`.InvalidRequestError` is raised.
2798
2799 To mark an object "dirty" without referring to any specific attribute
2800 so that it is considered within a flush, use the
2801 :func:`.attributes.flag_dirty` call.
2802
2803 .. seealso::
2804
2805 :func:`.attributes.flag_dirty`
2806
2807 """
2808 state, dict_ = instance_state(instance), instance_dict(instance)
2809 impl = state.manager[key].impl
2810 impl.dispatch.modified(state, impl._modified_token)
2811 state._modified_event(dict_, impl, NO_VALUE, is_userland=True)
2812
2813
2814def flag_dirty(instance: object) -> None:
2815 """Mark an instance as 'dirty' without any specific attribute mentioned.
2816
2817 This is a special operation that will allow the object to travel through
2818 the flush process for interception by events such as
2819 :meth:`.SessionEvents.before_flush`. Note that no SQL will be emitted in
2820 the flush process for an object that has no changes, even if marked dirty
2821 via this method. However, a :meth:`.SessionEvents.before_flush` handler
2822 will be able to see the object in the :attr:`.Session.dirty` collection and
2823 may establish changes on it, which will then be included in the SQL
2824 emitted.
2825
2826 .. versionadded:: 1.2
2827
2828 .. seealso::
2829
2830 :func:`.attributes.flag_modified`
2831
2832 """
2833
2834 state, dict_ = instance_state(instance), instance_dict(instance)
2835 state._modified_event(dict_, None, NO_VALUE, is_userland=True)