Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/sqlalchemy/orm/state.py : 55%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# orm/state.py
2# Copyright (C) 2005-2020 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
8"""Defines instrumentation of instances.
10This module is usually not directly visible to user applications, but
11defines a large part of the ORM's interactivity.
13"""
15import weakref
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
34@inspection._self_inspects
35class InstanceState(interfaces.InspectionAttrInfo):
36 """tracks state information at the instance level.
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.
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::
52 >>> from sqlalchemy import inspect
53 >>> insp = inspect(some_mapped_object)
55 .. seealso::
57 :ref:`core_inspection_toplevel`
59 """
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 = ()
77 callables = ()
78 """A namespace where a per-state loader callable can be associated.
80 In SQLAlchemy 1.0, this is only used for lazy loaders / deferred
81 loaders that were set up via query option.
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.
87 """
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()
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.
101 see also the ``unmodified`` collection which is intersected
102 against this set when a refresh operation occurs."""
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.
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.
115 """
116 return util.ImmutableProperties(
117 dict((key, AttributeState(self, key)) for key in self.manager)
118 )
120 @property
121 def transient(self):
122 """Return true if the object is :term:`transient`.
124 .. seealso::
126 :ref:`session_object_states`
128 """
129 return self.key is None and not self._attached
131 @property
132 def pending(self):
133 """Return true if the object is :term:`pending`.
136 .. seealso::
138 :ref:`session_object_states`
140 """
141 return self.key is None and self._attached
143 @property
144 def deleted(self):
145 """Return true if the object is :term:`deleted`.
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.
153 .. note::
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.
164 .. versionadded: 1.1
166 .. seealso::
168 :ref:`session_object_states`
170 """
171 return self.key is not None and self._attached and self._deleted
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.
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.
183 .. versionadded:: 1.1 - added a local method form of
184 :func:`.orm.util.was_deleted`.
186 .. seealso::
188 :attr:`.InstanceState.deleted` - refers to the "deleted" state
190 :func:`.orm.util.was_deleted` - standalone function
192 :ref:`session_object_states`
194 """
195 return self._deleted
197 @property
198 def persistent(self):
199 """Return true if the object is :term:`persistent`.
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`.
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.
211 .. seealso::
213 :ref:`session_object_states`
215 """
216 return self.key is not None and self._attached and not self._deleted
218 @property
219 def detached(self):
220 """Return true if the object is :term:`detached`.
222 .. seealso::
224 :ref:`session_object_states`
226 """
227 return self.key is not None and not self._attached
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 )
237 def _track_last_known_value(self, key):
238 """Track the last known value of a particular key after expiration
239 operations.
241 .. versionadded:: 1.3
243 """
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
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.
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.
262 """
263 return sessionlib._state_session(self)
265 @property
266 def object(self):
267 """Return the mapped object represented by this
268 :class:`.InstanceState`."""
269 return self.obj()
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`.
278 Returns ``None`` if the object has no primary key identity.
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.
285 """
286 if self.key is None:
287 return None
288 else:
289 return self.key[1]
291 @property
292 def identity_key(self):
293 """Return the identity key for the mapped object.
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.
300 """
301 # TODO: just change .key to .identity_key across
302 # the board ? probably
303 return self.key
305 @util.memoized_property
306 def parents(self):
307 return {}
309 @util.memoized_property
310 def _pending_mutations(self):
311 return {}
313 @util.memoized_property
314 def mapper(self):
315 """Return the :class:`_orm.Mapper` used for this mapped object."""
316 return self.manager.mapper
318 @property
319 def has_identity(self):
320 """Return ``True`` if this object has an identity key.
322 This should always have the same value as the
323 expression ``state.persistent or state.detached``.
325 """
326 return bool(self.key)
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 )
339 for state in states:
340 deleted = state._deleted
341 pending = state.key is None
342 persistent = not pending and not deleted
344 state.session_id = None
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)
359 state._strong_obj = None
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
367 def _dispose(self):
368 self._detach()
369 del self.obj
371 def _cleanup(self, ref):
372 """Weakref callback cleanup.
374 This callable cleans out the state when it is being garbage
375 collected.
377 this _cleanup **assumes** that there are no strong refs to us!
378 Will not work otherwise!
380 """
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
388 instance_dict = self._instance_dict()
389 if instance_dict is not None:
390 instance_dict._fast_discard(self)
391 del self._instance_dict
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
398 self.session_id = self._strong_obj = None
399 del self.obj
401 def obj(self):
402 return None
404 @property
405 def dict(self):
406 """Return the instance dict used by the object.
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.
413 In the case that the actual object has been garbage
414 collected, this accessor returns a blank dictionary.
416 """
417 o = self.obj()
418 if o is not None:
419 return base.instance_dict(o)
420 else:
421 return {}
423 def _initialize_instance(*mixed, **kwargs):
424 self, instance, args = mixed[0], mixed[1], mixed[2:] # noqa
425 manager = self.manager
427 manager.dispatch.init(self, args, kwargs)
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)
435 def get_history(self, key, passive):
436 return self.manager[key].impl.get_history(self, self.dict, passive)
438 def get_impl(self, key):
439 return self.manager[key].impl
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]
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()
468 state_dict["manager"] = self.manager._serialize(self, state_dict)
470 return state_dict
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_"]
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"]
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()
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
525 if "load_path" in state_dict:
526 self.load_path = PathRegistry.deserialize(state_dict["load_path"])
528 state_dict["manager"](self, inst, state_dict)
530 def _reset(self, dict_, key):
531 """Remove the given attribute and any
532 callables associated with it."""
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)
541 def _copy_callables(self, from_):
542 if "callables" in from_.__dict__:
543 self.callables = dict(from_.callables)
545 @classmethod
546 def _instance_level_callable_processor(cls, manager, fn, key):
547 impl = manager[key].impl
548 if impl.collection:
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
558 else:
560 def _set_callable(state, dict_, row):
561 if "callables" not in state.__dict__:
562 state.callables = {}
563 state.callables[key] = fn
565 return _set_callable
567 def _expire(self, dict_, modified_set):
568 self.expired = True
570 if self.modified:
571 modified_set.discard(self)
572 self.committed_state.clear()
573 self.modified = False
575 self._strong_obj = None
577 if "_pending_mutations" in self.__dict__:
578 del self.__dict__["_pending_mutations"]
580 if "parents" in self.__dict__:
581 del self.__dict__["parents"]
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 )
591 if self.callables:
592 for k in self.expired_attributes.intersection(self.callables):
593 del self.callables[k]
595 for k in self.manager._collection_impl_keys.intersection(dict_):
596 collection = dict_.pop(k)
597 collection._sa_adapter.invalidated = True
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 )
604 for key in self.manager._all_key_set.intersection(dict_):
605 del dict_[key]
607 self.manager.dispatch.expire(self, None)
609 def _expire_attributes(self, dict_, attribute_names, no_loader=False):
610 pending = self.__dict__.get("_pending_mutations", None)
612 callables = self.callables
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
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)
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
634 self.committed_state.pop(key, None)
635 if pending:
636 pending.pop(key, None)
638 self.manager.dispatch.expire(self, attribute_names)
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).
645 """
647 if not passive & SQL_OK:
648 return PASSIVE_NO_RESULT
650 toload = self.expired_attributes.intersection(self.unmodified)
652 self.manager.deferred_scalar_loader(self, toload)
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()
660 return ATTR_WAS_SET
662 @property
663 def unmodified(self):
664 """Return the set of keys which have no uncommitted changes"""
666 return set(self.manager).difference(self.committed_state)
668 def unmodified_intersection(self, keys):
669 """Return self.unmodified.intersection(keys)."""
671 return (
672 set(keys)
673 .intersection(self.manager)
674 .difference(self.committed_state)
675 )
677 @property
678 def unloaded(self):
679 """Return the set of keys which do not have a loaded value.
681 This includes expired attributes and any other attribute that
682 was never populated or modified.
684 """
685 return (
686 set(self.manager)
687 .difference(self.committed_state)
688 .difference(self.dict)
689 )
691 @property
692 def unloaded_expirable(self):
693 """Return the set of keys which do not have a loaded value.
695 This includes expired attributes and any other attribute that
696 was never populated or modified.
698 """
699 return self.unloaded.intersection(
700 attr
701 for attr in self.manager
702 if self.manager[attr].impl.expire_missing
703 )
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 )
713 def _instance_dict(self):
714 return None
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]
733 if previous not in (None, NO_VALUE, NEVER_SET):
734 previous = attr.copy(previous)
735 self.committed_state[attr.key] = previous
737 if attr.key in self._last_known_values:
738 self._last_known_values[attr.key] = NO_VALUE
740 # assert self._strong_obj is None or self.modified
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)
748 # only create _strong_obj link if attached
749 # to a session
751 inst = self.obj()
752 if self.session_id:
753 self._strong_obj = inst
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 )
763 def _commit(self, dict_, keys):
764 """Commit attributes.
766 This is used by a partial-attribute load operation to mark committed
767 those attributes which were refreshed from the database.
769 Attributes marked as "expired" can potentially remain "expired" after
770 this step if a value was not populated in state.dict.
772 """
773 for key in keys:
774 self.committed_state.pop(key, None)
776 self.expired = False
778 self.expired_attributes.difference_update(
779 set(keys).intersection(dict_)
780 )
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]
792 def _commit_all(self, dict_, instance_dict=None):
793 """commit all attributes unconditionally.
795 This is used after a flush() or a full load/refresh
796 to remove all pending state from the instance.
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*
804 Attributes marked as "expired" can potentially remain
805 "expired" after this step if a value was not populated in state.dict.
807 """
808 self._commit_all_states([(self, dict_)], instance_dict)
810 @classmethod
811 def _commit_all_states(self, iter_, instance_dict=None):
812 """Mass / highly inlined version of commit_all()."""
814 for state, dict_ in iter_:
815 state_dict = state.__dict__
817 state.committed_state.clear()
819 if "_pending_mutations" in state_dict:
820 del state_dict["_pending_mutations"]
822 state.expired_attributes.difference_update(dict_)
824 if instance_dict and state.modified:
825 instance_dict._modified.discard(state)
827 state.modified = state.expired = False
828 state._strong_obj = None
831class AttributeState(object):
832 """Provide an inspection interface corresponding
833 to a particular attribute on a particular mapped object.
835 The :class:`.AttributeState` object is accessed
836 via the :attr:`.InstanceState.attrs` collection
837 of a particular :class:`.InstanceState`::
839 from sqlalchemy import inspect
841 insp = inspect(some_mapped_object)
842 attr_state = insp.attrs.some_attribute
844 """
846 def __init__(self, state, key):
847 self.state = state
848 self.key = key
850 @property
851 def loaded_value(self):
852 """The current value of this attribute as loaded from the database.
854 If the value has not been loaded, or is otherwise not present
855 in the object's dictionary, returns NO_VALUE.
857 """
858 return self.state.dict.get(self.key, NO_VALUE)
860 @property
861 def value(self):
862 """Return the value of this attribute.
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.
868 """
869 return self.state.manager[self.key].__get__(
870 self.state.obj(), self.state.class_
871 )
873 @property
874 def history(self):
875 """Return the current **pre-flush** change history for
876 this attribute, via the :class:`.History` interface.
878 This method will **not** emit loader callables if the value of the
879 attribute is unloaded.
881 .. note::
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`.
891 .. seealso::
893 :meth:`.AttributeState.load_history` - retrieve history
894 using loader callables if the value is not locally present.
896 :func:`.attributes.get_history` - underlying function
898 """
899 return self.state.get_history(self.key, PASSIVE_NO_INITIALIZE)
901 def load_history(self):
902 """Return the current **pre-flush** change history for
903 this attribute, via the :class:`.History` interface.
905 This method **will** emit loader callables if the value of the
906 attribute is unloaded.
908 .. note::
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`.
917 .. seealso::
919 :attr:`.AttributeState.history`
921 :func:`.attributes.get_history` - underlying function
923 .. versionadded:: 0.9.0
925 """
926 return self.state.get_history(self.key, PASSIVE_OFF ^ INIT_OK)
929class PendingCollection(object):
930 """A writable placeholder for an unloaded collection.
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.
936 """
938 def __init__(self):
939 self.deleted_items = util.IdentitySet()
940 self.added_items = util.OrderedIdentitySet()
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)
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)