|
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 |