|
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 import itertools |
|
19 import collections |
|
20 import re |
|
21 from html import escape |
|
22 |
|
23 # import interfaces |
|
24 from pyams_zodbbrowser.interfaces import IValueRenderer, IObjectHistory |
|
25 |
|
26 # import packages |
|
27 from persistent import Persistent |
|
28 from persistent.dict import PersistentDict |
|
29 from persistent.list import PersistentList |
|
30 from persistent.mapping import PersistentMapping |
|
31 from pyams_utils.adapter import adapter_config |
|
32 from ZODB.utils import u64, oid_repr |
|
33 from zope.interface import implementer, Interface |
|
34 from zope.interface.declarations import ProvidesClass |
|
35 |
|
36 |
|
37 log = logging.getLogger(__name__) |
|
38 |
|
39 |
|
40 MAX_CACHE_SIZE = 1000 |
|
41 TRUNCATIONS = {} |
|
42 TRUNCATIONS_IN_ORDER = collections.deque() |
|
43 next_id = itertools.count(1).__next__ |
|
44 |
|
45 |
|
46 def resetTruncations(): # for tests only! |
|
47 global next_id |
|
48 next_id = itertools.count(1).__next__ |
|
49 TRUNCATIONS.clear() |
|
50 TRUNCATIONS_IN_ORDER.clear() |
|
51 |
|
52 |
|
53 def pruneTruncations(): |
|
54 while len(TRUNCATIONS_IN_ORDER) > MAX_CACHE_SIZE: |
|
55 del TRUNCATIONS[TRUNCATIONS_IN_ORDER.popleft()] |
|
56 |
|
57 |
|
58 def truncate(text): |
|
59 id = 'tr%d' % next_id() |
|
60 TRUNCATIONS[id] = text |
|
61 TRUNCATIONS_IN_ORDER.append(id) |
|
62 return id |
|
63 |
|
64 |
|
65 @adapter_config(context=Interface, provides=IValueRenderer) |
|
66 @implementer(IValueRenderer) |
|
67 class GenericValue(object): |
|
68 """Default value renderer. |
|
69 |
|
70 Uses the object's __repr__, truncating if too long. |
|
71 """ |
|
72 |
|
73 def __init__(self, context): |
|
74 self.context = context |
|
75 |
|
76 def _repr(self): |
|
77 # hook for subclasses |
|
78 if getattr(self.context.__class__, '__repr__', None) is object.__repr__: |
|
79 # Special-case objects with the default __repr__ (LP#1087138) |
|
80 if isinstance(self.context, Persistent): |
|
81 return '<%s.%s with oid %s>' % ( |
|
82 self.context.__class__.__module__, |
|
83 self.context.__class__.__name__, |
|
84 oid_repr(self.context._p_oid)) |
|
85 try: |
|
86 return repr(self.context) |
|
87 except Exception: |
|
88 try: |
|
89 return '<unrepresentable %s>' % self.context.__class__.__name__ |
|
90 except Exception: |
|
91 return '<unrepresentable>' |
|
92 |
|
93 def render(self, tid=None, can_link=True, limit=200): |
|
94 text = self._repr() |
|
95 if len(text) > limit: |
|
96 id = truncate(text[limit:]) |
|
97 text = '%s<span id="%s" class="truncated">...</span>' % ( |
|
98 escape(text[:limit]), id) |
|
99 else: |
|
100 text = escape(text) |
|
101 if not isinstance(self.context, str): |
|
102 try: |
|
103 n = len(self.context) |
|
104 except Exception: |
|
105 pass |
|
106 else: |
|
107 if n == 1: # this is a crime against i18n, but oh well |
|
108 text += ' (%d item)' % n |
|
109 else: |
|
110 text += ' (%d items)' % n |
|
111 return text |
|
112 |
|
113 |
|
114 def join_with_commas(html, open, close): |
|
115 """Helper to join multiple html snippets into a struct.""" |
|
116 prefix = open + '<span class="struct">' |
|
117 suffix = '</span>' |
|
118 for n, item in enumerate(html): |
|
119 if n == len(html) - 1: |
|
120 trailer = close |
|
121 else: |
|
122 trailer = ',' |
|
123 if item.endswith(suffix): |
|
124 item = item[:-len(suffix)] + trailer + suffix |
|
125 else: |
|
126 item += trailer |
|
127 html[n] = item |
|
128 return prefix + '<br />'.join(html) + suffix |
|
129 |
|
130 |
|
131 @adapter_config(context=str, provides=IValueRenderer) |
|
132 class StringValue(GenericValue): |
|
133 """String renderer.""" |
|
134 |
|
135 def __init__(self, context): |
|
136 self.context = context |
|
137 |
|
138 def render(self, tid=None, can_link=True, limit=200, threshold=4): |
|
139 if self.context.count('\n') <= threshold: |
|
140 return GenericValue.render(self, tid, can_link=can_link, |
|
141 limit=limit) |
|
142 else: |
|
143 if isinstance(self.context, str): |
|
144 prefix = 'u' |
|
145 context = self.context |
|
146 else: |
|
147 prefix = '' |
|
148 context = self.context.decode('latin-1').encode('ascii', |
|
149 'backslashreplace') |
|
150 lines = [re.sub(r'^[ \t]+', |
|
151 lambda m: ' ' * len(m.group(0).expandtabs()), |
|
152 escape(line)) |
|
153 for line in context.splitlines()] |
|
154 nl = '<br />' # hm, maybe '\\n<br />'? |
|
155 if sum(map(len, lines)) > limit: |
|
156 head = nl.join(lines[:5]) |
|
157 tail = nl.join(lines[5:]) |
|
158 id = truncate(tail) |
|
159 return (prefix + "'<span class=\"struct\">" + head + nl |
|
160 + '<span id="%s" class="truncated">...</span>' % id |
|
161 + "'</span>") |
|
162 else: |
|
163 return (prefix + "'<span class=\"struct\">" + nl.join(lines) |
|
164 + "'</span>") |
|
165 |
|
166 |
|
167 @adapter_config(context=tuple, provides=IValueRenderer) |
|
168 @implementer(IValueRenderer) |
|
169 class TupleValue(object): |
|
170 """Tuple renderer.""" |
|
171 |
|
172 def __init__(self, context): |
|
173 self.context = context |
|
174 |
|
175 def render(self, tid=None, can_link=True, threshold=100): |
|
176 html = [] |
|
177 for item in self.context: |
|
178 html.append(IValueRenderer(item).render(tid, can_link)) |
|
179 if len(html) == 1: |
|
180 html.append('') # (item) -> (item, ) |
|
181 result = '(%s)' % ', '.join(html) |
|
182 if len(result) > threshold or '<span class="struct">' in result: |
|
183 if len(html) == 2 and html[1] == '': |
|
184 return join_with_commas(html[:1], '(', ', )') |
|
185 else: |
|
186 return join_with_commas(html, '(', ')') |
|
187 return result |
|
188 |
|
189 |
|
190 @adapter_config(context=list, provides=IValueRenderer) |
|
191 @implementer(IValueRenderer) |
|
192 class ListValue(object): |
|
193 """List renderer.""" |
|
194 |
|
195 def __init__(self, context): |
|
196 self.context = context |
|
197 |
|
198 def render(self, tid=None, can_link=True, threshold=100): |
|
199 html = [] |
|
200 for item in self.context: |
|
201 html.append(IValueRenderer(item).render(tid, can_link)) |
|
202 result = '[%s]' % ', '.join(html) |
|
203 if len(result) > threshold or '<span class="struct">' in result: |
|
204 return join_with_commas(html, '[', ']') |
|
205 return result |
|
206 |
|
207 |
|
208 @adapter_config(context=dict, provides=IValueRenderer) |
|
209 @implementer(IValueRenderer) |
|
210 class DictValue(object): |
|
211 """Dict renderer.""" |
|
212 |
|
213 def __init__(self, context): |
|
214 self.context = context |
|
215 |
|
216 def render(self, tid=None, can_link=True, threshold=100): |
|
217 html = [] |
|
218 for key, value in sorted(self.context.items()): |
|
219 html.append(IValueRenderer(key).render(tid, can_link) + ': ' + |
|
220 IValueRenderer(value).render(tid, can_link)) |
|
221 if (sum(map(len, html)) < threshold and |
|
222 '<span class="struct">' not in ''.join(html)): |
|
223 return '{%s}' % ', '.join(html) |
|
224 else: |
|
225 return join_with_commas(html, '{', '}') |
|
226 |
|
227 |
|
228 @adapter_config(context=Persistent, provides=IValueRenderer) |
|
229 @implementer(IValueRenderer) |
|
230 class PersistentValue(object): |
|
231 """Persistent object renderer. |
|
232 |
|
233 Uses __repr__ and makes it a hyperlink to the actual object. |
|
234 """ |
|
235 |
|
236 view_name = '#zodbbrowser' |
|
237 delegate_to = GenericValue |
|
238 |
|
239 def __init__(self, context): |
|
240 self.context = context |
|
241 |
|
242 def render(self, tid=None, can_link=True): |
|
243 obj = self.context |
|
244 url = '%s?oid=0x%x' % (self.view_name, u64(self.context._p_oid)) |
|
245 if tid is not None: |
|
246 url += "&tid=%d" % u64(tid) |
|
247 try: |
|
248 oldstate = IObjectHistory(self.context).loadState(tid) |
|
249 clone = self.context.__class__.__new__(self.context.__class__) |
|
250 clone.__setstate__(oldstate) |
|
251 clone._p_oid = self.context._p_oid |
|
252 obj = clone |
|
253 except Exception: |
|
254 log.debug('Could not load old state for %s 0x%x', |
|
255 self.context.__class__, u64(self.context._p_oid)) |
|
256 value = self.delegate_to(obj).render(tid, can_link=False) |
|
257 if can_link: |
|
258 return '<a class="objlink" href="%s">%s</a>' % (escape(url), value) |
|
259 else: |
|
260 return value |
|
261 |
|
262 |
|
263 @adapter_config(context=PersistentMapping, provides=IValueRenderer) |
|
264 class PersistentMappingValue(PersistentValue): |
|
265 delegate_to = DictValue |
|
266 |
|
267 |
|
268 @adapter_config(context=PersistentList, provides=IValueRenderer) |
|
269 class PersistentListValue(PersistentValue): |
|
270 delegate_to = ListValue |
|
271 |
|
272 |
|
273 if PersistentMapping is PersistentDict: |
|
274 # ZODB 3.9 deprecated PersistentDict and made it an alias for |
|
275 # PersistentMapping. I don't know a clean way to conditionally disable the |
|
276 # <adapter> directive in ZCML to avoid conflicting configuration actions, |
|
277 # therefore I'll register a decoy adapter registered for a decoy class. |
|
278 # This adapter will never get used. |
|
279 |
|
280 class DecoyPersistentDict(PersistentMapping): |
|
281 """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9.""" |
|
282 |
|
283 @adapter_config(context=DecoyPersistentDict, provides=IValueRenderer) |
|
284 class PersistentDictValue(PersistentValue): |
|
285 """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9.""" |
|
286 delegate_to = DictValue |
|
287 |
|
288 else: |
|
289 @adapter_config(context=PersistentDict, provides=IValueRenderer) |
|
290 class PersistentDictValue(PersistentValue): |
|
291 delegate_to = DictValue |
|
292 |
|
293 |
|
294 @adapter_config(context=ProvidesClass, provides=IValueRenderer) |
|
295 class ProvidesValue(GenericValue): |
|
296 """zope.interface.Provides object renderer. |
|
297 |
|
298 The __repr__ of zope.interface.Provides is decidedly unhelpful. |
|
299 """ |
|
300 |
|
301 def _repr(self): |
|
302 return '<Provides: %s>' % ', '.join(i.__identifier__ |
|
303 for i in self.context._Provides__args[1:]) |
|
304 |