comparison piecrust/data/debug.py @ 556:93b656f0af54

serve: Improve debug information in the preview server. Now the debug window only loads debug info on demand.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 12 Aug 2015 23:18:35 -0700
parents 32c7c2d219d2
children eedd63b7cf42
comparison
equal deleted inserted replaced
555:daf8df5ade7d 556:93b656f0af54
12 12
13 13
14 css_id_re = re.compile(r'[^\w\d\-]+') 14 css_id_re = re.compile(r'[^\w\d\-]+')
15 15
16 16
17 # CSS for the debug window. 17 CSS_DATA = 'piecrust-debug-info-data'
18 CSS_DEBUGWINDOW = """ 18 CSS_DATABLOCK = 'piecrust-debug-info-datablock'
19 text-align: left; 19 CSS_VALUE = 'piecrust-debug-info-datavalue'
20 font-family: serif; 20 CSS_DOC = 'piecrust-debug-info-doc'
21 font-style: normal; 21 CSS_BIGHEADER = 'piecrust-debug-info-header1'
22 font-weight: normal; 22 CSS_HEADER = 'piecrust-debug-info-header2'
23 position: fixed; 23
24 width: 50%;
25 bottom: 0;
26 right: 0;
27 overflow: auto;
28 max-height: 50%;
29 box-shadow: 0 0 10px #633;
30 """
31
32 CSS_PIPELINESTATUS = """
33 background: #fff;
34 color: #a22;
35 """
36
37 CSS_DEBUGINFO = """
38 padding: 1em;
39 background: #a42;
40 color: #fff;
41 """
42
43 # HTML elements.
44 CSS_P = 'margin: 0; padding: 0;'
45 CSS_A = 'color: #fff; text-decoration: none;'
46
47 # Headers.
48 CSS_BIGHEADER = 'margin: 0.5em 0; font-weight: bold;'
49 CSS_HEADER = 'margin: 0.5em 0; font-weight: bold;'
50
51 # Data block elements.
52 CSS_DATA = 'font-family: Courier, sans-serif; font-size: 0.9em;'
53 CSS_DATABLOCK = 'margin-left: 2em;'
54 CSS_VALUE = 'color: #fca;'
55 CSS_DOC = 'color: #fa8; font-size: 0.9em;'
56 24
57 # 'Baked with PieCrust' text 25 # 'Baked with PieCrust' text
58 BRANDING_TEXT = 'Baked with <em><a href="%s">PieCrust</a> %s</em>.' % ( 26 BRANDING_TEXT = 'Baked with <em><a href="%s">PieCrust</a> %s</em>.' % (
59 PIECRUST_URL, APP_VERSION) 27 PIECRUST_URL, APP_VERSION)
60 28
61 29
62 def build_debug_info(page, data): 30 def build_debug_info(page):
63 """ Generates HTML debug info for the given page's data. 31 """ Generates HTML debug info for the given page's data.
64 """ 32 """
65 output = io.StringIO() 33 output = io.StringIO()
66 try: 34 try:
67 _do_build_debug_info(page, data, output) 35 _do_build_debug_info(page, output)
68 return output.getvalue() 36 return output.getvalue()
69 finally: 37 finally:
70 output.close() 38 output.close()
71 39
72 40
73 def _do_build_debug_info(page, data, output): 41 def _do_build_debug_info(page, output):
74 app = page.app 42 app = page.app
75 43
76 print('<div id="piecrust-debug-info" style="%s">' % CSS_DEBUGWINDOW, 44 print('<div id="piecrust-debug-window" class="piecrust-debug-window">',
77 file=output) 45 file=output)
78 46
79 print('<div id="piecrust-debug-info-pipeline-status" style="%s">' % 47 print('<div><strong>PieCrust %s</strong> &mdash; ' % APP_VERSION,
80 CSS_PIPELINESTATUS, file=output) 48 file=output)
81 print('</div>', file=output)
82
83 print('<div style="%s">' % CSS_DEBUGINFO, file=output)
84 print('<p style="%s"><strong>PieCrust %s</strong> &mdash; ' %
85 (CSS_P, APP_VERSION), file=output)
86 49
87 # If we have some execution info in the environment, 50 # If we have some execution info in the environment,
88 # add more information. 51 # add more information.
89 if page.flags & FLAG_RAW_CACHE_VALID: 52 if page.flags & FLAG_RAW_CACHE_VALID:
90 output.write('baked this morning') 53 output.write('baked this morning')
99 else: 62 else:
100 output.write(', with no cache') 63 output.write(', with no cache')
101 64
102 output.write(', ') 65 output.write(', ')
103 if app.env.start_time != 0: 66 if app.env.start_time != 0:
104 output.write('in __PIECRUST_TIMING_INFORMATION__') 67 output.write('in __PIECRUST_TIMING_INFORMATION__. ')
105 else: 68 else:
106 output.write('no timing information available') 69 output.write('no timing information available. ')
107 70
108 print('</p>', file=output) 71 print('<a class="piecrust-debug-expander" hred="#">[+]</a>',
72 file=output)
73
109 print('</div>', file=output) 74 print('</div>', file=output)
110 75
111 if data: 76 print('<div class="piecrust-debug-info piecrust-debug-info-unloaded">',
112 print('<div style="%s padding-top: 0;">' % CSS_DEBUGINFO, file=output) 77 file=output)
113 print(('<p style="%s cursor: pointer;" onclick="var l = '
114 'document.getElementById(\'piecrust-debug-details\'); '
115 'if (l.style.display == \'none\') l.style.display = '
116 '\'block\'; else l.style.display = \'none\';">' % CSS_P), file=output)
117 print(('<span style="%s">Template engine data</span> '
118 '&mdash; click to toggle</a>.</p>' % CSS_BIGHEADER), file=output)
119
120 print('<div id="piecrust-debug-details" style="display: none;">', file=output)
121 print(('<p style="%s">The following key/value pairs are '
122 'available in the layout\'s markup, and most are '
123 'available in the page\'s markup.</p>' % CSS_DOC), file=output)
124
125 filtered_data = dict(data)
126 for k in list(filtered_data.keys()):
127 if k.startswith('__'):
128 del filtered_data[k]
129
130 renderer = DebugDataRenderer(output)
131 renderer.external_docs['data-site'] = (
132 "This section comes from the site configuration file.")
133 renderer.external_docs['data-page'] = (
134 "This section comes from the page's configuration header.")
135 renderer.renderData(filtered_data)
136
137 print('</div>', file=output)
138 print('</div>', file=output)
139
140 print('</div>', file=output) 78 print('</div>', file=output)
141 79
80 print('</div>', file=output)
81
142 print('<script src="/__piecrust_static/piecrust-debug-info.js"></script>', 82 print('<script src="/__piecrust_static/piecrust-debug-info.js"></script>',
143 file=output) 83 file=output)
84
85
86 def build_var_debug_info(data, var_path=None):
87 output = io.StringIO()
88 try:
89 _do_build_var_debug_info(data, output, var_path)
90 return output.getvalue()
91 finally:
92 output.close()
93
94
95 def _do_build_var_debug_info(data, output, var_path=None):
96 if False:
97 print('<html>', file=output)
98 print('<head>', file=output)
99 print('<meta charset="utf-8" />', file=output)
100 print('<link rel="stylesheet" type="text/css" '
101 'href="/__piecrust_static/piecrust-debug-info.css" />',
102 file=output)
103 print('</head>', file=output)
104 print('<body class="piecrust-debug-page">', file=output)
105
106 #print('<div class="piecrust-debug-info">', file=output)
107 #print(('<p style="cursor: pointer;" onclick="var l = '
108 # 'document.getElementById(\'piecrust-debug-details\'); '
109 # 'if (l.style.display == \'none\') l.style.display = '
110 # '\'block\'; else l.style.display = \'none\';">'),
111 # file=output)
112 #print(('<span class="%s">Template engine data</span> '
113 # '&mdash; click to toggle</a>.</p>' % CSS_BIGHEADER), file=output)
114
115 #print('<div id="piecrust-debug-details" style="display: none;">',
116 # file=output)
117 #print(('<p class="%s">The following key/value pairs are '
118 # 'available in the layout\'s markup, and most are '
119 # 'available in the page\'s markup.</p>' % CSS_DOC), file=output)
120
121 filtered_data = dict(data)
122 for k in list(filtered_data.keys()):
123 if k.startswith('__'):
124 del filtered_data[k]
125
126 renderer = DebugDataRenderer(output)
127 renderer.external_docs['data-site'] = (
128 "This section comes from the site configuration file.")
129 renderer.external_docs['data-page'] = (
130 "This section comes from the page's configuration header.")
131 renderer.renderData(filtered_data)
132
133 print('</div>', file=output)
134 #print('</div>', file=output)
135
136 if False:
137 print('</body>', file=output)
138 print('</html>', file=output)
144 139
145 140
146 class DebugDataRenderer(object): 141 class DebugDataRenderer(object):
147 MAX_VALUE_LENGTH = 150 142 MAX_VALUE_LENGTH = 150
148 143
152 self.external_docs = {} 147 self.external_docs = {}
153 148
154 def renderData(self, data): 149 def renderData(self, data):
155 if not isinstance(data, dict): 150 if not isinstance(data, dict):
156 raise Exception("Expected top level data to be a dict.") 151 raise Exception("Expected top level data to be a dict.")
157 self._writeLine('<div style="%s">' % CSS_DATA) 152 self._writeLine('<div class="%s">' % CSS_DATA)
158 self._renderDict(data, 'data') 153 self._renderDict(data, 'data')
159 self._writeLine('</div>') 154 self._writeLine('</div>')
160 155
161 def _renderValue(self, data, path): 156 def _renderValue(self, data, path):
162 if data is None: 157 if data is None:
177 self._renderCollapsableValueEnd() 172 self._renderCollapsableValueEnd()
178 return 173 return
179 174
180 data_type = type(data) 175 data_type = type(data)
181 if data_type is bool: 176 if data_type is bool:
182 self._write('<span style="%s">%s</span>' % (CSS_VALUE, 177 self._write('<span class="%s">%s</span>' % (CSS_VALUE,
183 'true' if bool(data) else 'false')) 178 'true' if bool(data) else 'false'))
184 return 179 return
185 180
186 if data_type is int: 181 if data_type is int:
187 self._write('<span style="%s">%d</span>' % (CSS_VALUE, data)) 182 self._write('<span class="%s">%d</span>' % (CSS_VALUE, data))
188 return 183 return
189 184
190 if data_type is float: 185 if data_type is float:
191 self._write('<span style="%s">%4.2f</span>' % (CSS_VALUE, data)) 186 self._write('<span class="%s">%4.2f</span>' % (CSS_VALUE, data))
192 return 187 return
193 188
194 if data_type is str: 189 if data_type is str:
195 if len(data) > DebugDataRenderer.MAX_VALUE_LENGTH: 190 if len(data) > DebugDataRenderer.MAX_VALUE_LENGTH:
196 data = data[:DebugDataRenderer.MAX_VALUE_LENGTH - 5] 191 data = data[:DebugDataRenderer.MAX_VALUE_LENGTH - 5]
197 data += '[...]' 192 data += '[...]'
198 data = html.escape(data) 193 data = html.escape(data)
199 self._write('<span style="%s">%s</span>' % (CSS_VALUE, data)) 194 self._write('<span class="%s">%s</span>' % (CSS_VALUE, data))
200 return 195 return
201 196
202 self._renderCollapsableValueStart(path) 197 self._renderCollapsableValueStart(path)
203 with IndentScope(self): 198 with IndentScope(self):
204 self._renderObject(data, path) 199 self._renderObject(data, path)
205 self._renderCollapsableValueEnd() 200 self._renderCollapsableValueEnd()
206 201
207 def _renderList(self, data, path): 202 def _renderList(self, data, path):
208 self._writeLine('<div style="%s">' % CSS_DATABLOCK) 203 self._writeLine('<div class="%s">' % CSS_DATABLOCK)
209 self._renderDoc(data, path) 204 self._renderDoc(data, path)
210 self._renderAttributes(data, path) 205 self._renderAttributes(data, path)
211 rendered_count = self._renderIterable(data, path, lambda d: enumerate(d)) 206 rendered_count = self._renderIterable(
207 data, path, lambda d: enumerate(d))
212 if (rendered_count == 0 and 208 if (rendered_count == 0 and
213 not hasattr(data.__class__, 'debug_render_not_empty')): 209 not hasattr(data.__class__, 'debug_render_not_empty')):
214 self._writeLine('<p style="%s %s">(empty array)</p>' % (CSS_P, CSS_DOC)) 210 self._writeLine('<p class="%s">(empty array)</p>' % (CSS_DOC))
215 self._writeLine('</div>') 211 self._writeLine('</div>')
216 212
217 def _renderDict(self, data, path): 213 def _renderDict(self, data, path):
218 self._writeLine('<div style="%s">' % CSS_DATABLOCK) 214 self._writeLine('<div class="%s">' % CSS_DATABLOCK)
219 self._renderDoc(data, path) 215 self._renderDoc(data, path)
220 self._renderAttributes(data, path) 216 self._renderAttributes(data, path)
221 rendered_count = self._renderIterable(data, path, 217 rendered_count = self._renderIterable(
218 data, path,
222 lambda d: sorted(iter(d.items()), key=lambda i: i[0])) 219 lambda d: sorted(iter(d.items()), key=lambda i: i[0]))
223 if (rendered_count == 0 and 220 if (rendered_count == 0 and
224 not hasattr(data.__class__, 'debug_render_not_empty')): 221 not hasattr(data.__class__, 'debug_render_not_empty')):
225 self._writeLine('<p style="%s %s">(empty dictionary)</p>' % (CSS_P, CSS_DOC)) 222 self._writeLine('<p class="%s %s">(empty dictionary)</p>' %
223 CSS_DOC)
226 self._writeLine('</div>') 224 self._writeLine('</div>')
227 225
228 def _renderObject(self, data, path): 226 def _renderObject(self, data, path):
229 if hasattr(data.__class__, 'debug_render_func'): 227 if hasattr(data.__class__, 'debug_render_func'):
230 # This object wants to be rendered as a simple string... 228 # This object wants to be rendered as a simple string...
232 render_func = getattr(data, render_func_name) 230 render_func = getattr(data, render_func_name)
233 value = render_func() 231 value = render_func()
234 self._renderValue(value, path) 232 self._renderValue(value, path)
235 return 233 return
236 234
237 self._writeLine('<div style="%s">' % CSS_DATABLOCK) 235 self._writeLine('<div class="%s">' % CSS_DATABLOCK)
238 self._renderDoc(data, path) 236 self._renderDoc(data, path)
239 rendered_attrs = self._renderAttributes(data, path) 237 rendered_attrs = self._renderAttributes(data, path)
240 238
241 if (hasattr(data, '__iter__') and 239 if (hasattr(data, '__iter__') and
242 hasattr(data.__class__, 'debug_render_items') and 240 hasattr(data.__class__, 'debug_render_items') and
243 data.__class__.debug_render_items): 241 data.__class__.debug_render_items):
244 rendered_count = self._renderIterable(data, path, 242 rendered_count = self._renderIterable(
245 lambda d: enumerate(d)) 243 data, path, lambda d: enumerate(d))
246 if (rendered_count == 0 and 244 if (rendered_count == 0 and
247 not hasattr(data.__class__, 'debug_render_not_empty')): 245 not hasattr(data.__class__, 'debug_render_not_empty')):
248 self._writeLine('<p style="%s %s">(empty)</p>' % (CSS_P, CSS_DOC)) 246 self._writeLine('<p class="%s">(empty)</p>' % CSS_DOC)
249 247
250 elif (rendered_attrs == 0 and 248 elif (rendered_attrs == 0 and
251 not hasattr(data.__class__, 'debug_render_not_empty')): 249 not hasattr(data.__class__, 'debug_render_not_empty')):
252 self._writeLine('<p style="%s %s">(empty)</p>' % (CSS_P, CSS_DOC)) 250 self._writeLine('<p class="%s">(empty)</p>' % CSS_DOC)
253 251
254 self._writeLine('</div>') 252 self._writeLine('</div>')
255 253
256 def _renderIterable(self, data, path, iter_func): 254 def _renderIterable(self, data, path, iter_func):
257 rendered_count = 0 255 rendered_count = 0
265 rendered_count += 1 263 rendered_count += 1
266 return rendered_count 264 return rendered_count
267 265
268 def _renderDoc(self, data, path): 266 def _renderDoc(self, data, path):
269 if hasattr(data.__class__, 'debug_render_doc'): 267 if hasattr(data.__class__, 'debug_render_doc'):
270 self._writeLine('<span style="%s">&ndash; %s</span>' % 268 self._writeLine('<span class="%s">&ndash; %s</span>' %
271 (CSS_DOC, data.__class__.debug_render_doc)) 269 (CSS_DOC, data.__class__.debug_render_doc))
272 270
273 if hasattr(data.__class__, 'debug_render_doc_dynamic'): 271 if hasattr(data.__class__, 'debug_render_doc_dynamic'):
274 drdd = data.__class__.debug_render_doc_dynamic 272 drdd = data.__class__.debug_render_doc_dynamic
275 for ng in drdd: 273 for ng in drdd:
276 doc = getattr(data, ng) 274 doc = getattr(data, ng)
277 self._writeLine('<span style="%s">&ndash; %s</span>' % 275 self._writeLine('<span class="%s">&ndash; %s</span>' %
278 (CSS_DOC, doc())) 276 (CSS_DOC, doc()))
279 277
280 doc = self.external_docs.get(path) 278 doc = self.external_docs.get(path)
281 if doc is not None: 279 if doc is not None:
282 self._writeLine('<span style="%s">&ndash; %s</span>' % 280 self._writeLine('<span class="%s">&ndash; %s</span>' %
283 (CSS_DOC, doc)) 281 (CSS_DOC, doc))
284 282
285 def _renderAttributes(self, data, path): 283 def _renderAttributes(self, data, path):
286 if not hasattr(data.__class__, 'debug_render'): 284 if not hasattr(data.__class__, 'debug_render'):
287 return 0 285 return 0
288 286
353 351
354 return rendered_count 352 return rendered_count
355 353
356 def _renderCollapsableValueStart(self, path): 354 def _renderCollapsableValueStart(self, path):
357 self._writeLine('<span style="cursor: pointer;" onclick="var l = ' 355 self._writeLine('<span style="cursor: pointer;" onclick="var l = '
358 'document.getElementById(\'piecrust-debug-data-%s\'); ' 356 'document.getElementById(\'piecrust-debug-data-%s\'); '
359 'if (l.style.display == \'none\') {' 357 'if (l.style.display == \'none\') {'
360 ' l.style.display = \'block\';' 358 ' l.style.display = \'block\';'
361 ' this.innerHTML = \'[-]\';' 359 ' this.innerHTML = \'[-]\';'
362 '} else {' 360 '} else {'
363 ' l.style.display = \'none\';' 361 ' l.style.display = \'none\';'
364 ' this.innerHTML = \'[+]\';' 362 ' this.innerHTML = \'[+]\';'
365 '}">' 363 '}">'
366 '[+]' 364 '[+]'
367 '</span>' % 365 '</span>' %
368 path) 366 path)
369 self._writeLine('<div style="display: none"' 367 self._writeLine('<div style="display: none"'
370 'id="piecrust-debug-data-%s">' % path) 368 'id="piecrust-debug-data-%s">' % path)
371 369
372 def _renderCollapsableValueEnd(self): 370 def _renderCollapsableValueEnd(self):
373 self._writeLine('</div>') 371 self._writeLine('</div>')
374 372
375 def _makePath(self, parent_path, key): 373 def _makePath(self, parent_path, key):