diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/data/debug.py	Sun Aug 10 23:43:16 2014 -0700
@@ -0,0 +1,370 @@
+import re
+import cgi
+import logging
+import StringIO
+from piecrust import APP_VERSION, PIECRUST_URL
+
+
+logger = logging.getLogger(__name__)
+
+
+css_id_re = re.compile(r'[^\w\d\-]+')
+
+
+# CSS for the debug window.
+CSS_DEBUGINFO = """
+text-align: left;
+font-style: normal;
+padding: 1em;
+background: #a42;
+color: #fff;
+position: fixed;
+width: 50%;
+bottom: 0;
+right: 0;
+overflow: auto;
+max-height: 50%;
+box-shadow: 0 0 10px #633;
+"""
+
+# 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 <em><a href="%s">PieCrust</a> %s</em>.' % (
+        PIECRUST_URL, APP_VERSION)
+
+
+def build_debug_info(page, data):
+    """ Generates HTML debug info for the given page's data.
+    """
+    output = StringIO.StringIO()
+    try:
+        _do_build_debug_info(page, data, output)
+        return output.getvalue()
+    finally:
+        output.close()
+
+
+def _do_build_debug_info(page, data, output):
+    app = page.app
+    exec_info = app.env.exec_info_stack.current_page_info
+
+    print >>output, '<div id="piecrust-debug-info" style="%s">' % CSS_DEBUGINFO
+
+    print >>output, '<div>'
+    print >>output, '<p style="%s"><strong>PieCrust %s</strong> &mdash; ' % (CSS_P, APP_VERSION)
+
+    # If we have some execution info in the environment,
+    # add more information.
+    if exec_info:
+        if exec_info.was_cache_valid:
+            output.write('baked this morning')
+        else:
+            output.write('baked just now')
+
+        if app.cache.enabled:
+            if app.env.was_cache_cleaned:
+                output.write(', from a brand new cache')
+            else:
+                output.write(', from a valid cache')
+        else:
+            output.write(', with no cache')
+
+    else:
+        output.write('no caching information available')
+
+    output.write(', ')
+    if app.env.start_time != 0:
+        output.write('in __PIECRUST_TIMING_INFORMATION__')
+    else:
+        output.write('no timing information available')
+
+    print >>output, '</p>'
+    print >>output, '</div>'
+
+    if data:
+        print >>output, '<div>'
+        print >>output, ('<p style="%s cursor: pointer;" onclick="var l = '
+                         'document.getElementById(\'piecrust-debug-details\'); '
+                         'if (l.style.display == \'none\') l.style.display = '
+                         '\'block\'; else l.style.display = \'none\';">' % CSS_P)
+        print >>output, ('<span style="%s">Template engine data</span> '
+                         '&mdash; click to toggle</a>.</p>' % CSS_BIGHEADER)
+
+        print >>output, '<div id="piecrust-debug-details" style="display: none;">'
+        print >>output, ('<p style="%s">The following key/value pairs are '
+                         'available in the layout\'s markup, and most are '
+                         'available in the page\'s markup.</p>' % CSS_DOC)
+
+        filtered_data = dict(data)
+        for k in filtered_data.keys():
+            if k.startswith('__'):
+                del filtered_data[k]
+
+        renderer = DebugDataRenderer(output)
+        renderer.external_docs['data-site'] = (
+                "This section comes from the site configuration file.")
+        renderer.external_docs['data-page'] = (
+                "This section comes from the page's configuration header.")
+        renderer.renderData(filtered_data)
+
+        print >>output, '</div>'
+        print >>output, '</div>'
+
+    print >>output, '</div>'
+
+
+class DebugDataRenderer(object):
+    MAX_VALUE_LENGTH = 150
+
+    def __init__(self, output):
+        self.indent = 0
+        self.output = output
+        self.external_docs = {}
+
+    def renderData(self, data):
+        if not isinstance(data, dict):
+            raise Exception("Expected top level data to be a dict.")
+        self._writeLine('<div style="%s">' % CSS_DATA)
+        self._renderDict(data, 'data')
+        self._writeLine('</div>')
+
+    def _renderValue(self, data, path):
+        if data is None:
+            self._write('&lt;null&gt;')
+            return
+
+        if isinstance(data, dict):
+            self._renderCollapsableValueStart(path)
+            with IndentScope(self):
+                self._renderDict(data, path)
+            self._renderCollapsableValueEnd()
+            return
+
+        if isinstance(data, list):
+            self._renderCollapsableValueStart(path)
+            with IndentScope(self):
+                self._renderList(data, path)
+            self._renderCollapsableValueEnd()
+            return
+
+        data_type = type(data)
+        if data_type is bool:
+            self._write('<span style="%s">%s</span>' % (CSS_VALUE,
+                'true' if bool(data) else 'false'))
+            return
+
+        if data_type is int:
+            self._write('<span style="%s">%d</span>' % (CSS_VALUE, data))
+            return
+
+        if data_type is float:
+            self._write('<span style="%s">%4.2f</span>' % (CSS_VALUE, data))
+            return
+
+        if data_type in (str, unicode):
+            if data_type == str:
+                data = data.decode('utf8')
+            if len(data) > DebugDataRenderer.MAX_VALUE_LENGTH:
+                data = data[:DebugDataRenderer.MAX_VALUE_LENGTH - 5]
+                data += '[...]'
+            data = cgi.escape(data).encode('ascii', 'xmlcharrefreplace')
+            self._write('<span style="%s">%s</span>' % (CSS_VALUE, data))
+            return
+
+        self._renderCollapsableValueStart(path)
+        with IndentScope(self):
+            self._renderObject(data, path)
+        self._renderCollapsableValueEnd()
+
+    def _renderList(self, data, path):
+        self._writeLine('<div style="%s">' % CSS_DATABLOCK)
+        self._renderDoc(data, path)
+        self._renderAttributes(data, path)
+        rendered_count = self._renderIterable(data, path, lambda d: enumerate(d))
+        if rendered_count == 0:
+            self._writeLine('<p style="%s %s">(empty array)</p>' % (CSS_P, CSS_DOC))
+        self._writeLine('</div>')
+
+    def _renderDict(self, data, path):
+        self._writeLine('<div style="%s">' % CSS_DATABLOCK)
+        self._renderDoc(data, path)
+        self._renderAttributes(data, path)
+        rendered_count = self._renderIterable(data, path,
+                lambda d: sorted(d.iteritems(), key=lambda i: i[0]))
+        if rendered_count == 0:
+            self._writeLine('<p style="%s %s">(empty dictionary)</p>' % (CSS_P, CSS_DOC))
+        self._writeLine('</div>')
+
+    def _renderObject(self, data, path):
+        if hasattr(data.__class__, 'debug_render_func'):
+            # This object wants to be rendered as a simple string...
+            render_func_name = data.__class__.debug_render_func
+            render_func = getattr(data, render_func_name)
+            value = render_func()
+            self._renderValue(value, path)
+            return
+
+        self._writeLine('<div style="%s">' % 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))
+            if rendered_count == 0:
+                self._writeLine('<p style="%s %s">(empty)</p>' % (CSS_P, CSS_DOC))
+
+        elif rendered_attrs == 0:
+            self._writeLine('<p style="%s %s">(empty)</p>' % (CSS_P, CSS_DOC))
+
+        self._writeLine('</div>')
+
+    def _renderIterable(self, data, path, iter_func):
+        rendered_count = 0
+        with IndentScope(self):
+            for i, item in iter_func(data):
+                self._writeStart('<div>%s' % i)
+                if item is not None:
+                    self._write(' : ')
+                    self._renderValue(item, self._makePath(path, i))
+                self._writeEnd('</div>')
+                rendered_count += 1
+        return rendered_count
+
+    def _renderDoc(self, data, path):
+        if hasattr(data.__class__, 'debug_render_doc'):
+            self._writeLine('<span style="%s">&ndash; %s</span>' %
+                    (CSS_DOC, data.__class__.debug_render_doc))
+
+        doc = self.external_docs.get(path)
+        if doc is not None:
+            self._writeLine('<span style="%s">&ndash; %s</span>' %
+                    (CSS_DOC, doc))
+
+    def _renderAttributes(self, data, path):
+        if not hasattr(data.__class__, 'debug_render'):
+            return 0
+
+        attr_names = list(data.__class__.debug_render)
+        if hasattr(data.__class__, 'debug_render_dynamic'):
+            drd = data.__class__.debug_render_dynamic
+            for ng in drd:
+                name_gen = getattr(data, ng)
+                attr_names += name_gen()
+
+        invoke_attrs = []
+        if hasattr(data.__class__, 'debug_render_invoke'):
+            invoke_attrs = list(data.__class__.debug_render_invoke)
+        if hasattr(data.__class__, 'debug_render_invoke_dynamic'):
+            drid = data.__class__.debug_render_invoke_dynamic
+            for ng in drid:
+                name_gen = getattr(data, ng)
+                invoke_attrs += name_gen()
+
+        rendered_count = 0
+        for name in attr_names:
+            value = None
+            render_name = name
+            should_call = name in invoke_attrs
+
+            try:
+                attr = getattr(data.__class__, name)
+            except AttributeError:
+                # This could be an attribute on the instance itself, or some
+                # dynamic attribute.
+                attr = getattr(data, name)
+
+            if callable(attr):
+                attr_func = getattr(data, name)
+                argcount = attr_func.__code__.co_argcount
+                var_names = attr_func.__code__.co_varnames
+                if argcount == 1 and should_call:
+                    render_name += '()'
+                    value = attr_func()
+                else:
+                    if should_call:
+                        logger.warning("Method '%s' should be invoked for "
+                                       "rendering, but it has %s arguments." %
+                                       (name, argcount))
+                        should_call = False
+                    render_name += '(%s)' % ','.join(var_names[1:])
+            elif should_call:
+                value = getattr(data, name)
+
+            self._writeLine('<div>%s' % render_name)
+            with IndentScope(self):
+                if should_call:
+                    self._write(' : ')
+                    self._renderValue(value, self._makePath(path, name))
+            self._writeLine('</div>')
+            rendered_count += 1
+
+        return rendered_count
+
+    def _renderCollapsableValueStart(self, path):
+        self._writeLine('<span style="cursor: pointer;" onclick="var l = '
+                    'document.getElementById(\'piecrust-debug-data-%s\'); '
+                    'if (l.style.display == \'none\') {'
+                    '  l.style.display = \'block\';'
+                    '  this.innerHTML = \'[-]\';'
+                    '} else {'
+                    '  l.style.display = \'none\';'
+                    '  this.innerHTML = \'[+]\';'
+                    '}">'
+                    '[+]'
+                    '</span>' %
+                    path)
+        self._writeLine('<div style="display: none"'
+                         'id="piecrust-debug-data-%s">' % path)
+
+    def _renderCollapsableValueEnd(self):
+        self._writeLine('</div>')
+
+    def _makePath(self, parent_path, key):
+        return '%s-%s' % (parent_path, css_id_re.sub('-', str(key)))
+
+    def _writeLine(self, msg):
+        self.output.write(self.indent * '  ')
+        self.output.write(msg)
+        self.output.write('\n')
+
+    def _writeStart(self, msg=None):
+        self.output.write(self.indent * '  ')
+        if msg is not None:
+            self.output.write(msg)
+
+    def _write(self, msg):
+        self.output.write(msg)
+
+    def _writeEnd(self, msg=None):
+        if msg is not None:
+            self.output.write(msg)
+        self.output.write('\n')
+
+
+class IndentScope(object):
+    def __init__(self, target):
+        self.target = target
+
+    def __enter__(self):
+        self.target.indent += 1
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.target.indent -= 1
+