codekingpro/portable-devtools
114k
1# orm/state.py
2# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8"""Defines instrumentation of instances.
9
10This module is usually not directly visible to user applications, but
11defines a large part of the ORM's interactivity.
12
13"""
14
15from __future__ import annotations
16
17from typing import Any
18from typing import Callable
19from typing import Dict
20from typing import Generic
21from typing import Iterable
22from typing import Optional
23from typing import Set
24from typing import Tuple
25from typing import TYPE_CHECKING
26from typing import Union
27import weakref
28
29from . import base
30from . import exc as orm_exc
31from . import interfaces
32from ._typing import _O
33from ._typing import is_collection_impl
34from .base import ATTR_WAS_SET
35from .base import INIT_OK
36from .base import LoaderCallableStatus
37from .base import NEVER_SET
38from .base import NO_VALUE
39from .base import PASSIVE_NO_INITIALIZE
40from .base import PASSIVE_NO_RESULT
41from .base import PASSIVE_OFF
42from .base import SQL_OK
43from .path_registry import PathRegistry
44from .. import exc as sa_exc
45from .. import inspection
46from .. import util
47from ..util.typing import Literal
48from ..util.typing import Protocol
49
50if TYPE_CHECKING:
51 from ._typing import _IdentityKeyType
52 from ._typing import _InstanceDict
53 from ._typing import _LoaderCallable
54 from .attributes import AttributeImpl
55 from .attributes import History
56 from .base import PassiveFlag
57 from .collections import _AdaptedCollectionProtocol
58 from .identity import IdentityMap
59 from .instrumentation import ClassManager
60 from .interfaces import ORMOption
61 from .mapper import Mapper
62 from .session import Session
63 from ..engine import Row
64 from ..ext.asyncio.session import async_session as _async_provider
65 from ..ext.asyncio.session import AsyncSession
66
67if TYPE_CHECKING:
68 _sessions: weakref.WeakValueDictionary[int, Session]
69else:
70 # late-populated by session.py
71 _sessions = None
72
73
74if not TYPE_CHECKING:
75 # optionally late-provided by sqlalchemy.ext.asyncio.session
76
77 _async_provider = None # noqa
78
79
80class _InstanceDictProto(Protocol):
81 def __call__(self) -> Optional[IdentityMap]: ...
82
83
84class _InstallLoaderCallableProto(Protocol[_O]):
85 """used at result loading time to install a _LoaderCallable callable
86 upon a specific InstanceState, which will be used to populate an
87 attribute when that attribute is accessed.
88
89 Concrete examples are per-instance deferred column loaders and
90 relationship lazy loaders.
91
92 """
93
94 def __call__(
95 self, state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any]
96 ) -> None: ...
97
98
99@inspection._self_inspects
100class InstanceState(interfaces.InspectionAttrInfo, Generic[_O]):
101 """tracks state information at the instance level.
102
103 The :class:`.InstanceState` is a key object used by the
104 SQLAlchemy ORM in order to track the state of an object;
105 it is created the moment an object is instantiated, typically
106 as a result of :term:`instrumentation` which SQLAlchemy applies
107 to the ``__init__()`` method of the class.
108
109 :class:`.InstanceState` is also a semi-public object,
110 available for runtime inspection as to the state of a
111 mapped instance, including information such as its current
112 status within a particular :class:`.Session` and details
113 about data on individual attributes. The public API
114 in order to acquire a :class:`.InstanceState` object
115 is to use the :func:`_sa.inspect` system::
116
117 >>> from sqlalchemy import inspect
118 >>> insp = inspect(some_mapped_object)
119 >>> insp.attrs.nickname.history
120 History(added=['new nickname'], unchanged=(), deleted=['nickname'])
121
122 .. seealso::
123
124 :ref:`orm_mapper_inspection_instancestate`
125
126 """
127
128 __slots__ = (
129 "__dict__",
130 "__weakref__",
131 "class_",
132 "manager",
133 "obj",
134 "committed_state",
135 "expired_attributes",
136 )
137
138 manager: ClassManager[_O]
139 session_id: Optional[int] = None
140 key: Optional[_IdentityKeyType[_O]] = None
141 runid: Optional[int] = None
142 load_options: Tuple[ORMOption, ...] = ()
143 load_path: PathRegistry = PathRegistry.root
144 insert_order: Optional[int] = None
145 _strong_obj: Optional[object] = None
146 obj: weakref.ref[_O]
147
148 committed_state: Dict[str, Any]
149
150 modified: bool = False
151 expired: bool = False
152 _deleted: bool = False
153 _load_pending: bool = False
154 _orphaned_outside_of_session: bool = False
155 is_instance: bool = True
156 identity_token: object = None
157 _last_known_values: Optional[Dict[str, Any]] = None
158
159 _instance_dict: _InstanceDictProto
160 """A weak reference, or in the default case a plain callable, that
161 returns a reference to the current :class:`.IdentityMap`, if any.
162
163 """
164 if not TYPE_CHECKING:
165
166 def _instance_dict(self):
167 """default 'weak reference' for _instance_dict"""
168 return None
169
170 expired_attributes: Set[str]
171 """The set of keys which are 'expired' to be loaded by
172 the manager's deferred scalar loader, assuming no pending
173 changes.
174
175 see also the ``unmodified`` collection which is intersected
176 against this set when a refresh operation occurs."""
177
178 callables: Dict[str, Callable[[InstanceState[_O], PassiveFlag], Any]]
179 """A namespace where a per-state loader callable can be associated.
180
181 In SQLAlchemy 1.0, this is only used for lazy loaders / deferred
182 loaders that were set up via query option.
183
184 Previously, callables was used also to indicate expired attributes
185 by storing a link to the InstanceState itself in this dictionary.
186 This role is now handled by the expired_attributes set.
187
188 """
189
190 if not TYPE_CHECKING:
191 callables = util.EMPTY_DICT
192
193 def __init__(self, obj: _O, manager: ClassManager[_O]):
194 self.class_ = obj.__class__
195 self.manager = manager
196 self.obj = weakref.ref(obj, self._cleanup)
197 self.committed_state = {}
198 self.expired_attributes = set()
199
200 @util.memoized_property
201 def attrs(self) -> util.ReadOnlyProperties[AttributeState]:
202 """Return a namespace representing each attribute on
203 the mapped object, including its current value
204 and history.
205
206 The returned object is an instance of :class:`.AttributeState`.
207 This object allows inspection of the current data
208 within an attribute as well as attribute history
209 since the last flush.
210
211 """
212 return util.ReadOnlyProperties(
213 {key: AttributeState(self, key) for key in self.manager}
214 )
215
216 @property
217 def transient(self) -> bool:
218 """Return ``True`` if the object is :term:`transient`.
219
220 .. seealso::
221
222 :ref:`session_object_states`
223
224 """
225 return self.key is None and not self._attached
226
227 @property
228 def pending(self) -> bool:
229 """Return ``True`` if the object is :term:`pending`.
230
231
232 .. seealso::
233
234 :ref:`session_object_states`
235
236 """
237 return self.key is None and self._attached
238
239 @property
240 def deleted(self) -> bool:
241 """Return ``True`` if the object is :term:`deleted`.
242
243 An object that is in the deleted state is guaranteed to
244 not be within the :attr:`.Session.identity_map` of its parent
245 :class:`.Session`; however if the session's transaction is rolled
246 back, the object will be restored to the persistent state and
247 the identity map.
248
249 .. note::
250
251 The :attr:`.InstanceState.deleted` attribute refers to a specific
252 state of the object that occurs between the "persistent" and
253 "detached" states; once the object is :term:`detached`, the
254 :attr:`.InstanceState.deleted` attribute **no longer returns
255 True**; in order to detect that a state was deleted, regardless
256 of whether or not the object is associated with a
257 :class:`.Session`, use the :attr:`.InstanceState.was_deleted`
258 accessor.
259
260 .. versionadded: 1.1
261
262 .. seealso::
263
264 :ref:`session_object_states`
265
266 """
267 return self.key is not None and self._attached and self._deleted
268
269 @property
270 def was_deleted(self) -> bool:
271 """Return True if this object is or was previously in the
272 "deleted" state and has not been reverted to persistent.
273
274 This flag returns True once the object was deleted in flush.
275 When the object is expunged from the session either explicitly
276 or via transaction commit and enters the "detached" state,
277 this flag will continue to report True.
278
279 .. seealso::
280
281 :attr:`.InstanceState.deleted` - refers to the "deleted" state
282
283 :func:`.orm.util.was_deleted` - standalone function
284
285 :ref:`session_object_states`
286
287 """
288 return self._deleted
289
290 @property
291 def persistent(self) -> bool:
292 """Return ``True`` if the object is :term:`persistent`.
293
294 An object that is in the persistent state is guaranteed to
295 be within the :attr:`.Session.identity_map` of its parent
296 :class:`.Session`.
297
298 .. seealso::
299
300 :ref:`session_object_states`
301
302 """
303 return self.key is not None and self._attached and not self._deleted
304
305 @property
306 def detached(self) -> bool:
307 """Return ``True`` if the object is :term:`detached`.
308
309 .. seealso::
310
311 :ref:`session_object_states`
312
313 """
314 return self.key is not None and not self._attached
315
316 @util.non_memoized_property
317 @util.preload_module("sqlalchemy.orm.session")
318 def _attached(self) -> bool:
319 return (
320 self.session_id is not None
321 and self.session_id in util.preloaded.orm_session._sessions
322 )
323
324 def _track_last_known_value(self, key: str) -> None:
325 """Track the last known value of a particular key after expiration
326 operations.
327
328 .. versionadded:: 1.3
329
330 """
331
332 lkv = self._last_known_values
333 if lkv is None:
334 self._last_known_values = lkv = {}
335 if key not in lkv:
336 lkv[key] = NO_VALUE
337
338 @property
339 def session(self) -> Optional[Session]:
340 """Return the owning :class:`.Session` for this instance,
341 or ``None`` if none available.
342
343 Note that the result here can in some cases be *different*
344 from that of ``obj in session``; an object that's been deleted
345 will report as not ``in session``, however if the transaction is
346 still in progress, this attribute will still refer to that session.
347 Only when the transaction is completed does the object become
348 fully detached under normal circumstances.
349
350 .. seealso::
351
352 :attr:`_orm.InstanceState.async_session`
353
354 """
355 if self.session_id:
356 try:
357 return _sessions[self.session_id]
358 except KeyError:
359 pass
360 return None
361
362 @property
363 def async_session(self) -> Optional[AsyncSession]:
364 """Return the owning :class:`_asyncio.AsyncSession` for this instance,
365 or ``None`` if none available.
366
367 This attribute is only non-None when the :mod:`sqlalchemy.ext.asyncio`
368 API is in use for this ORM object. The returned
369 :class:`_asyncio.AsyncSession` object will be a proxy for the
370 :class:`_orm.Session` object that would be returned from the
371 :attr:`_orm.InstanceState.session` attribute for this
372 :class:`_orm.InstanceState`.
373
374 .. versionadded:: 1.4.18
375
376 .. seealso::
377
378 :ref:`asyncio_toplevel`
379
380 """
381 if _async_provider is None:
382 return None
383
384 sess = self.session
385 if sess is not None:
386 return _async_provider(sess)
387 else:
388 return None
389
390 @property
391 def object(self) -> Optional[_O]:
392 """Return the mapped object represented by this
393 :class:`.InstanceState`.
394
395 Returns None if the object has been garbage collected
396
397 """
398 return self.obj()
399
400 @property
401 def identity(self) -> Optional[Tuple[Any, ...]]:
402 """Return the mapped identity of the mapped object.
403 This is the primary key identity as persisted by the ORM
404 which can always be passed directly to
405 :meth:`_query.Query.get`.
406
407 Returns ``None`` if the object has no primary key identity.
408
409 .. note::
410 An object which is :term:`transient` or :term:`pending`
411 does **not** have a mapped identity until it is flushed,
412 even if its attributes include primary key values.
413
414 """
415 if self.key is None:
416 return None
417 else:
418 return self.key[1]
419
420 @property
421 def identity_key(self) -> Optional[_IdentityKeyType[_O]]:
422 """Return the identity key for the mapped object.
423
424 This is the key used to locate the object within
425 the :attr:`.Session.identity_map` mapping. It contains
426 the identity as returned by :attr:`.identity` within it.
427
428
429 """
430 return self.key
431
432 @util.memoized_property
433 def parents(self) -> Dict[int, Union[Literal[False], InstanceState[Any]]]:
434 return {}
435
436 @util.memoized_property
437 def _pending_mutations(self) -> Dict[str, PendingCollection]:
438 return {}
439
440 @util.memoized_property
441 def _empty_collections(self) -> Dict[str, _AdaptedCollectionProtocol]:
442 return {}
443
444 @util.memoized_property
445 def mapper(self) -> Mapper[_O]:
446 """Return the :class:`_orm.Mapper` used for this mapped object."""
447 return self.manager.mapper
448
449 @property
450 def has_identity(self) -> bool:
451 """Return ``True`` if this object has an identity key.
452
453 This should always have the same value as the
454 expression ``state.persistent`` or ``state.detached``.
455
456 """
457 return bool(self.key)
458
459 @classmethod
460 def _detach_states(
461 self,
462 states: Iterable[InstanceState[_O]],
463 session: Session,
464 to_transient: bool = False,
465 ) -> None:
466 persistent_to_detached = (
467 session.dispatch.persistent_to_detached or None
468 )
469 deleted_to_detached = session.dispatch.deleted_to_detached or None
470 pending_to_transient = session.dispatch.pending_to_transient or None
471 persistent_to_transient = (
472 session.dispatch.persistent_to_transient or None
473 )
474
475 for state in states:
476 deleted = state._deleted
477 pending = state.key is None
478 persistent = not pending and not deleted
479
480 state.session_id = None
481
482 if to_transient and state.key:
483 del state.key
484 if persistent:
485 if to_transient:
486 if persistent_to_transient is not None:
487 persistent_to_transient(session, state)
488 elif persistent_to_detached is not None:
489 persistent_to_detached(session, state)
490 elif deleted and deleted_to_detached is not None:
491 deleted_to_detached(session, state)
492 elif pending and pending_to_transient is not None:
493 pending_to_transient(session, state)
494
495 state._strong_obj = None
496
497 def _detach(self, session: Optional[Session] = None) -> None:
498 if session:
499 InstanceState._detach_states([self], session)
500 else:
501 self.session_id = self._strong_obj = None
502
503 def _dispose(self) -> None:
504 # used by the test suite, apparently
505 self._detach()
506
507 def _cleanup(self, ref: weakref.ref[_O]) -> None:
508 """Weakref callback cleanup.
509
510 This callable cleans out the state when it is being garbage
511 collected.
512
513 this _cleanup **assumes** that there are no strong refs to us!
514 Will not work otherwise!
515
516 """
517
518 # Python builtins become undefined during interpreter shutdown.
519 # Guard against exceptions during this phase, as the method cannot
520 # proceed in any case if builtins have been undefined.
521 if dict is None:
522 return
523
524 instance_dict = self._instance_dict()
525 if instance_dict is not None:
526 instance_dict._fast_discard(self)
527 del self._instance_dict
528
529 # we can't possibly be in instance_dict._modified
530 # b.c. this is weakref cleanup only, that set
531 # is strong referencing!
532 # assert self not in instance_dict._modified
533
534 self.session_id = self._strong_obj = None
535
536 @property
537 def dict(self) -> _InstanceDict:
538 """Return the instance dict used by the object.
539
540 Under normal circumstances, this is always synonymous
541 with the ``__dict__`` attribute of the mapped object,
542 unless an alternative instrumentation system has been
543 configured.
544
545 In the case that the actual object has been garbage
546 collected, this accessor returns a blank dictionary.
547
548 """
549 o = self.obj()
550 if o is not None:
551 return base.instance_dict(o)
552 else:
553 return {}
554
555 def _initialize_instance(*mixed: Any, **kwargs: Any) -> None:
556 self, instance, args = mixed[0], mixed[1], mixed[2:] # noqa
557 manager = self.manager
558
559 manager.dispatch.init(self, args, kwargs)
560
561 try:
562 manager.original_init(*mixed[1:], **kwargs)
563 except:
564 with util.safe_reraise():
565 manager.dispatch.init_failure(self, args, kwargs)
566
567 def get_history(self, key: str, passive: PassiveFlag) -> History:
568 return self.manager[key].impl.get_history(self, self.dict, passive)
569
570 def get_impl(self, key: str) -> AttributeImpl:
571 return self.manager[key].impl
572
573 def _get_pending_mutation(self, key: str) -> PendingCollection:
574 if key not in self._pending_mutations:
575 self._pending_mutations[key] = PendingCollection()
576 return self._pending_mutations[key]
577
578 def __getstate__(self) -> Dict[str, Any]:
579 state_dict: Dict[str, Any] = {
580 "instance": self.obj(),
581 "class_": self.class_,
582 "committed_state": self.committed_state,
583 "expired_attributes": self.expired_attributes,
584 }
585 state_dict.update(
586 (k, self.__dict__[k])
587 for k in (
588 "_pending_mutations",
589 "modified",
590 "expired",
591 "callables",
592 "key",
593 "parents",
594 "load_options",
595 "class_",
596 "expired_attributes",
597 "info",
598 )
599 if k in self.__dict__
600 )
601 if self.load_path:
602 state_dict["load_path"] = self.load_path.serialize()
603
604 state_dict["manager"] = self.manager._serialize(self, state_dict)
605
606 return state_dict
607
608 def __setstate__(self, state_dict: Dict[str, Any]) -> None:
609 inst = state_dict["instance"]
610 if inst is not None:
611 self.obj = weakref.ref(inst, self._cleanup)
612 self.class_ = inst.__class__
613 else:
614 self.obj = lambda: None # type: ignore
615 self.class_ = state_dict["class_"]
616
617 self.committed_state = state_dict.get("committed_state", {})
618 self._pending_mutations = state_dict.get("_pending_mutations", {})
619 self.parents = state_dict.get("parents", {})
620 self.modified = state_dict.get("modified", False)
621 self.expired = state_dict.get("expired", False)
622 if "info" in state_dict:
623 self.info.update(state_dict["info"])
624 if "callables" in state_dict:
625 self.callables = state_dict["callables"]
626
627 self.expired_attributes = state_dict["expired_attributes"]
628 else:
629 if "expired_attributes" in state_dict:
630 self.expired_attributes = state_dict["expired_attributes"]
631 else:
632 self.expired_attributes = set()
633
634 self.__dict__.update(
635 [
636 (k, state_dict[k])
637 for k in ("key", "load_options")
638 if k in state_dict
639 ]
640 )
641 if self.key:
642 self.identity_token = self.key[2]
643
644 if "load_path" in state_dict:
645 self.load_path = PathRegistry.deserialize(state_dict["load_path"])
646
647 state_dict["manager"](self, inst, state_dict)
648
649 def _reset(self, dict_: _InstanceDict, key: str) -> None:
650 """Remove the given attribute and any
651 callables associated with it."""
652
653 old = dict_.pop(key, None)
654 manager_impl = self.manager[key].impl
655 if old is not None and is_collection_impl(manager_impl):
656 manager_impl._invalidate_collection(old)
657 self.expired_attributes.discard(key)
658 if self.callables:
659 self.callables.pop(key, None)
660
661 def _copy_callables(self, from_: InstanceState[Any]) -> None:
662 if "callables" in from_.__dict__:
663 self.callables = dict(from_.callables)
664
665 @classmethod
666 def _instance_level_callable_processor(
667 cls, manager: ClassManager[_O], fn: _LoaderCallable, key: Any
668 ) -> _InstallLoaderCallableProto[_O]:
669 impl = manager[key].impl
670 if is_collection_impl(impl):
671 fixed_impl = impl
672
673 def _set_callable(
674 state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any]
675 ) -> None:
676 if "callables" not in state.__dict__:
677 state.callables = {}
678 old = dict_.pop(key, None)
679 if old is not None:
680 fixed_impl._invalidate_collection(old)
681 state.callables[key] = fn
682
683 else:
684
685 def _set_callable(
686 state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any]
687 ) -> None:
688 if "callables" not in state.__dict__:
689 state.callables = {}
690 state.callables[key] = fn
691
692 return _set_callable
693
694 def _expire(
695 self, dict_: _InstanceDict, modified_set: Set[InstanceState[Any]]
696 ) -> None:
697 self.expired = True
698 if self.modified:
699 modified_set.discard(self)
700 self.committed_state.clear()
701 self.modified = False
702
703 self._strong_obj = None
704
705 if "_pending_mutations" in self.__dict__:
706 del self.__dict__["_pending_mutations"]
707
708 if "parents" in self.__dict__:
709 del self.__dict__["parents"]
710
711 self.expired_attributes.update(
712 [impl.key for impl in self.manager._loader_impls]
713 )
714
715 if self.callables:
716 # the per state loader callables we can remove here are
717 # LoadDeferredColumns, which undefers a column at the instance
718 # level that is mapped with deferred, and LoadLazyAttribute,
719 # which lazy loads a relationship at the instance level that
720 # is mapped with "noload" or perhaps "immediateload".
721 # Before 1.4, only column-based
722 # attributes could be considered to be "expired", so here they
723 # were the only ones "unexpired", which means to make them deferred
724 # again. For the moment, as of 1.4 we also apply the same
725 # treatment relationships now, that is, an instance level lazy
726 # loader is reset in the same way as a column loader.
727 for k in self.expired_attributes.intersection(self.callables):
728 del self.callables[k]
729
730 for k in self.manager._collection_impl_keys.intersection(dict_):
731 collection = dict_.pop(k)
732 collection._sa_adapter.invalidated = True
733
734 if self._last_known_values:
735 self._last_known_values.update(
736 {k: dict_[k] for k in self._last_known_values if k in dict_}
737 )
738
739 for key in self.manager._all_key_set.intersection(dict_):
740 del dict_[key]
741
742 self.manager.dispatch.expire(self, None)
743
744 def _expire_attributes(
745 self,
746 dict_: _InstanceDict,
747 attribute_names: Iterable[str],
748 no_loader: bool = False,
749 ) -> None:
750 pending = self.__dict__.get("_pending_mutations", None)
751
752 callables = self.callables
753
754 for key in attribute_names:
755 impl = self.manager[key].impl
756 if impl.accepts_scalar_loader:
757 if no_loader and (impl.callable_ or key in callables):
758 continue
759
760 self.expired_attributes.add(key)
761 if callables and key in callables:
762 del callables[key]
763 old = dict_.pop(key, NO_VALUE)
764 if is_collection_impl(impl) and old is not NO_VALUE:
765 impl._invalidate_collection(old)
766
767 lkv = self._last_known_values
768 if lkv is not None and key in lkv and old is not NO_VALUE:
769 lkv[key] = old
770
771 self.committed_state.pop(key, None)
772 if pending:
773 pending.pop(key, None)
774
775 self.manager.dispatch.expire(self, attribute_names)
776
777 def _load_expired(
778 self, state: InstanceState[_O], passive: PassiveFlag
779 ) -> LoaderCallableStatus:
780 """__call__ allows the InstanceState to act as a deferred
781 callable for loading expired attributes, which is also
782 serializable (picklable).
783
784 """
785
786 if not passive & SQL_OK:
787 return PASSIVE_NO_RESULT
788
789 toload = self.expired_attributes.intersection(self.unmodified)
790 toload = toload.difference(
791 attr
792 for attr in toload
793 if not self.manager[attr].impl.load_on_unexpire
794 )
795
796 self.manager.expired_attribute_loader(self, toload, passive)
797
798 # if the loader failed, or this
799 # instance state didn't have an identity,
800 # the attributes still might be in the callables
801 # dict. ensure they are removed.
802 self.expired_attributes.clear()
803
804 return ATTR_WAS_SET
805
806 @property
807 def unmodified(self) -> Set[str]:
808 """Return the set of keys which have no uncommitted changes"""
809
810 return set(self.manager).difference(self.committed_state)
811
812 def unmodified_intersection(self, keys: Iterable[str]) -> Set[str]:
813 """Return self.unmodified.intersection(keys)."""
814
815 return (
816 set(keys)
817 .intersection(self.manager)
818 .difference(self.committed_state)
819 )
820
821 @property
822 def unloaded(self) -> Set[str]:
823 """Return the set of keys which do not have a loaded value.
824
825 This includes expired attributes and any other attribute that was never
826 populated or modified.
827
828 """
829 return (
830 set(self.manager)
831 .difference(self.committed_state)
832 .difference(self.dict)
833 )
834
835 @property
836 @util.deprecated(
837 "2.0",
838 "The :attr:`.InstanceState.unloaded_expirable` attribute is "
839 "deprecated. Please use :attr:`.InstanceState.unloaded`.",
840 )
841 def unloaded_expirable(self) -> Set[str]:
842 """Synonymous with :attr:`.InstanceState.unloaded`.
843
844 This attribute was added as an implementation-specific detail at some
845 point and should be considered to be private.
846
847 """
848 return self.unloaded
849
850 @property
851 def _unloaded_non_object(self) -> Set[str]:
852 return self.unloaded.intersection(
853 attr
854 for attr in self.manager
855 if self.manager[attr].impl.accepts_scalar_loader
856 )
857
858 def _modified_event(
859 self,
860 dict_: _InstanceDict,
861 attr: Optional[AttributeImpl],
862 previous: Any,
863 collection: bool = False,
864 is_userland: bool = False,
865 ) -> None:
866 if attr:
867 if not attr.send_modified_events:
868 return
869 if is_userland and attr.key not in dict_:
870 raise sa_exc.InvalidRequestError(
871 "Can't flag attribute '%s' modified; it's not present in "
872 "the object state" % attr.key
873 )
874 if attr.key not in self.committed_state or is_userland:
875 if collection:
876 if TYPE_CHECKING:
877 assert is_collection_impl(attr)
878 if previous is NEVER_SET:
879 if attr.key in dict_:
880 previous = dict_[attr.key]
881
882 if previous not in (None, NO_VALUE, NEVER_SET):
883 previous = attr.copy(previous)
884 self.committed_state[attr.key] = previous
885
886 lkv = self._last_known_values
887 if lkv is not None and attr.key in lkv:
888 lkv[attr.key] = NO_VALUE
889
890 # assert self._strong_obj is None or self.modified
891
892 if (self.session_id and self._strong_obj is None) or not self.modified:
893 self.modified = True
894 instance_dict = self._instance_dict()
895 if instance_dict:
896 has_modified = bool(instance_dict._modified)
897 instance_dict._modified.add(self)
898 else:
899 has_modified = False
900
901 # only create _strong_obj link if attached
902 # to a session
903
904 inst = self.obj()
905 if self.session_id:
906 self._strong_obj = inst
907
908 # if identity map already had modified objects,
909 # assume autobegin already occurred, else check
910 # for autobegin
911 if not has_modified:
912 # inline of autobegin, to ensure session transaction
913 # snapshot is established
914 try:
915 session = _sessions[self.session_id]
916 except KeyError:
917 pass
918 else:
919 if session._transaction is None:
920 session._autobegin_t()
921
922 if inst is None and attr:
923 raise orm_exc.ObjectDereferencedError(
924 "Can't emit change event for attribute '%s' - "
925 "parent object of type %s has been garbage "
926 "collected."
927 % (self.manager[attr.key], base.state_class_str(self))
928 )
929
930 def _commit(self, dict_: _InstanceDict, keys: Iterable[str]) -> None:
931 """Commit attributes.
932
933 This is used by a partial-attribute load operation to mark committed
934 those attributes which were refreshed from the database.
935
936 Attributes marked as "expired" can potentially remain "expired" after
937 this step if a value was not populated in state.dict.
938
939 """
940 for key in keys:
941 self.committed_state.pop(key, None)
942
943 self.expired = False
944
945 self.expired_attributes.difference_update(
946 set(keys).intersection(dict_)
947 )
948
949 # the per-keys commit removes object-level callables,
950 # while that of commit_all does not. it's not clear
951 # if this behavior has a clear rationale, however tests do
952 # ensure this is what it does.
953 if self.callables:
954 for key in (
955 set(self.callables).intersection(keys).intersection(dict_)
956 ):
957 del self.callables[key]
958
959 def _commit_all(
960 self, dict_: _InstanceDict, instance_dict: Optional[IdentityMap] = None
961 ) -> None:
962 """commit all attributes unconditionally.
963
964 This is used after a flush() or a full load/refresh
965 to remove all pending state from the instance.
966
967 - all attributes are marked as "committed"
968 - the "strong dirty reference" is removed
969 - the "modified" flag is set to False
970 - any "expired" markers for scalar attributes loaded are removed.
971 - lazy load callables for objects / collections *stay*
972
973 Attributes marked as "expired" can potentially remain
974 "expired" after this step if a value was not populated in state.dict.
975
976 """
977 self._commit_all_states([(self, dict_)], instance_dict)
978
979 @classmethod
980 def _commit_all_states(
981 self,
982 iter_: Iterable[Tuple[InstanceState[Any], _InstanceDict]],
983 instance_dict: Optional[IdentityMap] = None,
984 ) -> None:
985 """Mass / highly inlined version of commit_all()."""
986
987 for state, dict_ in iter_:
988 state_dict = state.__dict__
989
990 state.committed_state.clear()
991
992 if "_pending_mutations" in state_dict:
993 del state_dict["_pending_mutations"]
994
995 state.expired_attributes.difference_update(dict_)
996
997 if instance_dict and state.modified:
998 instance_dict._modified.discard(state)
999
1000 state.modified = state.expired = False
1001 state._strong_obj = None
1002
1003
1004class AttributeState:
1005 """Provide an inspection interface corresponding
1006 to a particular attribute on a particular mapped object.
1007
1008 The :class:`.AttributeState` object is accessed
1009 via the :attr:`.InstanceState.attrs` collection
1010 of a particular :class:`.InstanceState`::
1011
1012 from sqlalchemy import inspect
1013
1014 insp = inspect(some_mapped_object)
1015 attr_state = insp.attrs.some_attribute
1016
1017 """
1018
1019 __slots__ = ("state", "key")
1020
1021 state: InstanceState[Any]
1022 key: str
1023
1024 def __init__(self, state: InstanceState[Any], key: str):
1025 self.state = state
1026 self.key = key
1027
1028 @property
1029 def loaded_value(self) -> Any:
1030 """The current value of this attribute as loaded from the database.
1031
1032 If the value has not been loaded, or is otherwise not present
1033 in the object's dictionary, returns NO_VALUE.
1034
1035 """
1036 return self.state.dict.get(self.key, NO_VALUE)
1037
1038 @property
1039 def value(self) -> Any:
1040 """Return the value of this attribute.
1041
1042 This operation is equivalent to accessing the object's
1043 attribute directly or via ``getattr()``, and will fire
1044 off any pending loader callables if needed.
1045
1046 """
1047 return self.state.manager[self.key].__get__(
1048 self.state.obj(), self.state.class_
1049 )
1050
1051 @property
1052 def history(self) -> History:
1053 """Return the current **pre-flush** change history for
1054 this attribute, via the :class:`.History` interface.
1055
1056 This method will **not** emit loader callables if the value of the
1057 attribute is unloaded.
1058
1059 .. note::
1060
1061 The attribute history system tracks changes on a **per flush
1062 basis**. Each time the :class:`.Session` is flushed, the history
1063 of each attribute is reset to empty. The :class:`.Session` by
1064 default autoflushes each time a :class:`_query.Query` is invoked.
1065 For
1066 options on how to control this, see :ref:`session_flushing`.
1067
1068
1069 .. seealso::
1070
1071 :meth:`.AttributeState.load_history` - retrieve history
1072 using loader callables if the value is not locally present.
1073
1074 :func:`.attributes.get_history` - underlying function
1075
1076 """
1077 return self.state.get_history(self.key, PASSIVE_NO_INITIALIZE)
1078
1079 def load_history(self) -> History:
1080 """Return the current **pre-flush** change history for
1081 this attribute, via the :class:`.History` interface.
1082
1083 This method **will** emit loader callables if the value of the
1084 attribute is unloaded.
1085
1086 .. note::
1087
1088 The attribute history system tracks changes on a **per flush
1089 basis**. Each time the :class:`.Session` is flushed, the history
1090 of each attribute is reset to empty. The :class:`.Session` by
1091 default autoflushes each time a :class:`_query.Query` is invoked.
1092 For
1093 options on how to control this, see :ref:`session_flushing`.
1094
1095 .. seealso::
1096
1097 :attr:`.AttributeState.history`
1098
1099 :func:`.attributes.get_history` - underlying function
1100
1101 """
1102 return self.state.get_history(self.key, PASSIVE_OFF ^ INIT_OK)
1103
1104
1105class PendingCollection:
1106 """A writable placeholder for an unloaded collection.
1107
1108 Stores items appended to and removed from a collection that has not yet
1109 been loaded. When the collection is loaded, the changes stored in
1110 PendingCollection are applied to it to produce the final result.
1111
1112 """
1113
1114 __slots__ = ("deleted_items", "added_items")
1115
1116 deleted_items: util.IdentitySet
1117 added_items: util.OrderedIdentitySet
1118
1119 def __init__(self) -> None:
1120 self.deleted_items = util.IdentitySet()
1121 self.added_items = util.OrderedIdentitySet()
1122
1123 def merge_with_history(self, history: History) -> History:
1124 return history._merge(self.added_items, self.deleted_items)
1125
1126 def append(self, value: Any) -> None:
1127 if value in self.deleted_items:
1128 self.deleted_items.remove(value)
1129 else:
1130 self.added_items.add(value)
1131
1132 def remove(self, value: Any) -> None:
1133 if value in self.added_items:
1134 self.added_items.remove(value)
1135 else:
1136 self.deleted_items.add(value)
1137 