Mercurial > piecrust2
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> — ' % 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> — ' % | |
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 '— 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 # '— 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">– %s</span>' % | 268 self._writeLine('<span class="%s">– %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">– %s</span>' % | 275 self._writeLine('<span class="%s">– %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">– %s</span>' % | 280 self._writeLine('<span class="%s">– %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): |