Mercurial > piecrust2
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> — ' % (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 '— 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('<null>') | |
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">– %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">– %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 |