123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023 |
- # orm/state.py
- # Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
- # <see AUTHORS file>
- #
- # This module is part of SQLAlchemy and is released under
- # the MIT License: https://www.opensource.org/licenses/mit-license.php
- """Defines instrumentation of instances.
- This module is usually not directly visible to user applications, but
- defines a large part of the ORM's interactivity.
- """
- import weakref
- from . import base
- from . import exc as orm_exc
- from . import interfaces
- from .base import ATTR_WAS_SET
- from .base import INIT_OK
- from .base import NEVER_SET
- from .base import NO_VALUE
- from .base import PASSIVE_NO_INITIALIZE
- from .base import PASSIVE_NO_RESULT
- from .base import PASSIVE_OFF
- from .base import SQL_OK
- from .path_registry import PathRegistry
- from .. import exc as sa_exc
- from .. import inspection
- from .. import util
- # late-populated by session.py
- _sessions = None
- # optionally late-provided by sqlalchemy.ext.asyncio.session
- _async_provider = None
- @inspection._self_inspects
- class InstanceState(interfaces.InspectionAttrInfo):
- """tracks state information at the instance level.
- The :class:`.InstanceState` is a key object used by the
- SQLAlchemy ORM in order to track the state of an object;
- it is created the moment an object is instantiated, typically
- as a result of :term:`instrumentation` which SQLAlchemy applies
- to the ``__init__()`` method of the class.
- :class:`.InstanceState` is also a semi-public object,
- available for runtime inspection as to the state of a
- mapped instance, including information such as its current
- status within a particular :class:`.Session` and details
- about data on individual attributes. The public API
- in order to acquire a :class:`.InstanceState` object
- is to use the :func:`_sa.inspect` system::
- >>> from sqlalchemy import inspect
- >>> insp = inspect(some_mapped_object)
- .. seealso::
- :ref:`core_inspection_toplevel`
- """
- session_id = None
- key = None
- runid = None
- load_options = util.EMPTY_SET
- load_path = PathRegistry.root
- insert_order = None
- _strong_obj = None
- modified = False
- expired = False
- _deleted = False
- _load_pending = False
- _orphaned_outside_of_session = False
- is_instance = True
- identity_token = None
- _last_known_values = ()
- callables = ()
- """A namespace where a per-state loader callable can be associated.
- In SQLAlchemy 1.0, this is only used for lazy loaders / deferred
- loaders that were set up via query option.
- Previously, callables was used also to indicate expired attributes
- by storing a link to the InstanceState itself in this dictionary.
- This role is now handled by the expired_attributes set.
- """
- def __init__(self, obj, manager):
- self.class_ = obj.__class__
- self.manager = manager
- self.obj = weakref.ref(obj, self._cleanup)
- self.committed_state = {}
- self.expired_attributes = set()
- expired_attributes = None
- """The set of keys which are 'expired' to be loaded by
- the manager's deferred scalar loader, assuming no pending
- changes.
- see also the ``unmodified`` collection which is intersected
- against this set when a refresh operation occurs."""
- @util.memoized_property
- def attrs(self):
- """Return a namespace representing each attribute on
- the mapped object, including its current value
- and history.
- The returned object is an instance of :class:`.AttributeState`.
- This object allows inspection of the current data
- within an attribute as well as attribute history
- since the last flush.
- """
- return util.ImmutableProperties(
- dict((key, AttributeState(self, key)) for key in self.manager)
- )
- @property
- def transient(self):
- """Return ``True`` if the object is :term:`transient`.
- .. seealso::
- :ref:`session_object_states`
- """
- return self.key is None and not self._attached
- @property
- def pending(self):
- """Return ``True`` if the object is :term:`pending`.
- .. seealso::
- :ref:`session_object_states`
- """
- return self.key is None and self._attached
- @property
- def deleted(self):
- """Return ``True`` if the object is :term:`deleted`.
- An object that is in the deleted state is guaranteed to
- not be within the :attr:`.Session.identity_map` of its parent
- :class:`.Session`; however if the session's transaction is rolled
- back, the object will be restored to the persistent state and
- the identity map.
- .. note::
- The :attr:`.InstanceState.deleted` attribute refers to a specific
- state of the object that occurs between the "persistent" and
- "detached" states; once the object is :term:`detached`, the
- :attr:`.InstanceState.deleted` attribute **no longer returns
- True**; in order to detect that a state was deleted, regardless
- of whether or not the object is associated with a
- :class:`.Session`, use the :attr:`.InstanceState.was_deleted`
- accessor.
- .. versionadded: 1.1
- .. seealso::
- :ref:`session_object_states`
- """
- return self.key is not None and self._attached and self._deleted
- @property
- def was_deleted(self):
- """Return True if this object is or was previously in the
- "deleted" state and has not been reverted to persistent.
- This flag returns True once the object was deleted in flush.
- When the object is expunged from the session either explicitly
- or via transaction commit and enters the "detached" state,
- this flag will continue to report True.
- .. versionadded:: 1.1 - added a local method form of
- :func:`.orm.util.was_deleted`.
- .. seealso::
- :attr:`.InstanceState.deleted` - refers to the "deleted" state
- :func:`.orm.util.was_deleted` - standalone function
- :ref:`session_object_states`
- """
- return self._deleted
- @property
- def persistent(self):
- """Return ``True`` if the object is :term:`persistent`.
- An object that is in the persistent state is guaranteed to
- be within the :attr:`.Session.identity_map` of its parent
- :class:`.Session`.
- .. versionchanged:: 1.1 The :attr:`.InstanceState.persistent`
- accessor no longer returns True for an object that was
- "deleted" within a flush; use the :attr:`.InstanceState.deleted`
- accessor to detect this state. This allows the "persistent"
- state to guarantee membership in the identity map.
- .. seealso::
- :ref:`session_object_states`
- """
- return self.key is not None and self._attached and not self._deleted
- @property
- def detached(self):
- """Return ``True`` if the object is :term:`detached`.
- .. seealso::
- :ref:`session_object_states`
- """
- return self.key is not None and not self._attached
- @property
- @util.preload_module("sqlalchemy.orm.session")
- def _attached(self):
- return (
- self.session_id is not None
- and self.session_id in util.preloaded.orm_session._sessions
- )
- def _track_last_known_value(self, key):
- """Track the last known value of a particular key after expiration
- operations.
- .. versionadded:: 1.3
- """
- if key not in self._last_known_values:
- self._last_known_values = dict(self._last_known_values)
- self._last_known_values[key] = NO_VALUE
- @property
- def session(self):
- """Return the owning :class:`.Session` for this instance,
- or ``None`` if none available.
- Note that the result here can in some cases be *different*
- from that of ``obj in session``; an object that's been deleted
- will report as not ``in session``, however if the transaction is
- still in progress, this attribute will still refer to that session.
- Only when the transaction is completed does the object become
- fully detached under normal circumstances.
- .. seealso::
- :attr:`_orm.InstanceState.async_session`
- """
- if self.session_id:
- try:
- return _sessions[self.session_id]
- except KeyError:
- pass
- return None
- @property
- def async_session(self):
- """Return the owning :class:`_asyncio.AsyncSession` for this instance,
- or ``None`` if none available.
- This attribute is only non-None when the :mod:`sqlalchemy.ext.asyncio`
- API is in use for this ORM object. The returned
- :class:`_asyncio.AsyncSession` object will be a proxy for the
- :class:`_orm.Session` object that would be returned from the
- :attr:`_orm.InstanceState.session` attribute for this
- :class:`_orm.InstanceState`.
- .. versionadded:: 1.4.18
- .. seealso::
- :ref:`asyncio_toplevel`
- """
- if _async_provider is None:
- return None
- sess = self.session
- if sess is not None:
- return _async_provider(sess)
- else:
- return None
- @property
- def object(self):
- """Return the mapped object represented by this
- :class:`.InstanceState`."""
- return self.obj()
- @property
- def identity(self):
- """Return the mapped identity of the mapped object.
- This is the primary key identity as persisted by the ORM
- which can always be passed directly to
- :meth:`_query.Query.get`.
- Returns ``None`` if the object has no primary key identity.
- .. note::
- An object which is :term:`transient` or :term:`pending`
- does **not** have a mapped identity until it is flushed,
- even if its attributes include primary key values.
- """
- if self.key is None:
- return None
- else:
- return self.key[1]
- @property
- def identity_key(self):
- """Return the identity key for the mapped object.
- This is the key used to locate the object within
- the :attr:`.Session.identity_map` mapping. It contains
- the identity as returned by :attr:`.identity` within it.
- """
- # TODO: just change .key to .identity_key across
- # the board ? probably
- return self.key
- @util.memoized_property
- def parents(self):
- return {}
- @util.memoized_property
- def _pending_mutations(self):
- return {}
- @util.memoized_property
- def _empty_collections(self):
- return {}
- @util.memoized_property
- def mapper(self):
- """Return the :class:`_orm.Mapper` used for this mapped object."""
- return self.manager.mapper
- @property
- def has_identity(self):
- """Return ``True`` if this object has an identity key.
- This should always have the same value as the
- expression ``state.persistent`` or ``state.detached``.
- """
- return bool(self.key)
- @classmethod
- def _detach_states(self, states, session, to_transient=False):
- persistent_to_detached = (
- session.dispatch.persistent_to_detached or None
- )
- deleted_to_detached = session.dispatch.deleted_to_detached or None
- pending_to_transient = session.dispatch.pending_to_transient or None
- persistent_to_transient = (
- session.dispatch.persistent_to_transient or None
- )
- for state in states:
- deleted = state._deleted
- pending = state.key is None
- persistent = not pending and not deleted
- state.session_id = None
- if to_transient and state.key:
- del state.key
- if persistent:
- if to_transient:
- if persistent_to_transient is not None:
- persistent_to_transient(session, state)
- elif persistent_to_detached is not None:
- persistent_to_detached(session, state)
- elif deleted and deleted_to_detached is not None:
- deleted_to_detached(session, state)
- elif pending and pending_to_transient is not None:
- pending_to_transient(session, state)
- state._strong_obj = None
- def _detach(self, session=None):
- if session:
- InstanceState._detach_states([self], session)
- else:
- self.session_id = self._strong_obj = None
- def _dispose(self):
- self._detach()
- del self.obj
- def _cleanup(self, ref):
- """Weakref callback cleanup.
- This callable cleans out the state when it is being garbage
- collected.
- this _cleanup **assumes** that there are no strong refs to us!
- Will not work otherwise!
- """
- # Python builtins become undefined during interpreter shutdown.
- # Guard against exceptions during this phase, as the method cannot
- # proceed in any case if builtins have been undefined.
- if dict is None:
- return
- instance_dict = self._instance_dict()
- if instance_dict is not None:
- instance_dict._fast_discard(self)
- del self._instance_dict
- # we can't possibly be in instance_dict._modified
- # b.c. this is weakref cleanup only, that set
- # is strong referencing!
- # assert self not in instance_dict._modified
- self.session_id = self._strong_obj = None
- del self.obj
- def obj(self):
- return None
- @property
- def dict(self):
- """Return the instance dict used by the object.
- Under normal circumstances, this is always synonymous
- with the ``__dict__`` attribute of the mapped object,
- unless an alternative instrumentation system has been
- configured.
- In the case that the actual object has been garbage
- collected, this accessor returns a blank dictionary.
- """
- o = self.obj()
- if o is not None:
- return base.instance_dict(o)
- else:
- return {}
- def _initialize_instance(*mixed, **kwargs):
- self, instance, args = mixed[0], mixed[1], mixed[2:] # noqa
- manager = self.manager
- manager.dispatch.init(self, args, kwargs)
- try:
- return manager.original_init(*mixed[1:], **kwargs)
- except:
- with util.safe_reraise():
- manager.dispatch.init_failure(self, args, kwargs)
- def get_history(self, key, passive):
- return self.manager[key].impl.get_history(self, self.dict, passive)
- def get_impl(self, key):
- return self.manager[key].impl
- def _get_pending_mutation(self, key):
- if key not in self._pending_mutations:
- self._pending_mutations[key] = PendingCollection()
- return self._pending_mutations[key]
- def __getstate__(self):
- state_dict = {"instance": self.obj()}
- state_dict.update(
- (k, self.__dict__[k])
- for k in (
- "committed_state",
- "_pending_mutations",
- "modified",
- "expired",
- "callables",
- "key",
- "parents",
- "load_options",
- "class_",
- "expired_attributes",
- "info",
- )
- if k in self.__dict__
- )
- if self.load_path:
- state_dict["load_path"] = self.load_path.serialize()
- state_dict["manager"] = self.manager._serialize(self, state_dict)
- return state_dict
- def __setstate__(self, state_dict):
- inst = state_dict["instance"]
- if inst is not None:
- self.obj = weakref.ref(inst, self._cleanup)
- self.class_ = inst.__class__
- else:
- # None being possible here generally new as of 0.7.4
- # due to storage of state in "parents". "class_"
- # also new.
- self.obj = None
- self.class_ = state_dict["class_"]
- self.committed_state = state_dict.get("committed_state", {})
- self._pending_mutations = state_dict.get("_pending_mutations", {})
- self.parents = state_dict.get("parents", {})
- self.modified = state_dict.get("modified", False)
- self.expired = state_dict.get("expired", False)
- if "info" in state_dict:
- self.info.update(state_dict["info"])
- if "callables" in state_dict:
- self.callables = state_dict["callables"]
- try:
- self.expired_attributes = state_dict["expired_attributes"]
- except KeyError:
- self.expired_attributes = set()
- # 0.9 and earlier compat
- for k in list(self.callables):
- if self.callables[k] is self:
- self.expired_attributes.add(k)
- del self.callables[k]
- else:
- if "expired_attributes" in state_dict:
- self.expired_attributes = state_dict["expired_attributes"]
- else:
- self.expired_attributes = set()
- self.__dict__.update(
- [
- (k, state_dict[k])
- for k in ("key", "load_options")
- if k in state_dict
- ]
- )
- if self.key:
- try:
- self.identity_token = self.key[2]
- except IndexError:
- # 1.1 and earlier compat before identity_token
- assert len(self.key) == 2
- self.key = self.key + (None,)
- self.identity_token = None
- if "load_path" in state_dict:
- self.load_path = PathRegistry.deserialize(state_dict["load_path"])
- state_dict["manager"](self, inst, state_dict)
- def _reset(self, dict_, key):
- """Remove the given attribute and any
- callables associated with it."""
- old = dict_.pop(key, None)
- if old is not None and self.manager[key].impl.collection:
- self.manager[key].impl._invalidate_collection(old)
- self.expired_attributes.discard(key)
- if self.callables:
- self.callables.pop(key, None)
- def _copy_callables(self, from_):
- if "callables" in from_.__dict__:
- self.callables = dict(from_.callables)
- @classmethod
- def _instance_level_callable_processor(cls, manager, fn, key):
- impl = manager[key].impl
- if impl.collection:
- def _set_callable(state, dict_, row):
- if "callables" not in state.__dict__:
- state.callables = {}
- old = dict_.pop(key, None)
- if old is not None:
- impl._invalidate_collection(old)
- state.callables[key] = fn
- else:
- def _set_callable(state, dict_, row):
- if "callables" not in state.__dict__:
- state.callables = {}
- state.callables[key] = fn
- return _set_callable
- def _expire(self, dict_, modified_set):
- self.expired = True
- if self.modified:
- modified_set.discard(self)
- self.committed_state.clear()
- self.modified = False
- self._strong_obj = None
- if "_pending_mutations" in self.__dict__:
- del self.__dict__["_pending_mutations"]
- if "parents" in self.__dict__:
- del self.__dict__["parents"]
- self.expired_attributes.update(
- [impl.key for impl in self.manager._loader_impls]
- )
- if self.callables:
- # the per state loader callables we can remove here are
- # LoadDeferredColumns, which undefers a column at the instance
- # level that is mapped with deferred, and LoadLazyAttribute,
- # which lazy loads a relationship at the instance level that
- # is mapped with "noload" or perhaps "immediateload".
- # Before 1.4, only column-based
- # attributes could be considered to be "expired", so here they
- # were the only ones "unexpired", which means to make them deferred
- # again. For the moment, as of 1.4 we also apply the same
- # treatment relationships now, that is, an instance level lazy
- # loader is reset in the same way as a column loader.
- for k in self.expired_attributes.intersection(self.callables):
- del self.callables[k]
- for k in self.manager._collection_impl_keys.intersection(dict_):
- collection = dict_.pop(k)
- collection._sa_adapter.invalidated = True
- if self._last_known_values:
- self._last_known_values.update(
- (k, dict_[k]) for k in self._last_known_values if k in dict_
- )
- for key in self.manager._all_key_set.intersection(dict_):
- del dict_[key]
- self.manager.dispatch.expire(self, None)
- def _expire_attributes(self, dict_, attribute_names, no_loader=False):
- pending = self.__dict__.get("_pending_mutations", None)
- callables = self.callables
- for key in attribute_names:
- impl = self.manager[key].impl
- if impl.accepts_scalar_loader:
- if no_loader and (impl.callable_ or key in callables):
- continue
- self.expired_attributes.add(key)
- if callables and key in callables:
- del callables[key]
- old = dict_.pop(key, NO_VALUE)
- if impl.collection and old is not NO_VALUE:
- impl._invalidate_collection(old)
- if (
- self._last_known_values
- and key in self._last_known_values
- and old is not NO_VALUE
- ):
- self._last_known_values[key] = old
- self.committed_state.pop(key, None)
- if pending:
- pending.pop(key, None)
- self.manager.dispatch.expire(self, attribute_names)
- def _load_expired(self, state, passive):
- """__call__ allows the InstanceState to act as a deferred
- callable for loading expired attributes, which is also
- serializable (picklable).
- """
- if not passive & SQL_OK:
- return PASSIVE_NO_RESULT
- toload = self.expired_attributes.intersection(self.unmodified)
- toload = toload.difference(
- attr
- for attr in toload
- if not self.manager[attr].impl.load_on_unexpire
- )
- self.manager.expired_attribute_loader(self, toload, passive)
- # if the loader failed, or this
- # instance state didn't have an identity,
- # the attributes still might be in the callables
- # dict. ensure they are removed.
- self.expired_attributes.clear()
- return ATTR_WAS_SET
- @property
- def unmodified(self):
- """Return the set of keys which have no uncommitted changes"""
- return set(self.manager).difference(self.committed_state)
- def unmodified_intersection(self, keys):
- """Return self.unmodified.intersection(keys)."""
- return (
- set(keys)
- .intersection(self.manager)
- .difference(self.committed_state)
- )
- @property
- def unloaded(self):
- """Return the set of keys which do not have a loaded value.
- This includes expired attributes and any other attribute that
- was never populated or modified.
- """
- return (
- set(self.manager)
- .difference(self.committed_state)
- .difference(self.dict)
- )
- @property
- def unloaded_expirable(self):
- """Return the set of keys which do not have a loaded value.
- This includes expired attributes and any other attribute that
- was never populated or modified.
- """
- return self.unloaded
- @property
- def _unloaded_non_object(self):
- return self.unloaded.intersection(
- attr
- for attr in self.manager
- if self.manager[attr].impl.accepts_scalar_loader
- )
- def _instance_dict(self):
- return None
- def _modified_event(
- self, dict_, attr, previous, collection=False, is_userland=False
- ):
- if attr:
- if not attr.send_modified_events:
- return
- if is_userland and attr.key not in dict_:
- raise sa_exc.InvalidRequestError(
- "Can't flag attribute '%s' modified; it's not present in "
- "the object state" % attr.key
- )
- if attr.key not in self.committed_state or is_userland:
- if collection:
- if previous is NEVER_SET:
- if attr.key in dict_:
- previous = dict_[attr.key]
- if previous not in (None, NO_VALUE, NEVER_SET):
- previous = attr.copy(previous)
- self.committed_state[attr.key] = previous
- if attr.key in self._last_known_values:
- self._last_known_values[attr.key] = NO_VALUE
- # assert self._strong_obj is None or self.modified
- if (self.session_id and self._strong_obj is None) or not self.modified:
- self.modified = True
- instance_dict = self._instance_dict()
- if instance_dict:
- has_modified = bool(instance_dict._modified)
- instance_dict._modified.add(self)
- else:
- has_modified = False
- # only create _strong_obj link if attached
- # to a session
- inst = self.obj()
- if self.session_id:
- self._strong_obj = inst
- # if identity map already had modified objects,
- # assume autobegin already occurred, else check
- # for autobegin
- if not has_modified:
- # inline of autobegin, to ensure session transaction
- # snapshot is established
- try:
- session = _sessions[self.session_id]
- except KeyError:
- pass
- else:
- if session._transaction is None:
- session._autobegin()
- if inst is None and attr:
- raise orm_exc.ObjectDereferencedError(
- "Can't emit change event for attribute '%s' - "
- "parent object of type %s has been garbage "
- "collected."
- % (self.manager[attr.key], base.state_class_str(self))
- )
- def _commit(self, dict_, keys):
- """Commit attributes.
- This is used by a partial-attribute load operation to mark committed
- those attributes which were refreshed from the database.
- Attributes marked as "expired" can potentially remain "expired" after
- this step if a value was not populated in state.dict.
- """
- for key in keys:
- self.committed_state.pop(key, None)
- self.expired = False
- self.expired_attributes.difference_update(
- set(keys).intersection(dict_)
- )
- # the per-keys commit removes object-level callables,
- # while that of commit_all does not. it's not clear
- # if this behavior has a clear rationale, however tests do
- # ensure this is what it does.
- if self.callables:
- for key in (
- set(self.callables).intersection(keys).intersection(dict_)
- ):
- del self.callables[key]
- def _commit_all(self, dict_, instance_dict=None):
- """commit all attributes unconditionally.
- This is used after a flush() or a full load/refresh
- to remove all pending state from the instance.
- - all attributes are marked as "committed"
- - the "strong dirty reference" is removed
- - the "modified" flag is set to False
- - any "expired" markers for scalar attributes loaded are removed.
- - lazy load callables for objects / collections *stay*
- Attributes marked as "expired" can potentially remain
- "expired" after this step if a value was not populated in state.dict.
- """
- self._commit_all_states([(self, dict_)], instance_dict)
- @classmethod
- def _commit_all_states(self, iter_, instance_dict=None):
- """Mass / highly inlined version of commit_all()."""
- for state, dict_ in iter_:
- state_dict = state.__dict__
- state.committed_state.clear()
- if "_pending_mutations" in state_dict:
- del state_dict["_pending_mutations"]
- state.expired_attributes.difference_update(dict_)
- if instance_dict and state.modified:
- instance_dict._modified.discard(state)
- state.modified = state.expired = False
- state._strong_obj = None
- class AttributeState(object):
- """Provide an inspection interface corresponding
- to a particular attribute on a particular mapped object.
- The :class:`.AttributeState` object is accessed
- via the :attr:`.InstanceState.attrs` collection
- of a particular :class:`.InstanceState`::
- from sqlalchemy import inspect
- insp = inspect(some_mapped_object)
- attr_state = insp.attrs.some_attribute
- """
- def __init__(self, state, key):
- self.state = state
- self.key = key
- @property
- def loaded_value(self):
- """The current value of this attribute as loaded from the database.
- If the value has not been loaded, or is otherwise not present
- in the object's dictionary, returns NO_VALUE.
- """
- return self.state.dict.get(self.key, NO_VALUE)
- @property
- def value(self):
- """Return the value of this attribute.
- This operation is equivalent to accessing the object's
- attribute directly or via ``getattr()``, and will fire
- off any pending loader callables if needed.
- """
- return self.state.manager[self.key].__get__(
- self.state.obj(), self.state.class_
- )
- @property
- def history(self):
- """Return the current **pre-flush** change history for
- this attribute, via the :class:`.History` interface.
- This method will **not** emit loader callables if the value of the
- attribute is unloaded.
- .. note::
- The attribute history system tracks changes on a **per flush
- basis**. Each time the :class:`.Session` is flushed, the history
- of each attribute is reset to empty. The :class:`.Session` by
- default autoflushes each time a :class:`_query.Query` is invoked.
- For
- options on how to control this, see :ref:`session_flushing`.
- .. seealso::
- :meth:`.AttributeState.load_history` - retrieve history
- using loader callables if the value is not locally present.
- :func:`.attributes.get_history` - underlying function
- """
- return self.state.get_history(self.key, PASSIVE_NO_INITIALIZE)
- def load_history(self):
- """Return the current **pre-flush** change history for
- this attribute, via the :class:`.History` interface.
- This method **will** emit loader callables if the value of the
- attribute is unloaded.
- .. note::
- The attribute history system tracks changes on a **per flush
- basis**. Each time the :class:`.Session` is flushed, the history
- of each attribute is reset to empty. The :class:`.Session` by
- default autoflushes each time a :class:`_query.Query` is invoked.
- For
- options on how to control this, see :ref:`session_flushing`.
- .. seealso::
- :attr:`.AttributeState.history`
- :func:`.attributes.get_history` - underlying function
- .. versionadded:: 0.9.0
- """
- return self.state.get_history(self.key, PASSIVE_OFF ^ INIT_OK)
- class PendingCollection(object):
- """A writable placeholder for an unloaded collection.
- Stores items appended to and removed from a collection that has not yet
- been loaded. When the collection is loaded, the changes stored in
- PendingCollection are applied to it to produce the final result.
- """
- def __init__(self):
- self.deleted_items = util.IdentitySet()
- self.added_items = util.OrderedIdentitySet()
- def append(self, value):
- if value in self.deleted_items:
- self.deleted_items.remove(value)
- else:
- self.added_items.add(value)
- def remove(self, value):
- if value in self.added_items:
- self.added_items.remove(value)
- else:
- self.deleted_items.add(value)
|