codekingpro/portable-devtools
115k
1# orm/attributes.py
2# Copyright (C) 2005-2026 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 _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
466 return (self,)
467
468 def __getattr__(self, key: str) -> Any:
469 try:
470 return util.MemoizedSlots.__getattr__(self, key)
471 except AttributeError:
472 pass
473
474 try:
475 return getattr(self.comparator, key)
476 except AttributeError as err:
477 raise AttributeError(
478 "Neither %r object nor %r object associated with %s "
479 "has an attribute %r"
480 % (
481 type(self).__name__,
482 type(self.comparator).__name__,
483 self,
484 key,
485 )
486 ) from err
487
488 def __str__(self) -> str:
489 return f"{self.class_.__name__}.{self.key}"
490
491 def _memoized_attr_property(self) -> Optional[MapperProperty[Any]]:
492 return self.comparator.property
493
494
495def _queryable_attribute_unreduce(
496 key: str,
497 mapped_class: Type[_O],
498 parententity: _InternalEntityType[_O],
499 entity: _ExternalEntityType[Any],
500) -> Any:
501 # this method is only used in terms of the
502 # sqlalchemy.ext.serializer extension
503 if insp_is_aliased_class(parententity):
504 return entity._get_from_serialized(key, mapped_class, parententity)
505 else:
506 return getattr(entity, key)
507
508
509class InstrumentedAttribute(QueryableAttribute[_T_co]):
510 """Class bound instrumented attribute which adds basic
511 :term:`descriptor` methods.
512
513 See :class:`.QueryableAttribute` for a description of most features.
514
515
516 """
517
518 __slots__ = ()
519
520 inherit_cache = True
521 """:meta private:"""
522
523 # hack to make __doc__ writeable on instances of
524 # InstrumentedAttribute, while still keeping classlevel
525 # __doc__ correct
526
527 @util.rw_hybridproperty
528 def __doc__(self) -> Optional[str]:
529 return self._doc
530
531 @__doc__.setter # type: ignore
532 def __doc__(self, value: Optional[str]) -> None:
533 self._doc = value
534
535 @__doc__.classlevel # type: ignore
536 def __doc__(cls) -> Optional[str]:
537 return super().__doc__
538
539 def __set__(self, instance: object, value: Any) -> None:
540 self.impl.set(
541 instance_state(instance), instance_dict(instance), value, None
542 )
543
544 def __delete__(self, instance: object) -> None:
545 self.impl.delete(instance_state(instance), instance_dict(instance))
546
547 @overload
548 def __get__(
549 self, instance: None, owner: Any
550 ) -> InstrumentedAttribute[_T_co]: ...
551
552 @overload
553 def __get__(self, instance: object, owner: Any) -> _T_co: ...
554
555 def __get__(
556 self, instance: Optional[object], owner: Any
557 ) -> Union[InstrumentedAttribute[_T_co], _T_co]:
558 if instance is None:
559 return self
560
561 dict_ = instance_dict(instance)
562 if self.impl.supports_population and self.key in dict_:
563 return dict_[self.key] # type: ignore[no-any-return]
564 else:
565 try:
566 state = instance_state(instance)
567 except AttributeError as err:
568 raise orm_exc.UnmappedInstanceError(instance) from err
569 return self.impl.get(state, dict_) # type: ignore[no-any-return]
570
571
572@dataclasses.dataclass(frozen=True)
573class AdHocHasEntityNamespace(HasCacheKey):
574 _traverse_internals: ClassVar[_TraverseInternalsType] = [
575 ("_entity_namespace", InternalTraversal.dp_has_cache_key),
576 ]
577
578 # py37 compat, no slots=True on dataclass
579 __slots__ = ("_entity_namespace",)
580 _entity_namespace: _InternalEntityType[Any]
581 is_mapper: ClassVar[bool] = False
582 is_aliased_class: ClassVar[bool] = False
583
584 @property
585 def entity_namespace(self):
586 return self._entity_namespace.entity_namespace
587
588
589def create_proxied_attribute(
590 descriptor: Any,
591) -> Callable[..., QueryableAttribute[Any]]:
592 """Create an QueryableAttribute / user descriptor hybrid.
593
594 Returns a new QueryableAttribute type that delegates descriptor
595 behavior and getattr() to the given descriptor.
596 """
597
598 # TODO: can move this to descriptor_props if the need for this
599 # function is removed from ext/hybrid.py
600
601 class Proxy(QueryableAttribute[_T_co]):
602 """Presents the :class:`.QueryableAttribute` interface as a
603 proxy on top of a Python descriptor / :class:`.PropComparator`
604 combination.
605
606 """
607
608 _extra_criteria = ()
609
610 # the attribute error catches inside of __getattr__ basically create a
611 # singularity if you try putting slots on this too
612 # __slots__ = ("descriptor", "original_property", "_comparator")
613
614 def __init__(
615 self,
616 class_: _ExternalEntityType[Any],
617 key: str,
618 descriptor: Any,
619 comparator: interfaces.PropComparator[_T_co],
620 adapt_to_entity: Optional[AliasedInsp[Any]] = None,
621 doc: Optional[str] = None,
622 original_property: Optional[QueryableAttribute[_T_co]] = None,
623 ):
624 self.class_ = class_
625 self.key = key
626 self.descriptor = descriptor
627 self.original_property = original_property
628 self._comparator = comparator
629 self._adapt_to_entity = adapt_to_entity
630 self._doc = self.__doc__ = doc
631
632 @property
633 def _parententity(self): # type: ignore[override]
634 return inspection.inspect(self.class_, raiseerr=False)
635
636 @property
637 def parent(self): # type: ignore[override]
638 return inspection.inspect(self.class_, raiseerr=False)
639
640 _is_internal_proxy = True
641
642 _cache_key_traversal = [
643 ("key", visitors.ExtendedInternalTraversal.dp_string),
644 ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
645 ]
646
647 def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
648 prop = self.original_property
649 if prop is None:
650 return ()
651 else:
652 return prop._column_strategy_attrs()
653
654 @property
655 def _impl_uses_objects(self):
656 return (
657 self.original_property is not None
658 and getattr(self.class_, self.key).impl.uses_objects
659 )
660
661 @property
662 def _entity_namespace(self):
663 if hasattr(self._comparator, "_parententity"):
664 return self._comparator._parententity
665 else:
666 # used by hybrid attributes which try to remain
667 # agnostic of any ORM concepts like mappers
668 return AdHocHasEntityNamespace(self._parententity)
669
670 @property
671 def property(self):
672 return self.comparator.property
673
674 @util.memoized_property
675 def comparator(self):
676 if callable(self._comparator):
677 self._comparator = self._comparator()
678 if self._adapt_to_entity:
679 self._comparator = self._comparator.adapt_to_entity(
680 self._adapt_to_entity
681 )
682 return self._comparator
683
684 def adapt_to_entity(self, adapt_to_entity):
685 return self.__class__(
686 adapt_to_entity.entity,
687 self.key,
688 self.descriptor,
689 self._comparator,
690 adapt_to_entity,
691 )
692
693 def _clone(self, **kw):
694 return self.__class__(
695 self.class_,
696 self.key,
697 self.descriptor,
698 self._comparator,
699 adapt_to_entity=self._adapt_to_entity,
700 original_property=self.original_property,
701 )
702
703 def __get__(self, instance, owner):
704 retval = self.descriptor.__get__(instance, owner)
705 # detect if this is a plain Python @property, which just returns
706 # itself for class level access. If so, then return us.
707 # Otherwise, return the object returned by the descriptor.
708 if retval is self.descriptor and instance is None:
709 return self
710 else:
711 return retval
712
713 def __str__(self) -> str:
714 return f"{self.class_.__name__}.{self.key}"
715
716 def __getattr__(self, attribute):
717 """Delegate __getattr__ to the original descriptor and/or
718 comparator."""
719
720 # this is unfortunately very complicated, and is easily prone
721 # to recursion overflows when implementations of related
722 # __getattr__ schemes are changed
723
724 try:
725 return util.MemoizedSlots.__getattr__(self, attribute)
726 except AttributeError:
727 pass
728
729 try:
730 return getattr(descriptor, attribute)
731 except AttributeError as err:
732 if attribute == "comparator":
733 raise AttributeError("comparator") from err
734 try:
735 # comparator itself might be unreachable
736 comparator = self.comparator
737 except AttributeError as err2:
738 raise AttributeError(
739 "Neither %r object nor unconfigured comparator "
740 "object associated with %s has an attribute %r"
741 % (type(descriptor).__name__, self, attribute)
742 ) from err2
743 else:
744 try:
745 return getattr(comparator, attribute)
746 except AttributeError as err3:
747 raise AttributeError(
748 "Neither %r object nor %r object "
749 "associated with %s has an attribute %r"
750 % (
751 type(descriptor).__name__,
752 type(comparator).__name__,
753 self,
754 attribute,
755 )
756 ) from err3
757
758 Proxy.__name__ = type(descriptor).__name__ + "Proxy"
759
760 util.monkeypatch_proxied_specials(
761 Proxy, type(descriptor), name="descriptor", from_instance=descriptor
762 )
763 return Proxy
764
765
766OP_REMOVE = util.symbol("REMOVE")
767OP_APPEND = util.symbol("APPEND")
768OP_REPLACE = util.symbol("REPLACE")
769OP_BULK_REPLACE = util.symbol("BULK_REPLACE")
770OP_MODIFIED = util.symbol("MODIFIED")
771
772
773class AttributeEventToken:
774 """A token propagated throughout the course of a chain of attribute
775 events.
776
777 Serves as an indicator of the source of the event and also provides
778 a means of controlling propagation across a chain of attribute
779 operations.
780
781 The :class:`.Event` object is sent as the ``initiator`` argument
782 when dealing with events such as :meth:`.AttributeEvents.append`,
783 :meth:`.AttributeEvents.set`,
784 and :meth:`.AttributeEvents.remove`.
785
786 The :class:`.Event` object is currently interpreted by the backref
787 event handlers, and is used to control the propagation of operations
788 across two mutually-dependent attributes.
789
790 .. versionchanged:: 2.0 Changed the name from ``AttributeEvent``
791 to ``AttributeEventToken``.
792
793 :attribute impl: The :class:`.AttributeImpl` which is the current event
794 initiator.
795
796 :attribute op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE`,
797 :attr:`.OP_REPLACE`, or :attr:`.OP_BULK_REPLACE`, indicating the
798 source operation.
799
800 """
801
802 __slots__ = "impl", "op", "parent_token"
803
804 def __init__(self, attribute_impl: AttributeImpl, op: util.symbol):
805 self.impl = attribute_impl
806 self.op = op
807 self.parent_token = self.impl.parent_token
808
809 def __eq__(self, other):
810 return (
811 isinstance(other, AttributeEventToken)
812 and other.impl is self.impl
813 and other.op == self.op
814 )
815
816 @property
817 def key(self):
818 return self.impl.key
819
820 def hasparent(self, state):
821 return self.impl.hasparent(state)
822
823
824AttributeEvent = AttributeEventToken # legacy
825Event = AttributeEventToken # legacy
826
827
828class AttributeImpl:
829 """internal implementation for instrumented attributes."""
830
831 collection: bool
832 default_accepts_scalar_loader: bool
833 uses_objects: bool
834 supports_population: bool
835 dynamic: bool
836
837 _is_has_collection_adapter = False
838
839 _replace_token: AttributeEventToken
840 _remove_token: AttributeEventToken
841 _append_token: AttributeEventToken
842
843 def __init__(
844 self,
845 class_: _ExternalEntityType[_O],
846 key: str,
847 callable_: Optional[_LoaderCallable],
848 dispatch: _Dispatch[QueryableAttribute[Any]],
849 trackparent: bool = False,
850 compare_function: Optional[Callable[..., bool]] = None,
851 active_history: bool = False,
852 parent_token: Optional[AttributeEventToken] = None,
853 load_on_unexpire: bool = True,
854 send_modified_events: bool = True,
855 accepts_scalar_loader: Optional[bool] = None,
856 **kwargs: Any,
857 ):
858 r"""Construct an AttributeImpl.
859
860 :param \class_: associated class
861
862 :param key: string name of the attribute
863
864 :param \callable_:
865 optional function which generates a callable based on a parent
866 instance, which produces the "default" values for a scalar or
867 collection attribute when it's first accessed, if not present
868 already.
869
870 :param trackparent:
871 if True, attempt to track if an instance has a parent attached
872 to it via this attribute.
873
874 :param compare_function:
875 a function that compares two values which are normally
876 assignable to this attribute.
877
878 :param active_history:
879 indicates that get_history() should always return the "old" value,
880 even if it means executing a lazy callable upon attribute change.
881
882 :param parent_token:
883 Usually references the MapperProperty, used as a key for
884 the hasparent() function to identify an "owning" attribute.
885 Allows multiple AttributeImpls to all match a single
886 owner attribute.
887
888 :param load_on_unexpire:
889 if False, don't include this attribute in a load-on-expired
890 operation, i.e. the "expired_attribute_loader" process.
891 The attribute can still be in the "expired" list and be
892 considered to be "expired". Previously, this flag was called
893 "expire_missing" and is only used by a deferred column
894 attribute.
895
896 :param send_modified_events:
897 if False, the InstanceState._modified_event method will have no
898 effect; this means the attribute will never show up as changed in a
899 history entry.
900
901 """
902 self.class_ = class_
903 self.key = key
904 self.callable_ = callable_
905 self.dispatch = dispatch
906 self.trackparent = trackparent
907 self.parent_token = parent_token or self
908 self.send_modified_events = send_modified_events
909 if compare_function is None:
910 self.is_equal = operator.eq
911 else:
912 self.is_equal = compare_function
913
914 if accepts_scalar_loader is not None:
915 self.accepts_scalar_loader = accepts_scalar_loader
916 else:
917 self.accepts_scalar_loader = self.default_accepts_scalar_loader
918
919 _deferred_history = kwargs.pop("_deferred_history", False)
920 self._deferred_history = _deferred_history
921
922 if active_history:
923 self.dispatch._active_history = True
924
925 self.load_on_unexpire = load_on_unexpire
926 self._modified_token = AttributeEventToken(self, OP_MODIFIED)
927
928 __slots__ = (
929 "class_",
930 "key",
931 "callable_",
932 "dispatch",
933 "trackparent",
934 "parent_token",
935 "send_modified_events",
936 "is_equal",
937 "load_on_unexpire",
938 "_modified_token",
939 "accepts_scalar_loader",
940 "_deferred_history",
941 )
942
943 def __str__(self) -> str:
944 return f"{self.class_.__name__}.{self.key}"
945
946 def _get_active_history(self):
947 """Backwards compat for impl.active_history"""
948
949 return self.dispatch._active_history
950
951 def _set_active_history(self, value):
952 self.dispatch._active_history = value
953
954 active_history = property(_get_active_history, _set_active_history)
955
956 def hasparent(
957 self, state: InstanceState[Any], optimistic: bool = False
958 ) -> bool:
959 """Return the boolean value of a `hasparent` flag attached to
960 the given state.
961
962 The `optimistic` flag determines what the default return value
963 should be if no `hasparent` flag can be located.
964
965 As this function is used to determine if an instance is an
966 *orphan*, instances that were loaded from storage should be
967 assumed to not be orphans, until a True/False value for this
968 flag is set.
969
970 An instance attribute that is loaded by a callable function
971 will also not have a `hasparent` flag.
972
973 """
974 msg = "This AttributeImpl is not configured to track parents."
975 assert self.trackparent, msg
976
977 return (
978 state.parents.get(id(self.parent_token), optimistic) is not False
979 )
980
981 def sethasparent(
982 self,
983 state: InstanceState[Any],
984 parent_state: InstanceState[Any],
985 value: bool,
986 ) -> None:
987 """Set a boolean flag on the given item corresponding to
988 whether or not it is attached to a parent object via the
989 attribute represented by this ``InstrumentedAttribute``.
990
991 """
992 msg = "This AttributeImpl is not configured to track parents."
993 assert self.trackparent, msg
994
995 id_ = id(self.parent_token)
996 if value:
997 state.parents[id_] = parent_state
998 else:
999 if id_ in state.parents:
1000 last_parent = state.parents[id_]
1001
1002 if (
1003 last_parent is not False
1004 and last_parent.key != parent_state.key
1005 ):
1006 if last_parent.obj() is None:
1007 raise orm_exc.StaleDataError(
1008 "Removing state %s from parent "
1009 "state %s along attribute '%s', "
1010 "but the parent record "
1011 "has gone stale, can't be sure this "
1012 "is the most recent parent."
1013 % (
1014 state_str(state),
1015 state_str(parent_state),
1016 self.key,
1017 )
1018 )
1019
1020 return
1021
1022 state.parents[id_] = False
1023
1024 def get_history(
1025 self,
1026 state: InstanceState[Any],
1027 dict_: _InstanceDict,
1028 passive: PassiveFlag = PASSIVE_OFF,
1029 ) -> History:
1030 raise NotImplementedError()
1031
1032 def get_all_pending(
1033 self,
1034 state: InstanceState[Any],
1035 dict_: _InstanceDict,
1036 passive: PassiveFlag = PASSIVE_NO_INITIALIZE,
1037 ) -> _AllPendingType:
1038 """Return a list of tuples of (state, obj)
1039 for all objects in this attribute's current state
1040 + history.
1041
1042 Only applies to object-based attributes.
1043
1044 This is an inlining of existing functionality
1045 which roughly corresponds to:
1046
1047 get_state_history(
1048 state,
1049 key,
1050 passive=PASSIVE_NO_INITIALIZE).sum()
1051
1052 """
1053 raise NotImplementedError()
1054
1055 def _default_value(
1056 self, state: InstanceState[Any], dict_: _InstanceDict
1057 ) -> Any:
1058 """Produce an empty value for an uninitialized scalar attribute."""
1059
1060 assert self.key not in dict_, (
1061 "_default_value should only be invoked for an "
1062 "uninitialized or expired attribute"
1063 )
1064
1065 value = None
1066 for fn in self.dispatch.init_scalar:
1067 ret = fn(state, value, dict_)
1068 if ret is not ATTR_EMPTY:
1069 value = ret
1070
1071 return value
1072
1073 def get(
1074 self,
1075 state: InstanceState[Any],
1076 dict_: _InstanceDict,
1077 passive: PassiveFlag = PASSIVE_OFF,
1078 ) -> Any:
1079 """Retrieve a value from the given object.
1080 If a callable is assembled on this object's attribute, and
1081 passive is False, the callable will be executed and the
1082 resulting value will be set as the new value for this attribute.
1083 """
1084 if self.key in dict_:
1085 return dict_[self.key]
1086 else:
1087 # if history present, don't load
1088 key = self.key
1089 if (
1090 key not in state.committed_state
1091 or state.committed_state[key] is NO_VALUE
1092 ):
1093 if not passive & CALLABLES_OK:
1094 return PASSIVE_NO_RESULT
1095
1096 value = self._fire_loader_callables(state, key, passive)
1097
1098 if value is PASSIVE_NO_RESULT or value is NO_VALUE:
1099 return value
1100 elif value is ATTR_WAS_SET:
1101 try:
1102 return dict_[key]
1103 except KeyError as err:
1104 # TODO: no test coverage here.
1105 raise KeyError(
1106 "Deferred loader for attribute "
1107 "%r failed to populate "
1108 "correctly" % key
1109 ) from err
1110 elif value is not ATTR_EMPTY:
1111 return self.set_committed_value(state, dict_, value)
1112
1113 if not passive & INIT_OK:
1114 return NO_VALUE
1115 else:
1116 return self._default_value(state, dict_)
1117
1118 def _fire_loader_callables(
1119 self, state: InstanceState[Any], key: str, passive: PassiveFlag
1120 ) -> Any:
1121 if (
1122 self.accepts_scalar_loader
1123 and self.load_on_unexpire
1124 and key in state.expired_attributes
1125 ):
1126 return state._load_expired(state, passive)
1127 elif key in state.callables:
1128 callable_ = state.callables[key]
1129 return callable_(state, passive)
1130 elif self.callable_:
1131 return self.callable_(state, passive)
1132 else:
1133 return ATTR_EMPTY
1134
1135 def append(
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(state, dict_, value, initiator, passive=passive)
1144
1145 def remove(
1146 self,
1147 state: InstanceState[Any],
1148 dict_: _InstanceDict,
1149 value: Any,
1150 initiator: Optional[AttributeEventToken],
1151 passive: PassiveFlag = PASSIVE_OFF,
1152 ) -> None:
1153 self.set(
1154 state, dict_, None, initiator, passive=passive, check_old=value
1155 )
1156
1157 def pop(
1158 self,
1159 state: InstanceState[Any],
1160 dict_: _InstanceDict,
1161 value: Any,
1162 initiator: Optional[AttributeEventToken],
1163 passive: PassiveFlag = PASSIVE_OFF,
1164 ) -> None:
1165 self.set(
1166 state,
1167 dict_,
1168 None,
1169 initiator,
1170 passive=passive,
1171 check_old=value,
1172 pop=True,
1173 )
1174
1175 def set(
1176 self,
1177 state: InstanceState[Any],
1178 dict_: _InstanceDict,
1179 value: Any,
1180 initiator: Optional[AttributeEventToken] = None,
1181 passive: PassiveFlag = PASSIVE_OFF,
1182 check_old: Any = None,
1183 pop: bool = False,
1184 ) -> None:
1185 raise NotImplementedError()
1186
1187 def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None:
1188 raise NotImplementedError()
1189
1190 def get_committed_value(
1191 self,
1192 state: InstanceState[Any],
1193 dict_: _InstanceDict,
1194 passive: PassiveFlag = PASSIVE_OFF,
1195 ) -> Any:
1196 """return the unchanged value of this attribute"""
1197
1198 if self.key in state.committed_state:
1199 value = state.committed_state[self.key]
1200 if value is NO_VALUE:
