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