src/pyams_zodbbrowser/state.py
changeset 0 a02202f95e2c
equal deleted inserted replaced
-1:000000000000 0:a02202f95e2c
       
     1 #
       
     2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 
       
    13 __docformat__ = 'restructuredtext'
       
    14 
       
    15 
       
    16 # import standard library
       
    17 import logging
       
    18 
       
    19 # import interfaces
       
    20 from zope.interface.interfaces import IInterface
       
    21 from zope.traversing.interfaces import IContainmentRoot
       
    22 
       
    23 # import packages
       
    24 from persistent.dict import PersistentDict
       
    25 from persistent.list import PersistentList
       
    26 from persistent.mapping import PersistentMapping
       
    27 from pyams_utils.adapter import adapter_config
       
    28 from pyams_utils.request import check_request
       
    29 from pyams_zodbbrowser.interfaces import IStateInterpreter, IObjectHistory
       
    30 from ZODB.utils import u64
       
    31 from zope.interface import implementer, Interface
       
    32 from zope.interface.interface import InterfaceClass
       
    33 
       
    34 import zope.interface.declarations
       
    35 
       
    36 # be compatible with Zope 3.4, but prefer the modern package structure
       
    37 from zope.container.sample import SampleContainer
       
    38 from zope.container.ordered import OrderedContainer
       
    39 from zope.container.contained import ContainedProxy
       
    40 
       
    41 
       
    42 log = logging.getLogger(__name__)
       
    43 
       
    44 
       
    45 real_Provides = zope.interface.declarations.Provides
       
    46 
       
    47 
       
    48 def install_provides_hack():
       
    49     """Monkey-patch zope.interface.Provides with a more lenient version.
       
    50 
       
    51     A common result of missing modules in sys.path is that you cannot
       
    52     unpickle objects that have been marked with directlyProvides() to
       
    53     implement interfaces that aren't currently available.  Those interfaces
       
    54     are replaced by persistent broken placeholders, which aren classes,
       
    55     not interfaces, and aren't iterable, causing TypeErrors during unpickling.
       
    56     """
       
    57     zope.interface.declarations.Provides = Provides
       
    58 
       
    59 
       
    60 def flatten_interfaces(args):
       
    61     result = []
       
    62     for a in args:
       
    63         if isinstance(a, (list, tuple)):
       
    64             result.extend(flatten_interfaces(a))
       
    65         elif IInterface.providedBy(a):
       
    66             result.append(a)
       
    67         else:
       
    68             log.warning('  replacing %s with a placeholder', repr(a))
       
    69             result.append(InterfaceClass(a.__name__,
       
    70                             __module__='broken ' + a.__module__))
       
    71     return result
       
    72 
       
    73 
       
    74 def Provides(cls, *interfaces):
       
    75     try:
       
    76         return real_Provides(cls, *interfaces)
       
    77     except TypeError as e:
       
    78         log.warning('Suppressing TypeError while unpickling Provides: %s', e)
       
    79         args = flatten_interfaces(interfaces)
       
    80         return real_Provides(cls, *args)
       
    81 
       
    82 
       
    83 @implementer(IStateInterpreter)
       
    84 class ZodbObjectState(object):
       
    85 
       
    86     def __init__(self, obj, tid=None, _history=None):
       
    87         self.obj = obj
       
    88         if _history is None:
       
    89             _history = IObjectHistory(self.obj)
       
    90         else:
       
    91             assert _history._obj is self.obj
       
    92         self.history = _history
       
    93         self.tid = None
       
    94         self.requestedTid = tid
       
    95         self.loadError = None
       
    96         self.pickledState = ''
       
    97         self._load()
       
    98 
       
    99     def _load(self):
       
   100         self.tid = self.history.lastChange(self.requestedTid)
       
   101         try:
       
   102             self.pickledState = self.history.loadStatePickle(self.tid)
       
   103             loadedState = self.history.loadState(self.tid)
       
   104         except Exception as e:
       
   105             self.loadError = "%s: %s" % (e.__class__.__name__, e)
       
   106             self.state = LoadErrorState(self.loadError, self.requestedTid)
       
   107         else:
       
   108             request = check_request()
       
   109             self.state = request.registry.getMultiAdapter((self.obj, loadedState, self.requestedTid),
       
   110                                                           IStateInterpreter)
       
   111 
       
   112     def getError(self):
       
   113         return self.loadError
       
   114 
       
   115     def listAttributes(self):
       
   116         return self.state.listAttributes()
       
   117 
       
   118     def listItems(self):
       
   119         return self.state.listItems()
       
   120 
       
   121     def getParent(self):
       
   122         return self.state.getParent()
       
   123 
       
   124     def getName(self):
       
   125         name = self.state.getName()
       
   126         if name is None:
       
   127             # __name__ is not in the pickled state, but it may be defined
       
   128             # via other means (e.g. class attributes, custom __getattr__ etc.)
       
   129             try:
       
   130                 name = getattr(self.obj, '__name__', None)
       
   131             except Exception:
       
   132                 # Ouch.  Oh well, we can't determine the name.
       
   133                 pass
       
   134         return name
       
   135 
       
   136     def asDict(self):
       
   137         return self.state.asDict()
       
   138 
       
   139     # These are not part of IStateInterpreter
       
   140 
       
   141     def getObjectId(self):
       
   142         return u64(self.obj._p_oid)
       
   143 
       
   144     def isRoot(self):
       
   145         return IContainmentRoot.providedBy(self.obj)
       
   146 
       
   147     def getParentState(self):
       
   148         parent = self.getParent()
       
   149         if parent is None:
       
   150             return None
       
   151         else:
       
   152             return ZodbObjectState(parent, self.requestedTid)
       
   153 
       
   154 
       
   155 @implementer(IStateInterpreter)
       
   156 class LoadErrorState(object):
       
   157     """Placeholder for when an object's state could not be loaded"""
       
   158 
       
   159     def __init__(self, error, tid):
       
   160         self.error = error
       
   161         self.tid = tid
       
   162 
       
   163     def getError(self):
       
   164         return self.error
       
   165 
       
   166     def getName(self):
       
   167         return None
       
   168 
       
   169     def getParent(self):
       
   170         return None
       
   171 
       
   172     def listAttributes(self):
       
   173         return []
       
   174 
       
   175     def listItems(self):
       
   176         return None
       
   177 
       
   178     def asDict(self):
       
   179         return {}
       
   180 
       
   181 
       
   182 @adapter_config(context=(Interface, dict, None), provides=IStateInterpreter)
       
   183 @implementer(IStateInterpreter)
       
   184 class GenericState(object):
       
   185     """Most persistent objects represent their state as a dict."""
       
   186 
       
   187     def __init__(self, type, state, tid):
       
   188         self.state = state
       
   189         self.tid = tid
       
   190 
       
   191     def getError(self):
       
   192         return None
       
   193 
       
   194     def getName(self):
       
   195         return self.state.get('__name__')
       
   196 
       
   197     def getParent(self):
       
   198         return self.state.get('__parent__')
       
   199 
       
   200     def listAttributes(self):
       
   201         return list(self.state.items())
       
   202 
       
   203     def listItems(self):
       
   204         return None
       
   205 
       
   206     def asDict(self):
       
   207         return self.state
       
   208 
       
   209 
       
   210 @adapter_config(context=(PersistentMapping, dict, None), provides=IStateInterpreter)
       
   211 class PersistentMappingState(GenericState):
       
   212     """Convenient access to a persistent mapping's items."""
       
   213 
       
   214     def listItems(self):
       
   215         return sorted(self.state.get('data', {}).items())
       
   216 
       
   217 
       
   218 if PersistentMapping is PersistentDict:
       
   219     # ZODB 3.9 deprecated PersistentDict and made it an alias for
       
   220     # PersistentMapping.  I don't know a clean way to conditionally disable the
       
   221     # <adapter> directive in ZCML to avoid conflicting configuration actions,
       
   222     # therefore I'll register a decoy adapter registered for a decoy class.
       
   223     # This adapter will never get used.
       
   224 
       
   225     class DecoyPersistentDict(PersistentMapping):
       
   226         """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
       
   227 
       
   228     @adapter_config(context=(DecoyPersistentDict, dict, None), provides=IStateInterpreter)
       
   229     class PersistentDictState(PersistentMappingState):
       
   230         """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
       
   231 
       
   232 else:
       
   233 
       
   234     @adapter_config(context=(PersistentDict, dict, None), provides=IStateInterpreter)
       
   235     class PersistentDictState(PersistentMappingState):
       
   236         """Convenient access to a persistent dict's items."""
       
   237 
       
   238 
       
   239 @adapter_config(context=(SampleContainer, dict, None), provides=IStateInterpreter)
       
   240 class SampleContainerState(GenericState):
       
   241     """Convenient access to a SampleContainer's items"""
       
   242 
       
   243     def listItems(self):
       
   244         data = self.state.get('_SampleContainer__data')
       
   245         if not data:
       
   246             return []
       
   247         # data will be something persistent, maybe a PersistentDict, maybe a
       
   248         # OOBTree -- SampleContainer itself uses a plain Python dict, but
       
   249         # subclasses are supposed to overwrite the _newContainerData() method
       
   250         # and use something persistent.
       
   251         loadedstate = IObjectHistory(data).loadState(self.tid)
       
   252         request = check_request()
       
   253         return request.registry.getMultiAdapter((data, loadedstate, self.tid),
       
   254                                                 IStateInterpreter).listItems()
       
   255 
       
   256 
       
   257 @adapter_config(context=(OrderedContainer, dict, None), provides=IStateInterpreter)
       
   258 class OrderedContainerState(GenericState):
       
   259     """Convenient access to an OrderedContainer's items"""
       
   260 
       
   261     def listItems(self):
       
   262         # Now this is tricky: we want to construct a small object graph using
       
   263         # old state pickles without ever calling __setstate__ on a real
       
   264         # Persistent object, as _that_ would poison ZODB in-memory caches
       
   265         # in a nasty way (LP #487243).
       
   266         container = OrderedContainer()
       
   267         container.__setstate__(self.state)
       
   268         if isinstance(container._data, PersistentDict):
       
   269             old_data_state = IObjectHistory(container._data).loadState(self.tid)
       
   270             container._data = PersistentDict()
       
   271             container._data.__setstate__(old_data_state)
       
   272         if isinstance(container._order, PersistentList):
       
   273             old_order_state = IObjectHistory(container._order).loadState(self.tid)
       
   274             container._order = PersistentList()
       
   275             container._order.__setstate__(old_order_state)
       
   276         return list(container.items())
       
   277 
       
   278 
       
   279 @adapter_config(context=(ContainedProxy, tuple, None), provides=IStateInterpreter)
       
   280 class ContainedProxyState(GenericState):
       
   281 
       
   282     def __init__(self, proxy, state, tid):
       
   283         GenericState.__init__(self, proxy, state, tid)
       
   284         self.proxy = proxy
       
   285 
       
   286     def getName(self):
       
   287         return self.state[1]
       
   288 
       
   289     def getParent(self):
       
   290         return self.state[0]
       
   291 
       
   292     def listAttributes(self):
       
   293         return [('__name__', self.getName()),
       
   294                 ('__parent__', self.getParent()),
       
   295                 ('proxied_object', self.proxy.__getnewargs__()[0])]
       
   296 
       
   297     def listItems(self):
       
   298         return []
       
   299 
       
   300     def asDict(self):
       
   301         return dict(self.listAttributes())
       
   302 
       
   303 
       
   304 @adapter_config(context=(Interface, Interface, None), provides=IStateInterpreter)
       
   305 @implementer(IStateInterpreter)
       
   306 class FallbackState(object):
       
   307     """Fallback when we've got no idea how to interpret the state"""
       
   308 
       
   309     def __init__(self, type, state, tid):
       
   310         self.state = state
       
   311 
       
   312     def getError(self):
       
   313         return None
       
   314 
       
   315     def getName(self):
       
   316         return None
       
   317 
       
   318     def getParent(self):
       
   319         return None
       
   320 
       
   321     def listAttributes(self):
       
   322         return [('pickled state', self.state)]
       
   323 
       
   324     def listItems(self):
       
   325         return None
       
   326 
       
   327     def asDict(self):
       
   328         return dict(self.listAttributes())
       
   329