codekingpro/portable-devtools
115k
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[Any]) -> 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
