Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/state.py: 31%
402 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1# orm/state.py
2# Copyright (C) 2005-2023 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
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# late-populated by session.py
35_sessions = None
37# optionally late-provided by sqlalchemy.ext.asyncio.session
38_async_provider = None
41@inspection._self_inspects
42class InstanceState(interfaces.InspectionAttrInfo):
43 """tracks state information at the instance level.
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.
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::
59 >>> from sqlalchemy import inspect
60 >>> insp = inspect(some_mapped_object)
61 >>> insp.attrs.nickname.history
62 History(added=['new nickname'], unchanged=(), deleted=['nickname'])
64 .. seealso::
66 :ref:`orm_mapper_inspection_instancestate`
68 """
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 = ()
86 callables = ()
87 """A namespace where a per-state loader callable can be associated.
89 In SQLAlchemy 1.0, this is only used for lazy loaders / deferred
90 loaders that were set up via query option.
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.
96 """
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()
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.
110 see also the ``unmodified`` collection which is intersected
111 against this set when a refresh operation occurs."""
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.
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.
124 """
125 return util.ImmutableProperties(
126 dict((key, AttributeState(self, key)) for key in self.manager)
127 )
129 @property
130 def transient(self):
131 """Return ``True`` if the object is :term:`transient`.
133 .. seealso::
135 :ref:`session_object_states`
137 """
138 return self.key is None and not self._attached
140 @property
141 def pending(self):
142 """Return ``True`` if the object is :term:`pending`.
145 .. seealso::
147 :ref:`session_object_states`
149 """
150 return self.key is None and self._attached
152 @property
153 def deleted(self):
154 """Return ``True`` if the object is :term:`deleted`.
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.
162 .. note::
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.
173 .. versionadded: 1.1
175 .. seealso::
177 :ref:`session_object_states`
179 """
180 return self.key is not None and self._attached and self._deleted
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.
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.
192 .. versionadded:: 1.1 - added a local method form of
193 :func:`.orm.util.was_deleted`.
195 .. seealso::
197 :attr:`.InstanceState.deleted` - refers to the "deleted" state
199 :func:`.orm.util.was_deleted` - standalone function
201 :ref:`session_object_states`
203 """
204 return self._deleted
206 @property
207 def persistent(self):
208 """Return ``True`` if the object is :term:`persistent`.
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`.
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.
220 .. seealso::
222 :ref:`session_object_states`
224 """
225 return self.key is not None and self._attached and not self._deleted
227 @property
228 def detached(self):
229 """Return ``True`` if the object is :term:`detached`.
231 .. seealso::
233 :ref:`session_object_states`
235 """
236 return self.key is not None and not self._attached
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 )
246 def _track_last_known_value(self, key):
247 """Track the last known value of a particular key after expiration
248 operations.
250 .. versionadded:: 1.3
252 """
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
258 @property
259 def session(self):
260 """Return the owning :class:`.Session` for this instance,
261 or ``None`` if none available.
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.
270 .. seealso::
272 :attr:`_orm.InstanceState.async_session`
274 """
275 if self.session_id:
276 try:
277 return _sessions[self.session_id]
278 except KeyError:
279 pass
280 return None
282 @property
283 def async_session(self):
284 """Return the owning :class:`_asyncio.AsyncSession` for this instance,
285 or ``None`` if none available.
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`.
294 .. versionadded:: 1.4.18
296 .. seealso::
298 :ref:`asyncio_toplevel`
300 """
301 if _async_provider is None:
302 return None
304 sess = self.session
305 if sess is not None:
306 return _async_provider(sess)
307 else:
308 return None
310 @property
311 def object(self):
312 """Return the mapped object represented by this
313 :class:`.InstanceState`."""
314 return self.obj()
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`.
323 Returns ``None`` if the object has no primary key identity.
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.
330 """
331 if self.key is None:
332 return None
333 else:
334 return self.key[1]
336 @property
337 def identity_key(self):
338 """Return the identity key for the mapped object.
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.
345 """
346 # TODO: just change .key to .identity_key across
347 # the board ? probably
348 return self.key
350 @util.memoized_property
351 def parents(self):
352 return {}
354 @util.memoized_property
355 def _pending_mutations(self):
356 return {}
358 @util.memoized_property
359 def _empty_collections(self):
360 return {}
362 @util.memoized_property
363 def mapper(self):
364 """Return the :class:`_orm.Mapper` used for this mapped object."""
365 return self.manager.mapper
367 @property
368 def has_identity(self):
369 """Return ``True`` if this object has an identity key.
371 This should always have the same value as the
372 expression ``state.persistent`` or ``state.detached``.
374 """
375 return bool(self.key)
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 )
388 for state in states:
389 deleted = state._deleted
390 pending = state.key is None
391 persistent = not pending and not deleted
393 state.session_id = None
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)
408 state._strong_obj = None
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
416 def _dispose(self):
417 self._detach()
418 del self.obj
420 def _cleanup(self, ref):
421 """Weakref callback cleanup.
423 This callable cleans out the state when it is being garbage
424 collected.
426 this _cleanup **assumes** that there are no strong refs to us!
427 Will not work otherwise!
429 """
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
437 instance_dict = self._instance_dict()
438 if instance_dict is not None:
439 instance_dict._fast_discard(self)
440 del self._instance_dict
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
447 self.session_id = self._strong_obj = None
448 del self.obj
450 def obj(self):
451 return None
453 @property
454 def dict(self):
455 """Return the instance dict used by the object.
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.
462 In the case that the actual object has been garbage
463 collected, this accessor returns a blank dictionary.
465 """
466 o = self.obj()
467 if o is not None:
468 return base.instance_dict(o)
469 else:
470 return {}
472 def _initialize_instance(*mixed, **kwargs):
473 self, instance, args = mixed[0], mixed[1], mixed[2:] # noqa
474 manager = self.manager
476 manager.dispatch.init(self, args, kwargs)
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)
484 def get_history(self, key, passive):
485 return self.manager[key].impl.get_history(self, self.dict, passive)
487 def get_impl(self, key):
488 return self.manager[key].impl
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]
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()
517 state_dict["manager"] = self.manager._serialize(self, state_dict)
519 return state_dict
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_"]
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"]
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()
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
574 if "load_path" in state_dict:
575 self.load_path = PathRegistry.deserialize(state_dict["load_path"])
577 state_dict["manager"](self, inst, state_dict)
579 def _reset(self, dict_, key):
580 """Remove the given attribute and any
581 callables associated with it."""
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)
590 def _copy_callables(self, from_):
591 if "callables" in from_.__dict__:
592 self.callables = dict(from_.callables)
594 @classmethod
595 def _instance_level_callable_processor(cls, manager, fn, key):
596 impl = manager[key].impl
597 if impl.collection:
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
607 else:
609 def _set_callable(state, dict_, row):
610 if "callables" not in state.__dict__:
611 state.callables = {}
612 state.callables[key] = fn
614 return _set_callable
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
623 self._strong_obj = None
625 if "_pending_mutations" in self.__dict__:
626 del self.__dict__["_pending_mutations"]
628 if "parents" in self.__dict__:
629 del self.__dict__["parents"]
631 self.expired_attributes.update(
632 [impl.key for impl in self.manager._loader_impls]
633 )
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]
650 for k in self.manager._collection_impl_keys.intersection(dict_):
651 collection = dict_.pop(k)
652 collection._sa_adapter.invalidated = True
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 )
659 for key in self.manager._all_key_set.intersection(dict_):
660 del dict_[key]
662 self.manager.dispatch.expire(self, None)
664 def _expire_attributes(self, dict_, attribute_names, no_loader=False):
665 pending = self.__dict__.get("_pending_mutations", None)
667 callables = self.callables
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
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)
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
689 self.committed_state.pop(key, None)
690 if pending:
691 pending.pop(key, None)
693 self.manager.dispatch.expire(self, attribute_names)
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).
700 """
702 if not passive & SQL_OK:
703 return PASSIVE_NO_RESULT
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 )
712 self.manager.expired_attribute_loader(self, toload, passive)
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()
720 return ATTR_WAS_SET
722 @property
723 def unmodified(self):
724 """Return the set of keys which have no uncommitted changes"""
726 return set(self.manager).difference(self.committed_state)
728 def unmodified_intersection(self, keys):
729 """Return self.unmodified.intersection(keys)."""
731 return (
732 set(keys)
733 .intersection(self.manager)
734 .difference(self.committed_state)
735 )
737 @property
738 def unloaded(self):
739 """Return the set of keys which do not have a loaded value.
741 This includes expired attributes and any other attribute that
742 was never populated or modified.
744 """
745 return (
746 set(self.manager)
747 .difference(self.committed_state)
748 .difference(self.dict)
749 )
751 @property
752 def unloaded_expirable(self):
753 """Return the set of keys which do not have a loaded value.
755 This includes expired attributes and any other attribute that
756 was never populated or modified.
758 """
759 return self.unloaded
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 )
769 def _instance_dict(self):
770 return None
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]
789 if previous not in (None, NO_VALUE, NEVER_SET):
790 previous = attr.copy(previous)
791 self.committed_state[attr.key] = previous
793 if attr.key in self._last_known_values:
794 self._last_known_values[attr.key] = NO_VALUE
796 # assert self._strong_obj is None or self.modified
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
807 # only create _strong_obj link if attached
808 # to a session
810 inst = self.obj()
811 if self.session_id:
812 self._strong_obj = inst
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()
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 )
836 def _commit(self, dict_, keys):
837 """Commit attributes.
839 This is used by a partial-attribute load operation to mark committed
840 those attributes which were refreshed from the database.
842 Attributes marked as "expired" can potentially remain "expired" after
843 this step if a value was not populated in state.dict.
845 """
846 for key in keys:
847 self.committed_state.pop(key, None)
849 self.expired = False
851 self.expired_attributes.difference_update(
852 set(keys).intersection(dict_)
853 )
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]
865 def _commit_all(self, dict_, instance_dict=None):
866 """commit all attributes unconditionally.
868 This is used after a flush() or a full load/refresh
869 to remove all pending state from the instance.
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*
877 Attributes marked as "expired" can potentially remain
878 "expired" after this step if a value was not populated in state.dict.
880 """
881 self._commit_all_states([(self, dict_)], instance_dict)
883 @classmethod
884 def _commit_all_states(self, iter_, instance_dict=None):
885 """Mass / highly inlined version of commit_all()."""
887 for state, dict_ in iter_:
888 state_dict = state.__dict__
890 state.committed_state.clear()
892 if "_pending_mutations" in state_dict:
893 del state_dict["_pending_mutations"]
895 state.expired_attributes.difference_update(dict_)
897 if instance_dict and state.modified:
898 instance_dict._modified.discard(state)
900 state.modified = state.expired = False
901 state._strong_obj = None
904class AttributeState(object):
905 """Provide an inspection interface corresponding
906 to a particular attribute on a particular mapped object.
908 The :class:`.AttributeState` object is accessed
909 via the :attr:`.InstanceState.attrs` collection
910 of a particular :class:`.InstanceState`::
912 from sqlalchemy import inspect
914 insp = inspect(some_mapped_object)
915 attr_state = insp.attrs.some_attribute
917 """
919 def __init__(self, state, key):
920 self.state = state
921 self.key = key
923 @property
924 def loaded_value(self):
925 """The current value of this attribute as loaded from the database.
927 If the value has not been loaded, or is otherwise not present
928 in the object's dictionary, returns NO_VALUE.
930 """
931 return self.state.dict.get(self.key, NO_VALUE)
933 @property
934 def value(self):
935 """Return the value of this attribute.
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.
941 """
942 return self.state.manager[self.key].__get__(
943 self.state.obj(), self.state.class_
944 )
946 @property
947 def history(self):
948 """Return the current **pre-flush** change history for
949 this attribute, via the :class:`.History` interface.
951 This method will **not** emit loader callables if the value of the
952 attribute is unloaded.
954 .. note::
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`.
964 .. seealso::
966 :meth:`.AttributeState.load_history` - retrieve history
967 using loader callables if the value is not locally present.
969 :func:`.attributes.get_history` - underlying function
971 """
972 return self.state.get_history(self.key, PASSIVE_NO_INITIALIZE)
974 def load_history(self):
975 """Return the current **pre-flush** change history for
976 this attribute, via the :class:`.History` interface.
978 This method **will** emit loader callables if the value of the
979 attribute is unloaded.
981 .. note::
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`.
990 .. seealso::
992 :attr:`.AttributeState.history`
994 :func:`.attributes.get_history` - underlying function
996 .. versionadded:: 0.9.0
998 """
999 return self.state.get_history(self.key, PASSIVE_OFF ^ INIT_OK)
1002class PendingCollection(object):
1003 """A writable placeholder for an unloaded collection.
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.
1009 """
1011 def __init__(self):
1012 self.deleted_items = util.IdentitySet()
1013 self.added_items = util.OrderedIdentitySet()
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)
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)