comparison piecrust/data/debug.py @ 3:f485ba500df3

Gigantic change to basically make PieCrust 2 vaguely functional. - Serving works, with debug window. - Baking works, multi-threading, with dependency handling. - Various things not implemented yet.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 10 Aug 2014 23:43:16 -0700
parents
children 474c9882decf
comparison
equal deleted inserted replaced
2:40fa08b261b9 3:f485ba500df3
1 import re
2 import cgi
3 import logging
4 import StringIO
5 from piecrust import APP_VERSION, PIECRUST_URL
6
7
8 logger = logging.getLogger(__name__)
9
10
11 css_id_re = re.compile(r'[^\w\d\-]+')
12
13
14 # CSS for the debug window.
15 CSS_DEBUGINFO = """
16 text-align: left;
17 font-style: normal;
18 padding: 1em;
19 background: #a42;
20 color: #fff;
21 position: fixed;
22 width: 50%;
23 bottom: 0;
24 right: 0;
25 overflow: auto;
26 max-height: 50%;
27 box-shadow: 0 0 10px #633;
28 """
29
30 # HTML elements.
31 CSS_P = 'margin: 0; padding: 0;'
32 CSS_A = 'color: #fff; text-decoration: none;'
33
34 # Headers.
35 CSS_BIGHEADER = 'margin: 0.5em 0; font-weight: bold;'
36 CSS_HEADER = 'margin: 0.5em 0; font-weight: bold;'
37
38 # Data block elements.
39 CSS_DATA = 'font-family: Courier, sans-serif; font-size: 0.9em;'
40 CSS_DATABLOCK = 'margin-left: 2em;'
41 CSS_VALUE = 'color: #fca;'
42 CSS_DOC = 'color: #fa8; font-size: 0.9em;'
43
44 # 'Baked with PieCrust' text
45 BRANDING_TEXT = 'Baked with <em><a href="%s">PieCrust</a> %s</em>.' % (
46 PIECRUST_URL, APP_VERSION)
47
48
49 def build_debug_info(page, data):
50 """ Generates HTML debug info for the given page's data.
51 """
52 output = StringIO.StringIO()
53 try:
54 _do_build_debug_info(page, data, output)
55 return output.getvalue()
56 finally:
57 output.close()
58
59
60 def _do_build_debug_info(page, data, output):
61 app = page.app
62 exec_info = app.env.exec_info_stack.current_page_info
63
64 print >>output, '<div id="piecrust-debug-info" style="%s">' % CSS_DEBUGINFO
65
66 print >>output, '<div>'
67 print >>output, '<p style="%s"><strong>PieCrust %s</strong> &mdash; ' % (CSS_P, APP_VERSION)
68
69 # If we have some execution info in the environment,
70 # add more information.
71 if exec_info:
72 if exec_info.was_cache_valid:
73 output.write('baked this morning')
74 else:
75 output.write('baked just now')
76
77 if app.cache.enabled:
78 if app.env.was_cache_cleaned:
79 output.write(', from a brand new cache')
80 else:
81 output.write(', from a valid cache')
82 else:
83 output.write(', with no cache')
84
85 else:
86 output.write('no caching information available')
87
88 output.write(', ')
89 if app.env.start_time != 0:
90 output.write('in __PIECRUST_TIMING_INFORMATION__')
91 else:
92 output.write('no timing information available')
93
94 print >>output, '</p>'
95 print >>output, '</div>'
96
97 if data:
98 print >>output, '<div>'
99 print >>output, ('<p style="%s cursor: pointer;" onclick="var l = '
100 'document.getElementById(\'piecrust-debug-details\'); '
101 'if (l.style.display == \'none\') l.style.display = '
102 '\'block\'; else l.style.display = \'none\';">' % CSS_P)
103 print >>output, ('<span style="%s">Template engine data</span> '
104 '&mdash; click to toggle</a>.</p>' % CSS_BIGHEADER)
105
106 print >>output, '<div id="piecrust-debug-details" style="display: none;">'
107 print >>output, ('<p style="%s">The following key/value pairs are '
108 'available in the layout\'s markup, and most are '
109 'available in the page\'s markup.</p>' % CSS_DOC)
110
111 filtered_data = dict(data)
112 for k in filtered_data.keys():
113 if k.startswith('__'):
114 del filtered_data[k]
115
116 renderer = DebugDataRenderer(output)
117 renderer.external_docs['data-site'] = (
118 "This section comes from the site configuration file.")
119 renderer.external_docs['data-page'] = (
120 "This section comes from the page's configuration header.")
121 renderer.renderData(filtered_data)
122
123 print >>output, '</div>'
124 print >>output, '</div>'
125
126 print >>output, '</div>'
127
128
129 class DebugDataRenderer(object):
130 MAX_VALUE_LENGTH = 150
131
132 def __init__(self, output):
133 self.indent = 0
134 self.output = output
135 self.external_docs = {}
136
137 def renderData(self, data):
138 if not isinstance(data, dict):
139 raise Exception("Expected top level data to be a dict.")
140 self._writeLine('<div style="%s">' % CSS_DATA)
141 self._renderDict(data, 'data')
142 self._writeLine('</div>')
143
144 def _renderValue(self, data, path):
145 if data is None:
146 self._write('&lt;null&gt;')
147 return
148
149 if isinstance(data, dict):
150 self._renderCollapsableValueStart(path)
151 with IndentScope(self):
152 self._renderDict(data, path)
153 self._renderCollapsableValueEnd()
154 return
155
156 if isinstance(data, list):
157 self._renderCollapsableValueStart(path)
158 with IndentScope(self):
159 self._renderList(data, path)
160 self._renderCollapsableValueEnd()
161 return
162
163 data_type = type(data)
164 if data_type is bool:
165 self._write('<span style="%s">%s</span>' % (CSS_VALUE,
166 'true' if bool(data) else 'false'))
167 return
168
169 if data_type is int:
170 self._write('<span style="%s">%d</span>' % (CSS_VALUE, data))
171 return
172
173 if data_type is float:
174 self._write('<span style="%s">%4.2f</span>' % (CSS_VALUE, data))
175 return
176
177 if data_type in (str, unicode):
178 if data_type == str:
179 data = data.decode('utf8')
180 if len(data) > DebugDataRenderer.MAX_VALUE_LENGTH:
181 data = data[:DebugDataRenderer.MAX_VALUE_LENGTH - 5]
182 data += '[...]'
183 data = cgi.escape(data).encode('ascii', 'xmlcharrefreplace')
184 self._write('<span style="%s">%s</span>' % (CSS_VALUE, data))
185 return
186
187 self._renderCollapsableValueStart(path)
188 with IndentScope(self):
189 self._renderObject(data, path)
190 self._renderCollapsableValueEnd()
191
192 def _renderList(self, data, path):
193 self._writeLine('<div style="%s">' % CSS_DATABLOCK)
194 self._renderDoc(data, path)
195 self._renderAttributes(data, path)
196 rendered_count = self._renderIterable(data, path, lambda d: enumerate(d))
197 if rendered_count == 0:
198 self._writeLine('<p style="%s %s">(empty array)</p>' % (CSS_P, CSS_DOC))
199 self._writeLine('</div>')
200
201 def _renderDict(self, data, path):
202 self._writeLine('<div style="%s">' % CSS_DATABLOCK)
203 self._renderDoc(data, path)
204 self._renderAttributes(data, path)
205 rendered_count = self._renderIterable(data, path,
206 lambda d: sorted(d.iteritems(), key=lambda i: i[0]))
207 if rendered_count == 0:
208 self._writeLine('<p style="%s %s">(empty dictionary)</p>' % (CSS_P, CSS_DOC))
209 self._writeLine('</div>')
210
211 def _renderObject(self, data, path):
212 if hasattr(data.__class__, 'debug_render_func'):
213 # This object wants to be rendered as a simple string...
214 render_func_name = data.__class__.debug_render_func
215 render_func = getattr(data, render_func_name)
216 value = render_func()
217 self._renderValue(value, path)
218 return
219
220 self._writeLine('<div style="%s">' % CSS_DATABLOCK)
221 self._renderDoc(data, path)
222 rendered_attrs = self._renderAttributes(data, path)
223
224 if (hasattr(data, '__iter__') and
225 hasattr(data.__class__, 'debug_render_items') and
226 data.__class__.debug_render_items):
227 rendered_count = self._renderIterable(data, path,
228 lambda d: enumerate(d))
229 if rendered_count == 0:
230 self._writeLine('<p style="%s %s">(empty)</p>' % (CSS_P, CSS_DOC))
231
232 elif rendered_attrs == 0:
233 self._writeLine('<p style="%s %s">(empty)</p>' % (CSS_P, CSS_DOC))
234
235 self._writeLine('</div>')
236
237 def _renderIterable(self, data, path, iter_func):
238 rendered_count = 0
239 with IndentScope(self):
240 for i, item in iter_func(data):
241 self._writeStart('<div>%s' % i)
242 if item is not None:
243 self._write(' : ')
244 self._renderValue(item, self._makePath(path, i))
245 self._writeEnd('</div>')
246 rendered_count += 1
247 return rendered_count
248
249 def _renderDoc(self, data, path):
250 if hasattr(data.__class__, 'debug_render_doc'):
251 self._writeLine('<span style="%s">&ndash; %s</span>' %
252 (CSS_DOC, data.__class__.debug_render_doc))
253
254 doc = self.external_docs.get(path)
255 if doc is not None:
256 self._writeLine('<span style="%s">&ndash; %s</span>' %
257 (CSS_DOC, doc))
258
259 def _renderAttributes(self, data, path):
260 if not hasattr(data.__class__, 'debug_render'):
261 return 0
262
263 attr_names = list(data.__class__.debug_render)
264 if hasattr(data.__class__, 'debug_render_dynamic'):
265 drd = data.__class__.debug_render_dynamic
266 for ng in drd:
267 name_gen = getattr(data, ng)
268 attr_names += name_gen()
269
270 invoke_attrs = []
271 if hasattr(data.__class__, 'debug_render_invoke'):
272 invoke_attrs = list(data.__class__.debug_render_invoke)
273 if hasattr(data.__class__, 'debug_render_invoke_dynamic'):
274 drid = data.__class__.debug_render_invoke_dynamic
275 for ng in drid:
276 name_gen = getattr(data, ng)
277 invoke_attrs += name_gen()
278
279 rendered_count = 0
280 for name in attr_names:
281 value = None
282 render_name = name
283 should_call = name in invoke_attrs
284
285 try:
286 attr = getattr(data.__class__, name)
287 except AttributeError:
288 # This could be an attribute on the instance itself, or some
289 # dynamic attribute.
290 attr = getattr(data, name)
291
292 if callable(attr):
293 attr_func = getattr(data, name)
294 argcount = attr_func.__code__.co_argcount
295 var_names = attr_func.__code__.co_varnames
296 if argcount == 1 and should_call:
297 render_name += '()'
298 value = attr_func()
299 else:
300 if should_call:
301 logger.warning("Method '%s' should be invoked for "
302 "rendering, but it has %s arguments." %
303 (name, argcount))
304 should_call = False
305 render_name += '(%s)' % ','.join(var_names[1:])
306 elif should_call:
307 value = getattr(data, name)
308
309 self._writeLine('<div>%s' % render_name)
310 with IndentScope(self):
311 if should_call:
312 self._write(' : ')
313 self._renderValue(value, self._makePath(path, name))
314 self._writeLine('</div>')
315 rendered_count += 1
316
317 return rendered_count
318
319 def _renderCollapsableValueStart(self, path):
320 self._writeLine('<span style="cursor: pointer;" onclick="var l = '
321 'document.getElementById(\'piecrust-debug-data-%s\'); '
322 'if (l.style.display == \'none\') {'
323 ' l.style.display = \'block\';'
324 ' this.innerHTML = \'[-]\';'
325 '} else {'
326 ' l.style.display = \'none\';'
327 ' this.innerHTML = \'[+]\';'
328 '}">'
329 '[+]'
330 '</span>' %
331 path)
332 self._writeLine('<div style="display: none"'
333 'id="piecrust-debug-data-%s">' % path)
334
335 def _renderCollapsableValueEnd(self):
336 self._writeLine('</div>')
337
338 def _makePath(self, parent_path, key):
339 return '%s-%s' % (parent_path, css_id_re.sub('-', str(key)))
340
341 def _writeLine(self, msg):
342 self.output.write(self.indent * ' ')
343 self.output.write(msg)
344 self.output.write('\n')
345
346 def _writeStart(self, msg=None):
347 self.output.write(self.indent * ' ')
348 if msg is not None:
349 self.output.write(msg)
350
351 def _write(self, msg):
352 self.output.write(msg)
353
354 def _writeEnd(self, msg=None):
355 if msg is not None:
356 self.output.write(msg)
357 self.output.write('\n')
358
359
360 class IndentScope(object):
361 def __init__(self, target):
362 self.target = target
363
364 def __enter__(self):
365 self.target.indent += 1
366 return self
367
368 def __exit__(self, exc_type, exc_val, exc_tb):
369 self.target.indent -= 1
370