# HG changeset patch # User Ludovic Chabant # Date 1439446715 25200 # Node ID 93b656f0af543b10e47c0b0a8a95305ebe48ddb9 # Parent daf8df5ade7d7bc06b925f34a41a715ac3038ece serve: Improve debug information in the preview server. Now the debug window only loads debug info on demand. diff -r daf8df5ade7d -r 93b656f0af54 piecrust/data/builder.py --- a/piecrust/data/builder.py Wed Aug 12 23:04:46 2015 -0700 +++ b/piecrust/data/builder.py Wed Aug 12 23:18:35 2015 -0700 @@ -60,7 +60,7 @@ # displayed in the debugger window. if (app.config.get('site/show_debug_info') and not app.config.get('baker/is_baking')): - pc_data._enableDebugInfo(page, data) + pc_data.enableDebugInfo(page) return data diff -r daf8df5ade7d -r 93b656f0af54 piecrust/data/debug.py --- a/piecrust/data/debug.py Wed Aug 12 23:04:46 2015 -0700 +++ b/piecrust/data/debug.py Wed Aug 12 23:18:35 2015 -0700 @@ -14,75 +14,38 @@ css_id_re = re.compile(r'[^\w\d\-]+') -# CSS for the debug window. -CSS_DEBUGWINDOW = """ -text-align: left; -font-family: serif; -font-style: normal; -font-weight: normal; -position: fixed; -width: 50%; -bottom: 0; -right: 0; -overflow: auto; -max-height: 50%; -box-shadow: 0 0 10px #633; -""" - -CSS_PIPELINESTATUS = """ -background: #fff; -color: #a22; -""" +CSS_DATA = 'piecrust-debug-info-data' +CSS_DATABLOCK = 'piecrust-debug-info-datablock' +CSS_VALUE = 'piecrust-debug-info-datavalue' +CSS_DOC = 'piecrust-debug-info-doc' +CSS_BIGHEADER = 'piecrust-debug-info-header1' +CSS_HEADER = 'piecrust-debug-info-header2' -CSS_DEBUGINFO = """ -padding: 1em; -background: #a42; -color: #fff; -""" - -# HTML elements. -CSS_P = 'margin: 0; padding: 0;' -CSS_A = 'color: #fff; text-decoration: none;' - -# Headers. -CSS_BIGHEADER = 'margin: 0.5em 0; font-weight: bold;' -CSS_HEADER = 'margin: 0.5em 0; font-weight: bold;' - -# Data block elements. -CSS_DATA = 'font-family: Courier, sans-serif; font-size: 0.9em;' -CSS_DATABLOCK = 'margin-left: 2em;' -CSS_VALUE = 'color: #fca;' -CSS_DOC = 'color: #fa8; font-size: 0.9em;' # 'Baked with PieCrust' text BRANDING_TEXT = 'Baked with PieCrust %s.' % ( PIECRUST_URL, APP_VERSION) -def build_debug_info(page, data): +def build_debug_info(page): """ Generates HTML debug info for the given page's data. """ output = io.StringIO() try: - _do_build_debug_info(page, data, output) + _do_build_debug_info(page, output) return output.getvalue() finally: output.close() -def _do_build_debug_info(page, data, output): +def _do_build_debug_info(page, output): app = page.app - print('
' % CSS_DEBUGWINDOW, + print('
', file=output) - print('
' % - CSS_PIPELINESTATUS, file=output) - print('
', file=output) - - print('
' % CSS_DEBUGINFO, file=output) - print('

PieCrust %s — ' % - (CSS_P, APP_VERSION), file=output) + print('

PieCrust %s — ' % APP_VERSION, + file=output) # If we have some execution info in the environment, # add more information. @@ -101,46 +64,78 @@ output.write(', ') if app.env.start_time != 0: - output.write('in __PIECRUST_TIMING_INFORMATION__') + output.write('in __PIECRUST_TIMING_INFORMATION__. ') else: - output.write('no timing information available') + output.write('no timing information available. ') - print('

', file=output) + print('[+]', + file=output) + print('
', file=output) - if data: - print('
' % CSS_DEBUGINFO, file=output) - print(('

' % CSS_P), file=output) - print(('Template engine data ' - '— click to toggle.

' % CSS_BIGHEADER), file=output) - - print('', file=output) - print('
', file=output) + print('
', + file=output) + print('
', file=output) print('
', file=output) print('', - file=output) + file=output) + + +def build_var_debug_info(data, var_path=None): + output = io.StringIO() + try: + _do_build_var_debug_info(data, output, var_path) + return output.getvalue() + finally: + output.close() + + +def _do_build_var_debug_info(data, output, var_path=None): + if False: + print('', file=output) + print('', file=output) + print('', file=output) + print('', + file=output) + print('', file=output) + print('', file=output) + + #print('
', file=output) + #print(('

'), + # file=output) + #print(('Template engine data ' + # '— click to toggle.

' % CSS_BIGHEADER), file=output) + + #print('', file=output) + #print('
', file=output) + + if False: + print('', file=output) + print('', file=output) class DebugDataRenderer(object): @@ -154,7 +149,7 @@ def renderData(self, data): if not isinstance(data, dict): raise Exception("Expected top level data to be a dict.") - self._writeLine('
' % CSS_DATA) + self._writeLine('
' % CSS_DATA) self._renderDict(data, 'data') self._writeLine('
') @@ -179,16 +174,16 @@ data_type = type(data) if data_type is bool: - self._write('%s' % (CSS_VALUE, - 'true' if bool(data) else 'false')) + self._write('%s' % (CSS_VALUE, + 'true' if bool(data) else 'false')) return if data_type is int: - self._write('%d' % (CSS_VALUE, data)) + self._write('%d' % (CSS_VALUE, data)) return if data_type is float: - self._write('%4.2f' % (CSS_VALUE, data)) + self._write('%4.2f' % (CSS_VALUE, data)) return if data_type is str: @@ -196,7 +191,7 @@ data = data[:DebugDataRenderer.MAX_VALUE_LENGTH - 5] data += '[...]' data = html.escape(data) - self._write('%s' % (CSS_VALUE, data)) + self._write('%s' % (CSS_VALUE, data)) return self._renderCollapsableValueStart(path) @@ -205,24 +200,27 @@ self._renderCollapsableValueEnd() def _renderList(self, data, path): - self._writeLine('
' % CSS_DATABLOCK) + self._writeLine('
' % CSS_DATABLOCK) self._renderDoc(data, path) self._renderAttributes(data, path) - rendered_count = self._renderIterable(data, path, lambda d: enumerate(d)) + rendered_count = self._renderIterable( + data, path, lambda d: enumerate(d)) if (rendered_count == 0 and not hasattr(data.__class__, 'debug_render_not_empty')): - self._writeLine('

(empty array)

' % (CSS_P, CSS_DOC)) + self._writeLine('

(empty array)

' % (CSS_DOC)) self._writeLine('
') def _renderDict(self, data, path): - self._writeLine('
' % CSS_DATABLOCK) + self._writeLine('
' % CSS_DATABLOCK) self._renderDoc(data, path) self._renderAttributes(data, path) - rendered_count = self._renderIterable(data, path, + rendered_count = self._renderIterable( + data, path, lambda d: sorted(iter(d.items()), key=lambda i: i[0])) if (rendered_count == 0 and not hasattr(data.__class__, 'debug_render_not_empty')): - self._writeLine('

(empty dictionary)

' % (CSS_P, CSS_DOC)) + self._writeLine('

(empty dictionary)

' % + CSS_DOC) self._writeLine('
') def _renderObject(self, data, path): @@ -234,22 +232,22 @@ self._renderValue(value, path) return - self._writeLine('
' % CSS_DATABLOCK) + self._writeLine('
' % CSS_DATABLOCK) self._renderDoc(data, path) rendered_attrs = self._renderAttributes(data, path) if (hasattr(data, '__iter__') and hasattr(data.__class__, 'debug_render_items') and data.__class__.debug_render_items): - rendered_count = self._renderIterable(data, path, - lambda d: enumerate(d)) + rendered_count = self._renderIterable( + data, path, lambda d: enumerate(d)) if (rendered_count == 0 and not hasattr(data.__class__, 'debug_render_not_empty')): - self._writeLine('

(empty)

' % (CSS_P, CSS_DOC)) + self._writeLine('

(empty)

' % CSS_DOC) elif (rendered_attrs == 0 and not hasattr(data.__class__, 'debug_render_not_empty')): - self._writeLine('

(empty)

' % (CSS_P, CSS_DOC)) + self._writeLine('

(empty)

' % CSS_DOC) self._writeLine('
') @@ -267,20 +265,20 @@ def _renderDoc(self, data, path): if hasattr(data.__class__, 'debug_render_doc'): - self._writeLine('– %s' % - (CSS_DOC, data.__class__.debug_render_doc)) + self._writeLine('– %s' % + (CSS_DOC, data.__class__.debug_render_doc)) if hasattr(data.__class__, 'debug_render_doc_dynamic'): drdd = data.__class__.debug_render_doc_dynamic for ng in drdd: doc = getattr(data, ng) - self._writeLine('– %s' % - (CSS_DOC, doc())) + self._writeLine('– %s' % + (CSS_DOC, doc())) doc = self.external_docs.get(path) if doc is not None: - self._writeLine('– %s' % - (CSS_DOC, doc)) + self._writeLine('– %s' % + (CSS_DOC, doc)) def _renderAttributes(self, data, path): if not hasattr(data.__class__, 'debug_render'): @@ -355,19 +353,19 @@ def _renderCollapsableValueStart(self, path): self._writeLine('' - '[+]' - '' % - path) + 'document.getElementById(\'piecrust-debug-data-%s\'); ' + 'if (l.style.display == \'none\') {' + ' l.style.display = \'block\';' + ' this.innerHTML = \'[-]\';' + '} else {' + ' l.style.display = \'none\';' + ' this.innerHTML = \'[+]\';' + '}">' + '[+]' + '' % + path) self._writeLine('
' % path) + 'id="piecrust-debug-data-%s">' % path) def _renderCollapsableValueEnd(self): self._writeLine('
') diff -r daf8df5ade7d -r 93b656f0af54 piecrust/data/piecrustdata.py --- a/piecrust/data/piecrustdata.py Wed Aug 12 23:04:46 2015 -0700 +++ b/piecrust/data/piecrustdata.py Wed Aug 12 23:18:35 2015 -0700 @@ -17,22 +17,20 @@ self.branding = 'Baked with PieCrust %s.' % ( 'http://bolt80.com/piecrust/', APP_VERSION) self._page = None - self._data = None @property def debug_info(self): - if self._page is not None and self._data is not None: + if self._page is not None: try: - return build_debug_info(self._page, self._data) + return build_debug_info(self._page) except Exception as ex: logger.exception(ex) return ('An error occured while generating debug info. ' 'Please check the logs.') return '' - def _enableDebugInfo(self, page, data): + def enableDebugInfo(self, page): self._page = page - self._data = data def _debugRenderDebugInfo(self): return "The very thing you're looking at!" diff -r daf8df5ade7d -r 93b656f0af54 piecrust/resources/server/piecrust-debug-info.css --- a/piecrust/resources/server/piecrust-debug-info.css Wed Aug 12 23:04:46 2015 -0700 +++ b/piecrust/resources/server/piecrust-debug-info.css Wed Aug 12 23:18:35 2015 -0700 @@ -1,3 +1,6 @@ + +/*****************************************************************************/ + @keyframes slideNotification { 0% { opacity: 1; } 100% { opacity: 0; right: -11em; } @@ -30,3 +33,65 @@ border: 2px solid #810B0B; } +/*****************************************************************************/ + +.piecrust-debug-window { + padding: 1em; + text-align: left; + font-family: serif; + font-style: normal; + font-size: 1rem; + font-weight: normal; + background: #a42; + color: #fff; + position: fixed; + width: 50%; + bottom: 0; + right: 0; + overflow: auto; + max-height: 50%; + box-shadow: 0 0 10px #633; +} + +.piecrust-debug-info { +} + +.piecrust-debug-info a { + color: #fff; + text-decoration: none; +} + +.piecrust-debug-icon { + height: 2em; +} + +.piecrust-debug-info-header1 { + margin: 0.5em 0; + font-weight: bold; +} + +.piecrust-debug-info-header2 { + margin: 0.5em 0; + font-weight: bold; +} + +.piecrust-debug-info-data { + font-family: Courier, sans-serif; + font-size: 0.9em; +} + +.piecrust-debug-info-datablock { + margin-left: 2em; +} + +.piecrust-debug-info-datavalue { + color: #fca; +} + +.piecrust-debug-info-doc { + color: #fa8; + font-size: 0.9em; +} + +/*****************************************************************************/ + diff -r daf8df5ade7d -r 93b656f0af54 piecrust/resources/server/piecrust-debug-info.js --- a/piecrust/resources/server/piecrust-debug-info.js Wed Aug 12 23:04:46 2015 -0700 +++ b/piecrust/resources/server/piecrust-debug-info.js Wed Aug 12 23:18:35 2015 -0700 @@ -263,6 +263,46 @@ /////////////////////////////////////////////////////////////////////////////// +function toggleDebugInfo() { + var info = document.querySelector('.piecrust-debug-info'); + if (info.classList.contains('piecrust-debug-info-unloaded')) { + loadDebugInfo(); + info.classList.remove('piecrust-debug-info-unloaded'); + } + if (this.innerHTML == '[+]') { + this.innerHTML = '[-]'; + info.style = ""; + } else { + this.innerHTML = '[+]'; + info.style = "display: none;"; + } +} + +function loadDebugInfo() { + var xmlHttp = new XMLHttpRequest(); + + xmlHttp.onreadystatechange = function() { + if (xmlHttp.readyState == XMLHttpRequest.DONE) { + var info = document.querySelector('.piecrust-debug-info'); + if(xmlHttp.status == 200) { + info.innerHTML = xmlHttp.responseText; + } + else if(xmlHttp.status == 400) { + info.innerHTML = "Error fetching debug info."; + } + else { + info.innerHTML = "Unknown error."; + } + } + } + + var pageUrl = window.location.pathname; + xmlHttp.open("GET", "/__piecrust_debug/debug_info?page=" + pageUrl, true); + xmlHttp.send(); +} + +/////////////////////////////////////////////////////////////////////////////// + var notification = new NotificationArea(); var assetReloader = new AssetReloader(); @@ -274,6 +314,9 @@ style.type = 'text/css'; style.href = '/__piecrust_static/piecrust-debug-info.css' + cacheBust; document.head.appendChild(style); + + var expander = document.querySelector('.piecrust-debug-expander'); + expander.onclick = toggleDebugInfo; }; diff -r daf8df5ade7d -r 93b656f0af54 piecrust/serving/middlewares.py --- a/piecrust/serving/middlewares.py Wed Aug 12 23:04:46 2015 -0700 +++ b/piecrust/serving/middlewares.py Wed Aug 12 23:18:35 2015 -0700 @@ -1,8 +1,15 @@ import os.path +from werkzeug.exceptions import NotFound, Forbidden from werkzeug.wrappers import Request, Response from werkzeug.wsgi import ClosingIterator from piecrust import RESOURCES_DIR, CACHE_DIR -from piecrust.serving.util import make_wrapped_file_response +from piecrust.data.builder import ( + DataBuildingContext, build_page_data) +from piecrust.data.debug import build_var_debug_info +from piecrust.routing import RouteNotFoundError +from piecrust.serving.util import ( + make_wrapped_file_response, get_requested_page, get_app_for_server) +from piecrust.sources.pageref import PageNotFoundError class StaticResourcesMiddleware(object): @@ -38,12 +45,14 @@ self.app = app self.root_dir = root_dir self.debug = debug + self.sub_cache_dir = sub_cache_dir self.run_sse_check = run_sse_check self._proc_loop = None self._out_dir = os.path.join(root_dir, CACHE_DIR, 'server') if sub_cache_dir: self._out_dir = os.path.join(sub_cache_dir, 'server') self._handlers = { + 'debug_info': self._getDebugInfo, 'werkzeug_shutdown': self._shutdownWerkzeug, 'pipeline_status': self._startSSEProvider} @@ -70,6 +79,34 @@ return self.app(environ, start_response) + def _getDebugInfo(self, request, start_response): + app = get_app_for_server(self.root_dir, debug=self.debug, + sub_cache_dir=self.sub_cache_dir) + if not app.config.get('site/enable_debug_info'): + return Forbidden() + + found = False + page_path = request.args.get('page') + try: + req_page = get_requested_page(app, page_path) + found = (req_page is not None) + except (RouteNotFoundError, PageNotFoundError): + pass + if not found: + return NotFound("No such page: %s" % page_path) + + ctx = DataBuildingContext(req_page.qualified_page, + page_num=req_page.page_num) + data = build_page_data(ctx) + + var_path = request.args.getlist('var') + if not var_path: + var_path = None + output = build_var_debug_info(data, var_path) + + response = Response(output, mimetype='text/html') + return response(request.environ, start_response) + def _shutdownWerkzeug(self, request, start_response): shutdown_func = request.environ.get('werkzeug.server.shutdown') if shutdown_func is None: