Commits

Rick Copeland committed 19ea0b1

Add debug info (and tests) to xml templates

Comments (0)

Files changed (7)

kajiki/__init__.py

 from .util import expose, flattener
 from .template import Template
-from .loader import MockLoader
+from .loader import MockLoader, FileLoader
 from .text import TextTemplate
 from .xml_template import XMLTemplate
         return PyLine(self._filename, self._lineno, self._text, self._indent + sz)
 
     def __str__(self):
-        if self._lineno != 0:
+        if self._lineno:
             return (' ' * self._indent) + self._text + '\t# %s:%d' % (self._filename, self._lineno)
         else:
             return (' ' * self._indent) + self._text
     def import_(self, name):
         mod = self.modules.get(name)
         if mod: return mod
-        mod = self._load(mod)
+        mod = self._load(name)
         self.modules[name] = mod
         return mod
 
     def default_alias_for(self, name):
-        return name.replace('/', '_').replace('.', '_')
+        return os.path.splitext(os.path.basename(name))[0]
 
 class MockLoader(Loader):
 
         for v in self.modules.itervalues():
             v.loader = self
             
-    def default_alias_for(self, name):
-        return os.path.splitext(os.path.basename(name))[0]
+class FileLoader(Loader):
 
+    def __init__(self, base):
+        super(FileLoader, self).__init__()
+        from kajiki import XMLTemplate, TextTemplate
+        self.base = base
+        self.extension_map = dict(
+            txt=TextTemplate,
+            xml=XMLTemplate,
+            html=lambda *a,**kw:XMLTemplate(*a, mode='html', **kw),
+            html5=lambda *a,**kw:XMLTemplate(*a, mode='html5', **kw))
+
+    def _load(self, name):
+        filename = os.path.join(self.base, name)
+        ext = os.path.splitext(name)[1][1:]
+        source = open(filename, 'rb').read()
+        return self.extension_map[ext](source=source, filename=filename)
+        

kajiki/template.py

 import kajiki
 from .util import flattener
 from .html_utils import HTML_EMPTY_ATTRS
+from . import lnotab
 
 class _obj(object):
     def __init__(self, **kw):
     __methods__=()
     loader = None
     base_globals = None
+    filename = None
 
     def __init__(self, context=None):
         if context is None: context = {}
             yield unicode(chunk)
 
     def _render(self):
-        try:
-            return u''.join(self)
-        except:
-            for i, line in enumerate(self.py_text.splitlines()):
-                print '%3d %s' % (i+1, line)
-            raise
+        return u''.join(self)
 
     def _extend(self, parent):
         if isinstance(parent, basestring):
             if mode.startswith('html') and k in HTML_EMPTY_ATTRS: yield ' '+k.upper()
             else: yield ' %s="%s"' % (k,self._escape(v))
 
+    @classmethod
+    def annotate_lnotab(cls, py_to_tpl):
+        for name, meth in cls.__methods__:
+            meth.annotate_lnotab(cls.filename, py_to_tpl, dict(py_to_tpl))
+
 def Template(ns):
     dct = {}
     methods = dct['__methods__'] = []
 def from_ir(ir_node):
     py_lines = list(ir_node.py())
     py_text = '\n'.join(map(str, py_lines))
-    py_linenos = [
-        (i+1, l._lineno)
-        for i,l in enumerate(py_lines)
-        if l._lineno ]
-    print py_linenos
+    py_linenos = [ ]
+    last_lineno = 0
+    for i,l in enumerate(py_lines):
+        lno =max(last_lineno, l._lineno or 0)
+        py_linenos.append((i+1, lno))
+        last_lineno = lno
     dct = dict(kajiki=kajiki)
     try:
         exec py_text in dct
     tpl = dct['template']
     tpl.base_globals = dct
     tpl.py_text = py_text
+    tpl.filename = ir_node.filename
+    tpl.annotate_lnotab(py_linenos)
     return tpl
 
 class TplFunc(object):
         return update_wrapper(
             lambda *a,**kw:flattener(func(*a,**kw)),
             func)
+
+    def annotate_lnotab(self, filename, py_to_tpl, py_to_tpl_dct):
+        if not py_to_tpl: return
+        code = self._func.func_code
+        new_lnotab_numbers = []
+        for bc_off, py_lno in lnotab.lnotab_numbers(code.co_lnotab, code.co_firstlineno):
+            tpl_lno = py_to_tpl_dct.get(py_lno, None)
+            if tpl_lno is None:
+                print 'ERROR LOOKING UP LINE #%d' % py_lno
+                continue
+            new_lnotab_numbers.append((bc_off, tpl_lno))
+        if not new_lnotab_numbers: return
+        new_firstlineno = py_to_tpl_dct.get(code.co_firstlineno, 0)
+        new_lnotab = lnotab.lnotab_string(new_lnotab_numbers, new_firstlineno)
+        new_code = types.CodeType(
+            code.co_argcount,
+            code.co_nlocals,
+            code.co_stacksize,
+            code.co_flags,
+            code.co_code,
+            code.co_consts,
+            code.co_names,
+            code.co_varnames,
+            filename,
+            code.co_name,
+            new_firstlineno,
+            new_lnotab,
+            code.co_freevars,
+            code.co_cellvars)
+        self._func.func_code = new_code
+        return
+

kajiki/tests/data/simple.html

+<div>
+  <py:def function="one()">${two()}</py:def>
+  <py:def function="two()">${three()}</py:def>
+  <py:def function="three()">
+    <?py import pdb; pdb.set_trace() ?>
+  </py:def>
+  ${one()}
+</div>

kajiki/tests/test_xml.py

 import os
+import sys
+import traceback
 import xml.dom.minidom
 from unittest import TestCase, main
 
 import kajiki
-from kajiki import MockLoader, XMLTemplate
+from kajiki import MockLoader, XMLTemplate, FileLoader
 
 
 DATA = os.path.join(
         rsp = tpl(dict(checked=True)).__kj__.render()
         assert rsp == '<input type="checkbox" CHECKED>', rsp
 
+class TestDebug(TestCase):
+    
+    def test_debug(self):
+        loader = FileLoader(base=os.path.join(os.path.dirname(__file__), 'data'))
+        tpl = loader.import_('debug.html')
+        try:
+            tpl().__kj__.render()
+            assert False, 'Should have raised ValueError'
+        except ValueError:
+            exc_info = sys.exc_info()
+            stack = traceback.extract_tb(exc_info[2])
+        # Verify we have stack trace entries in the template
+        for fn, lno, func, line in stack:
+            if fn.endswith('debug.html'): break
+        else:
+            assert False, 'Stacktrace is all python'
+
 if __name__ == '__main__':
     main()

kajiki/xml_template.py

         filename = '<string>'
     doc = _Parser(filename, source).parse()
     expand(doc)
-    ir_ = _Compiler(filename, doc, mode, is_fragment).compile()
+    compiler = _Compiler(filename, doc, mode, is_fragment)
+    ir_ = compiler.compile()
     return template.from_ir(ir_)
 
 def annotate(gen):
         self.mode = mode
         self.functions = defaultdict(list)
         self.functions['__call__()'] = []
+        self.function_lnos = {}
         self.mod_py = []
         self.in_def = False
         self.is_child = False
 
     def compile(self):
         body = list(self._compile_node(self.doc.firstChild))
-        if not self.is_fragment:
-            # Never emit doctypes on fragments
+        if not self.is_fragment: # Never emit doctypes on fragments
             if self.mode == 'xml' and self.doc.doctype:
-                body = [ ir.TextNode(self.doc.doctype.toxml()) ] + body
+                dt = ir.TextNode(self.doc.doctype.toxml())
+                dt.filename = self.filename
+                dt.lineno = 1
+                body.insert(0, dt)
             elif self.mode == 'html5':
-                body = [ ir.TextNode('<!DOCTYPE html>')] +body
+                dt = ir.TextNode('<!DOCTYPE html>')
+                dt.filename = self.filename
+                dt.lineno = 1
+                body.insert(0, dt)
         self.functions['__call__()'] = body
-        defs = [ ir.DefNode(k, *v) for k,v in self.functions.iteritems() ]
-        return ir.TemplateNode(self.mod_py, defs)
+        defs = []
+        for k,v in self.functions.iteritems():
+            node = ir.DefNode(k, *v)
+            node.lineno = self.function_lnos.get(k)
+            defs.append(node)
+        node = ir.TemplateNode(self.mod_py, defs)
+        node.filename = self.filename
+        node.lineno = 0
+        return node
 
     def _anno(self, dom_node, ir_node):
-        if ir_node.lineno != 0: return
-        ir_node._filename = self.filename
-        ir_node._lineno = dom_node.lineno
+        if ir_node.lineno: return
+        ir_node.filename = self.filename
+        ir_node.lineno = dom_node.lineno
 
     def _compile_node(self, node):
         if isinstance(node, dom.Comment):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.