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