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