from sqlalchemy.util import EMPTY_SET import weakref from sqlalchemy import util from sqlalchemy.orm.attributes import PASSIVE_NO_RESULT, PASSIVE_OFF, \ NEVER_SET, NO_VALUE, manager_of_class, \ ATTR_WAS_SET from sqlalchemy.orm import attributes, exc as orm_exc, interfaces import sys attributes.state = sys.modules['sqlalchemy.orm.state'] class InstanceState(object): """tracks state information at the instance level.""" session_id = None key = None runid = None load_options = EMPTY_SET load_path = () insert_order = None mutable_dict = None _strong_obj = None modified = False expired = False def __init__(self, obj, manager): self.class_ = obj.__class__ self.manager = manager self.obj = weakref.ref(obj, self._cleanup) @util.memoized_property def committed_state(self): return {} @util.memoized_property def parents(self): return {} @util.memoized_property def pending(self): return {} @util.memoized_property def callables(self): return {} def detach(self): if self.session_id: try: del self.session_id except AttributeError: pass def dispose(self): self.detach() del self.obj def _cleanup(self, ref): instance_dict = self._instance_dict() if instance_dict: try: instance_dict.remove(self) except AssertionError: pass # remove possible cycles self.__dict__.pop('callables', None) self.dispose() def obj(self): return None @property def dict(self): o = self.obj() if o is not None: return attributes.instance_dict(o) else: return {} @property def sort_key(self): return self.key and self.key[1] or (self.insert_order, ) def initialize_instance(*mixed, **kwargs): self, instance, args = mixed[0], mixed[1], mixed[2:] manager = self.manager for fn in manager.events.on_init: fn(self, instance, args, kwargs) # LESSTHANIDEAL: # adjust for the case where the InstanceState was created before # mapper compilation, and this actually needs to be a MutableAttrInstanceState if manager.mutable_attributes and self.__class__ is not MutableAttrInstanceState: self.__class__ = MutableAttrInstanceState self.obj = weakref.ref(self.obj(), self._cleanup) self.mutable_dict = {} try: return manager.events.original_init(*mixed[1:], **kwargs) except: for fn in manager.events.on_init_failure: fn(self, instance, args, kwargs) raise def get_history(self, key, **kwargs): return self.manager.get_impl(key).get_history(self, self.dict, **kwargs) def get_impl(self, key): return self.manager.get_impl(key) def get_pending(self, key): if key not in self.pending: self.pending[key] = PendingCollection() return self.pending[key] def value_as_iterable(self, key, passive=PASSIVE_OFF): """return an InstanceState attribute as a list, regardless of it being a scalar or collection-based attribute. returns None if passive is not PASSIVE_OFF and the getter returns PASSIVE_NO_RESULT. """ impl = self.get_impl(key) dict_ = self.dict x = impl.get(self, dict_, passive=passive) if x is PASSIVE_NO_RESULT: return None elif hasattr(impl, 'get_collection'): return impl.get_collection(self, dict_, x, passive=passive) else: return [x] def _run_on_load(self, instance): self.manager.events.run('on_load', instance) def __getstate__(self): d = {'instance':self.obj()} d.update( (k, self.__dict__[k]) for k in ( 'committed_state', 'pending', 'parents', 'modified', 'expired', 'callables', 'key', 'load_options', 'mutable_dict' ) if k in self.__dict__ ) if self.load_path: d['load_path'] = interfaces.serialize_path(self.load_path) return d def __setstate__(self, state): self.obj = weakref.ref(state['instance'], self._cleanup) self.class_ = state['instance'].__class__ self.manager = manager = manager_of_class(self.class_) if manager is None: raise orm_exc.UnmappedInstanceError( state['instance'], "Cannot deserialize object of type %r - no mapper() has" " been configured for this class within the current Python process!" % self.class_) elif manager.mapper and not manager.mapper.compiled: manager.mapper.compile() self.committed_state = state.get('committed_state', {}) self.pending = state.get('pending', {}) self.parents = state.get('parents', {}) self.modified = state.get('modified', False) self.expired = state.get('expired', False) self.callables = state.get('callables', {}) if self.modified: self._strong_obj = state['instance'] self.__dict__.update([ (k, state[k]) for k in ( 'key', 'load_options', 'mutable_dict' ) if k in state ]) if 'load_path' in state: self.load_path = interfaces.deserialize_path(state['load_path']) def initialize(self, key): """Set this attribute to an empty value or collection, based on the AttributeImpl in use.""" self.manager.get_impl(key).initialize(self, self.dict) def reset(self, dict_, key): """Remove the given attribute and any callables associated with it.""" dict_.pop(key, None) self.callables.pop(key, None) def expire_attribute_pre_commit(self, dict_, key): """a fast expire that can be called by column loaders during a load. The additional bookkeeping is finished up in commit_all(). This method is actually called a lot with joined-table loading, when the second table isn't present in the result. """ dict_.pop(key, None) self.callables[key] = self def set_callable(self, dict_, key, callable_): """Remove the given attribute and set the given callable as a loader.""" dict_.pop(key, None) self.callables[key] = callable_ def expire_attributes(self, dict_, attribute_names, instance_dict=None): """Expire all or a group of attributes. If all attributes are expired, the "expired" flag is set to True. """ if attribute_names is None: attribute_names = self.manager.keys() self.expired = True if self.modified: if not instance_dict: instance_dict = self._instance_dict() if instance_dict: instance_dict._modified.discard(self) else: instance_dict._modified.discard(self) self.modified = False filter_deferred = True else: filter_deferred = False to_clear = ( self.__dict__.get('pending', None), self.__dict__.get('committed_state', None), self.mutable_dict ) for key in attribute_names: impl = self.manager[key].impl if impl.accepts_scalar_loader and \ (not filter_deferred or impl.expire_missing or key in dict_): self.callables[key] = self dict_.pop(key, None) for d in to_clear: if d is not None: d.pop(key, None) def __call__(self, **kw): """__call__ allows the InstanceState to act as a deferred callable for loading expired attributes, which is also serializable (picklable). """ if kw.get('passive') is attributes.PASSIVE_NO_FETCH: return attributes.PASSIVE_NO_RESULT toload = self.expired_attributes.\ intersection(self.unmodified) self.manager.deferred_scalar_loader(self, toload) # 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. for k in toload.intersection(self.callables): del self.callables[k] 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) @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 expired_attributes(self): """Return 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. """ return set([k for k, v in self.callables.items() if v is self]) def _instance_dict(self): return None def _is_really_none(self): return self.obj() def modified_event(self, dict_, attr, should_copy, previous, passive=PASSIVE_OFF): needs_committed = attr.key not in self.committed_state if needs_committed: if previous is NEVER_SET: if passive: if attr.key in dict_: previous = dict_[attr.key] else: previous = attr.get(self, dict_) if should_copy and previous not in (None, NO_VALUE, NEVER_SET): previous = attr.copy(previous) if needs_committed: self.committed_state[attr.key] = previous if not self.modified: instance_dict = self._instance_dict() if instance_dict: instance_dict._modified.add(self) self.modified = True if self._strong_obj is None: self._strong_obj = self.obj() 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. """ class_manager = self.manager for key in keys: if key in dict_ and key in class_manager.mutable_attributes: self.committed_state[key] = self.manager[key].impl.copy(dict_[key]) else: self.committed_state.pop(key, None) self.expired = False 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/callables for attributes loaded are removed. Attributes marked as "expired" can potentially remain "expired" after this step if a value was not populated in state.dict. """ self.__dict__.pop('committed_state', None) self.__dict__.pop('pending', None) if 'callables' in self.__dict__: callables = self.callables for key in list(callables): if key in dict_ and callables[key] is self: del callables[key] for key in self.manager.mutable_attributes: if key in dict_: self.committed_state[key] = self.manager[key].impl.copy(dict_[key]) if instance_dict and self.modified: instance_dict._modified.discard(self) self.modified = self.expired = False self._strong_obj = None class MutableAttrInstanceState(InstanceState): """InstanceState implementation for objects that reference 'mutable' attributes. Has a more involved "cleanup" handler that checks mutable attributes for changes upon dereference, resurrecting if needed. """ @util.memoized_property def mutable_dict(self): return {} def _get_modified(self, dict_=None): if self.__dict__.get('modified', False): return True else: if dict_ is None: dict_ = self.dict for key in self.manager.mutable_attributes: if self.manager[key].impl.check_mutable_modified(self, dict_): return True else: return False def _set_modified(self, value): self.__dict__['modified'] = value modified = property(_get_modified, _set_modified) @property def unmodified(self): """a set of keys which have no uncommitted changes""" dict_ = self.dict return set([ key for key in self.manager if (key not in self.committed_state or (key in self.manager.mutable_attributes and not self.manager[key].impl.check_mutable_modified(self, dict_)))]) def _is_really_none(self): """do a check modified/resurrect. This would be called in the extremely rare race condition that the weakref returned None but the cleanup handler had not yet established the __resurrect callable as its replacement. """ if self.modified: self.obj = self.__resurrect return self.obj() else: return None def reset(self, dict_, key): self.mutable_dict.pop(key, None) InstanceState.reset(self, dict_, key) def _cleanup(self, ref): """weakref callback. This method may be called by an asynchronous gc. If the state shows pending changes, the weakref is replaced by the __resurrect callable which will re-establish an object reference on next access, else removes this InstanceState from the owning identity map, if any. """ if self._get_modified(self.mutable_dict): self.obj = self.__resurrect else: instance_dict = self._instance_dict() if instance_dict: try: instance_dict.remove(self) except AssertionError: pass self.dispose() def __resurrect(self): """A substitute for the obj() weakref function which resurrects.""" # store strong ref'ed version of the object; will revert # to weakref when changes are persisted obj = self.manager.new_instance(state=self) self.obj = weakref.ref(obj, self._cleanup) self._strong_obj = obj obj.__dict__.update(self.mutable_dict) # re-establishes identity attributes from the key self.manager.events.run('on_resurrect', self, obj) # TODO: don't really think we should run this here. # resurrect is only meant to preserve the minimal state needed to # do an UPDATE, not to produce a fully usable object self._run_on_load(obj) return obj 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) self.added_items.add(value) def remove(self, value): if value in self.added_items: self.added_items.remove(value) self.deleted_items.add(value)