Source code for pyams_zodbbrowser.state

#
# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#

__docformat__ = 'restructuredtext'


# import standard library
import logging

# import interfaces
from zope.interface.interfaces import IInterface
from zope.traversing.interfaces import IContainmentRoot

# import packages
from persistent.dict import PersistentDict
from persistent.list import PersistentList
from persistent.mapping import PersistentMapping
from pyams_utils.adapter import adapter_config
from pyams_utils.request import check_request
from pyams_zodbbrowser.interfaces import IStateInterpreter, IObjectHistory
from ZODB.utils import u64
from zope.interface import implementer, Interface
from zope.interface.interface import InterfaceClass

import zope.interface.declarations

# be compatible with Zope 3.4, but prefer the modern package structure
from zope.container.sample import SampleContainer
from zope.container.ordered import OrderedContainer
from zope.container.contained import ContainedProxy


log = logging.getLogger(__name__)


real_Provides = zope.interface.declarations.Provides


[docs]def install_provides_hack(): """Monkey-patch zope.interface.Provides with a more lenient version. A common result of missing modules in sys.path is that you cannot unpickle objects that have been marked with directlyProvides() to implement interfaces that aren't currently available. Those interfaces are replaced by persistent broken placeholders, which aren classes, not interfaces, and aren't iterable, causing TypeErrors during unpickling. """ zope.interface.declarations.Provides = Provides
[docs]def flatten_interfaces(args): result = [] for a in args: if isinstance(a, (list, tuple)): result.extend(flatten_interfaces(a)) elif IInterface.providedBy(a): result.append(a) else: log.warning(' replacing %s with a placeholder', repr(a)) result.append(InterfaceClass(a.__name__, __module__='broken ' + a.__module__)) return result
[docs]def Provides(cls, *interfaces): try: return real_Provides(cls, *interfaces) except TypeError as e: log.warning('Suppressing TypeError while unpickling Provides: %s', e) args = flatten_interfaces(interfaces) return real_Provides(cls, *args)
[docs]@implementer(IStateInterpreter) class ZodbObjectState(object): def __init__(self, obj, tid=None, _history=None): self.obj = obj if _history is None: _history = IObjectHistory(self.obj) else: assert _history._obj is self.obj self.history = _history self.tid = None self.requestedTid = tid self.loadError = None self.pickledState = '' self._load() def _load(self): self.tid = self.history.lastChange(self.requestedTid) try: self.pickledState = self.history.loadStatePickle(self.tid) loadedState = self.history.loadState(self.tid) except Exception as e: self.loadError = "%s: %s" % (e.__class__.__name__, e) self.state = LoadErrorState(self.loadError, self.requestedTid) else: request = check_request() self.state = request.registry.getMultiAdapter((self.obj, loadedState, self.requestedTid), IStateInterpreter)
[docs] def getError(self): return self.loadError
[docs] def listAttributes(self): return self.state.listAttributes()
[docs] def listItems(self): return self.state.listItems()
[docs] def getParent(self): return self.state.getParent()
[docs] def getName(self): name = self.state.getName() if name is None: # __name__ is not in the pickled state, but it may be defined # via other means (e.g. class attributes, custom __getattr__ etc.) try: name = getattr(self.obj, '__name__', None) except Exception: # Ouch. Oh well, we can't determine the name. pass return name
[docs] def asDict(self): return self.state.asDict()
# These are not part of IStateInterpreter
[docs] def getObjectId(self): return u64(self.obj._p_oid)
[docs] def isRoot(self): return IContainmentRoot.providedBy(self.obj)
[docs] def getParentState(self): parent = self.getParent() if parent is None: return None else: return ZodbObjectState(parent, self.requestedTid)
[docs]@implementer(IStateInterpreter) class LoadErrorState(object): """Placeholder for when an object's state could not be loaded""" def __init__(self, error, tid): self.error = error self.tid = tid
[docs] def getError(self): return self.error
[docs] def getName(self): return None
[docs] def getParent(self): return None
[docs] def listAttributes(self): return []
[docs] def listItems(self): return None
[docs] def asDict(self): return {}
[docs]@adapter_config(context=(Interface, dict, None), provides=IStateInterpreter) @implementer(IStateInterpreter) class GenericState(object): """Most persistent objects represent their state as a dict.""" def __init__(self, type, state, tid): self.state = state self.tid = tid
[docs] def getError(self): return None
[docs] def getName(self): return self.state.get('__name__')
[docs] def getParent(self): return self.state.get('__parent__')
[docs] def listAttributes(self): return list(self.state.items())
[docs] def listItems(self): return None
[docs] def asDict(self): return self.state
[docs]@adapter_config(context=(PersistentMapping, dict, None), provides=IStateInterpreter) class PersistentMappingState(GenericState): """Convenient access to a persistent mapping's items."""
[docs] def listItems(self): return sorted(self.state.get('data', {}).items())
if PersistentMapping is PersistentDict: # ZODB 3.9 deprecated PersistentDict and made it an alias for # PersistentMapping. I don't know a clean way to conditionally disable the # <adapter> directive in ZCML to avoid conflicting configuration actions, # therefore I'll register a decoy adapter registered for a decoy class. # This adapter will never get used.
[docs] class DecoyPersistentDict(PersistentMapping): """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
@adapter_config(context=(DecoyPersistentDict, dict, None), provides=IStateInterpreter) class PersistentDictState(PersistentMappingState): """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9.""" else:
[docs] @adapter_config(context=(PersistentDict, dict, None), provides=IStateInterpreter) class PersistentDictState(PersistentMappingState): """Convenient access to a persistent dict's items."""
[docs]@adapter_config(context=(SampleContainer, dict, None), provides=IStateInterpreter) class SampleContainerState(GenericState): """Convenient access to a SampleContainer's items"""
[docs] def listItems(self): data = self.state.get('_SampleContainer__data') if not data: return [] # data will be something persistent, maybe a PersistentDict, maybe a # OOBTree -- SampleContainer itself uses a plain Python dict, but # subclasses are supposed to overwrite the _newContainerData() method # and use something persistent. loadedstate = IObjectHistory(data).loadState(self.tid) request = check_request() return request.registry.getMultiAdapter((data, loadedstate, self.tid), IStateInterpreter).listItems()
[docs]@adapter_config(context=(OrderedContainer, dict, None), provides=IStateInterpreter) class OrderedContainerState(GenericState): """Convenient access to an OrderedContainer's items"""
[docs] def listItems(self): # Now this is tricky: we want to construct a small object graph using # old state pickles without ever calling __setstate__ on a real # Persistent object, as _that_ would poison ZODB in-memory caches # in a nasty way (LP #487243). container = OrderedContainer() container.__setstate__(self.state) if isinstance(container._data, PersistentDict): old_data_state = IObjectHistory(container._data).loadState(self.tid) container._data = PersistentDict() container._data.__setstate__(old_data_state) if isinstance(container._order, PersistentList): old_order_state = IObjectHistory(container._order).loadState(self.tid) container._order = PersistentList() container._order.__setstate__(old_order_state) return list(container.items())
[docs]@adapter_config(context=(ContainedProxy, tuple, None), provides=IStateInterpreter) class ContainedProxyState(GenericState): def __init__(self, proxy, state, tid): GenericState.__init__(self, proxy, state, tid) self.proxy = proxy
[docs] def getName(self): return self.state[1]
[docs] def getParent(self): return self.state[0]
[docs] def listAttributes(self): return [('__name__', self.getName()), ('__parent__', self.getParent()), ('proxied_object', self.proxy.__getnewargs__()[0])]
[docs] def listItems(self): return []
[docs] def asDict(self): return dict(self.listAttributes())
[docs]@adapter_config(context=(Interface, Interface, None), provides=IStateInterpreter) @implementer(IStateInterpreter) class FallbackState(object): """Fallback when we've got no idea how to interpret the state""" def __init__(self, type, state, tid): self.state = state
[docs] def getError(self): return None
[docs] def getName(self): return None
[docs] def getParent(self): return None
[docs] def listAttributes(self): return [('pickled state', self.state)]
[docs] def listItems(self): return None
[docs] def asDict(self): return dict(self.listAttributes())