src/pyams_zodbbrowser/state.py
changeset 0 a02202f95e2c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/state.py	Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,329 @@
+#
+# 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
+
+
+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
+
+
+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
+
+
+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)
+
+
+@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)
+
+    def getError(self):
+        return self.loadError
+
+    def listAttributes(self):
+        return self.state.listAttributes()
+
+    def listItems(self):
+        return self.state.listItems()
+
+    def getParent(self):
+        return self.state.getParent()
+
+    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
+
+    def asDict(self):
+        return self.state.asDict()
+
+    # These are not part of IStateInterpreter
+
+    def getObjectId(self):
+        return u64(self.obj._p_oid)
+
+    def isRoot(self):
+        return IContainmentRoot.providedBy(self.obj)
+
+    def getParentState(self):
+        parent = self.getParent()
+        if parent is None:
+            return None
+        else:
+            return ZodbObjectState(parent, self.requestedTid)
+
+
+@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
+
+    def getError(self):
+        return self.error
+
+    def getName(self):
+        return None
+
+    def getParent(self):
+        return None
+
+    def listAttributes(self):
+        return []
+
+    def listItems(self):
+        return None
+
+    def asDict(self):
+        return {}
+
+
+@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
+
+    def getError(self):
+        return None
+
+    def getName(self):
+        return self.state.get('__name__')
+
+    def getParent(self):
+        return self.state.get('__parent__')
+
+    def listAttributes(self):
+        return list(self.state.items())
+
+    def listItems(self):
+        return None
+
+    def asDict(self):
+        return self.state
+
+
+@adapter_config(context=(PersistentMapping, dict, None), provides=IStateInterpreter)
+class PersistentMappingState(GenericState):
+    """Convenient access to a persistent mapping's items."""
+
+    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.
+
+    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:
+
+    @adapter_config(context=(PersistentDict, dict, None), provides=IStateInterpreter)
+    class PersistentDictState(PersistentMappingState):
+        """Convenient access to a persistent dict's items."""
+
+
+@adapter_config(context=(SampleContainer, dict, None), provides=IStateInterpreter)
+class SampleContainerState(GenericState):
+    """Convenient access to a SampleContainer's items"""
+
+    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()
+
+
+@adapter_config(context=(OrderedContainer, dict, None), provides=IStateInterpreter)
+class OrderedContainerState(GenericState):
+    """Convenient access to an OrderedContainer's items"""
+
+    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())
+
+
+@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
+
+    def getName(self):
+        return self.state[1]
+
+    def getParent(self):
+        return self.state[0]
+
+    def listAttributes(self):
+        return [('__name__', self.getName()),
+                ('__parent__', self.getParent()),
+                ('proxied_object', self.proxy.__getnewargs__()[0])]
+
+    def listItems(self):
+        return []
+
+    def asDict(self):
+        return dict(self.listAttributes())
+
+
+@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
+
+    def getError(self):
+        return None
+
+    def getName(self):
+        return None
+
+    def getParent(self):
+        return None
+
+    def listAttributes(self):
+        return [('pickled state', self.state)]
+
+    def listItems(self):
+        return None
+
+    def asDict(self):
+        return dict(self.listAttributes())
+