Commits

Rick Copeland committed f07bdeb

Rename to kajiki part 1

Comments (0)

Files changed (16)

docs/xml-templates.rst

+==================================
+FastPt XML Templates
+==================================
+
+Tags
+====
+
+- Syntax-related tags (these don't expand)
+  - py:content
+  - py:replace
+  - py:attrs
+  - py:strip
+  - py:nop
+- Control-flow
+  - py:with
+  - py:switch
+  - py:case
+  - py:else
+  - py:for
+  - py:if
+- Module-related tags
+   - py:extends
+   - py:import
+   - py:include
+- Function-related tags
+   - py:def
+   - py:block
+   - py:call
+   - py:super (?) -- ${parent_block()}

kajiki/__init__.py

+import loader
+from util import expose, flattener
+from template import Template
+
+from .util import gen_name
+
+class Node(object):
+
+    def __init__(self):
+        self.filename = '<string>'
+        self.lineno = 0
+
+    def py(self): # pragma no cover
+        return []
+
+    def line(self, text):
+        return PyLine(self.filename, self.lineno, text)
+
+class TemplateNode(Node):
+
+    def __init__(self, mod_py=None, defs=None):
+        super(TemplateNode, self).__init__()
+        if mod_py is None: mod_py = []
+        if defs is None: defs = []
+        self.mod_py = [ x for x in mod_py if x is not None ]
+        self.defs = [ x for x in defs if x is not None ]
+
+    def py(self):
+        for block in self.mod_py:
+            for  line in block.py():
+                yield line
+        yield self.line('@fpt.Template')
+        yield self.line('class template:')
+        for child in self.defs:
+            for line in child.py():
+                yield line.indent()
+
+class ImportNode(Node):
+
+    def __init__(self, tpl_name, alias=None):
+        super(ImportNode, self).__init__()
+        self.tpl_name = tpl_name
+        self.alias = alias
+
+    def py(self):
+        yield self.line(
+            'local.__fpt__.import_(%r, %r, globals())' % (
+                self.tpl_name, self.alias))
+
+class IncludeNode(Node):
+
+    def __init__(self, tpl_name):
+        super(IncludeNode, self).__init__()
+        self.tpl_name = tpl_name
+
+    def py(self):
+        yield self.line(
+            'yield local.__fpt__.import_(%r, None, {}).__call__()' % (
+                self.tpl_name))
+
+class ExtendNode(Node):
+
+    def __init__(self, tpl_name):
+        super(ExtendNode, self).__init__()
+        self.tpl_name = tpl_name
+
+    def py(self):
+        yield self.line(
+            'yield local.__fpt__.extend(%r).__call__()' % (
+                self.tpl_name))
+
+class DefNode(Node):
+    prefix = '@fpt.expose'
+
+    def __init__(self, decl, *body):
+        super(DefNode, self).__init__()
+        self.decl = decl
+        self.body = tuple(x for x in body if x is not None)
+
+    def py(self):
+        yield self.line(self.prefix)
+        yield self.line('def %s:' % (self.decl))
+        for child in self.body:
+            for line in child.py():
+                yield line.indent()
+
+class InnerDefNode(DefNode):
+    prefix='@__fpt__.flattener.decorate'
+
+class CallNode(Node):
+
+    def __init__(self, caller, callee, *body):
+        super(CallNode, self).__init__()
+        fname = gen_name()
+        self.decl = caller.replace('$caller', fname)
+        self.call = callee.replace('$caller', fname)
+        self.body = tuple(x for x in body if x is not None)
+
+    def py(self):
+        yield self.line('@__fpt__.flattener.decorate')
+        yield self.line('def %s:' % (self.decl))
+        for child in self.body:
+            for line in child.py():
+                yield line.indent()
+        yield self.line('yield ' + self.call)
+
+class ForNode(Node):
+
+    def __init__(self, decl, *body):
+        super(ForNode, self).__init__()
+        self.decl = decl
+        self.body = tuple(x for x in body if x is not None)
+
+    def py(self):
+        yield self.line('for %s:' % (self.decl))
+        for child in self.body:
+            for line in child.py():
+                yield line.indent()
+
+class SwitchNode(Node):
+
+    def __init__(self, decl, *body):
+        super(SwitchNode, self).__init__()
+        self.decl = decl
+        self.body = tuple(x for x in body if x is not None)
+
+    def py(self):
+        yield self.line('local.__fpt__.push_switch(%s)' % self.decl)
+        for child in self.body:
+            for line in child.py():
+                yield line
+        yield self.line('local.__fpt__.pop_switch()')
+
+class CaseNode(Node):
+
+    def __init__(self, decl, *body):
+        super(CaseNode, self).__init__()
+        self.decl = decl
+        self.body = tuple(x for x in body if x is not None)
+
+    def py(self):
+        yield self.line('if local.__fpt__.case(%s):' % self.decl)
+        for child in self.body:
+            for line in child.py():
+                yield line.indent()
+
+class IfNode(Node):
+
+    def __init__(self, decl, *body):
+        super(IfNode, self).__init__()
+        self.decl = decl
+        self.body = tuple(x for x in body if x is not None)
+
+    def py(self):
+        yield self.line('if %s:' % self.decl)
+        for child in self.body:
+            for line in child.py():
+                yield line.indent()
+
+class ElseNode(Node):
+
+    def __init__(self,  *body):
+        super(ElseNode, self).__init__()
+        self.body = tuple(x for x in body if x is not None)
+
+    def py(self):
+        yield self.line('else:')
+        for child in self.body:
+            for line in child.py():
+                yield line.indent()
+
+class TextNode(Node):
+
+    def __init__(self, text, guard=None):
+        super(TextNode, self).__init__()
+        self.text = text
+        self.guard = guard
+
+    def py(self):
+        s = 'yield %r' % self.text
+        if self.guard:
+            yield self.line('if %s: %s' % (self.guard, s))
+        else:
+            yield self.line(s)
+
+class ExprNode(Node):
+
+    def __init__(self, text):
+        super(ExprNode, self).__init__()
+        self.text = text
+
+    def py(self):
+        yield self.line('yield self.__fpt__.escape(%s)' % self.text)
+
+class AttrNode(Node):
+
+    def __init__(self, attr, value, guard=None):
+        super(AttrNode, self).__init__()
+        self.attr = attr
+        self.value = value
+        self.guard = guard
+
+    def py(self):
+        s = 'yield \' %s="%s"\'' % (self.attr, self.value)
+        if self.guard:
+            yield self.line('if %s: %s' % (self.guard, s))
+        else:
+            yield self.line(s)
+
+class AttrsNode(Node):
+
+    def __init__(self, attrs, guard=None):
+        super(AttrsNode, self).__init__()
+        self.attrs = attrs
+        self.guard = guard
+
+    def py(self):
+        k,v = gen_name(), gen_name()
+        def _body():
+            yield self.line('for %s,%s in self.__fpt__.iter_attrs(%s):' % (k, v, self.attrs))
+            yield self.line('    yield \' %%s="%%s"\' %% (%s, %s)' % (k,v))
+        if self.guard:
+            yield self.line('if %s:' % self.guard)
+            for l in _body():
+                yield l.indent()
+        else:
+            for l in _body(): yield l
+
+class PythonNode(Node):
+
+    def __init__(self, *body):
+        super(PythonNode, self).__init__()
+        self.module_level = False
+        blocks = []
+        for b in body:
+            assert isinstance(b, TextNode)
+            blocks.append(b.text)
+        text = ''.join(blocks)
+        if text[0] == '%':
+            self.module_level = True
+            text = text[1:]
+        self.lines = list(self._normalize(text))
+
+    def py(self):
+        for line in self.lines:
+            yield self.line(line)
+
+    def _normalize(self, text):
+        if text.startswith('#\n'):
+            text = text[2:]
+        prefix = None
+        for line in text.splitlines():
+            if prefix is None:
+                rest = line.lstrip()
+                prefix = line[:len(line)-len(rest)]
+            assert line.startswith(prefix)
+            yield line[len(prefix):]
+
+class PyLine(object):
+
+    def __init__(self, filename, lineno, text, indent=0):
+        self._filename = filename
+        self._lineno = lineno
+        self._text = text
+        self._indent = indent
+
+    def indent(self, sz=4):
+        return PyLine(self._filename, self._lineno, self._text, self._indent + sz)
+
+    def __str__(self):
+        return (' ' * self._indent) + self._text
+
+import os
+class Loader(object):
+
+    def __init__(self):
+        self.modules = {}
+
+    def import_(self, name):
+        mod = self.modules.get(name)
+        if mod: return mod
+        mod = self._load(mod)
+        self.modules[name] = mod
+        return mod
+
+    def default_alias_for(self, name):
+        return name.replace('/', '_').replace('.', '_')
+
+class MockLoader(Loader):
+
+    def __init__(self, modules):
+        super(MockLoader, self).__init__()
+        self.modules.update(modules)
+        for v in self.modules.itervalues():
+            v.loader = self
+            
+    def default_alias_for(self, name):
+        return os.path.splitext(os.path.basename(name))[0]
+

kajiki/markup_template.py

+DIRECTIVES=[
+    ('def','function'),
+    ('call','function'),
+    ('case', 'value'),
+    ('else',''),
+    ('for','each'),
+    ('if','test'),
+    ('switch', 'test'),
+    ('with', 'vars'),
+    ('replace', 'value'),
+    ('block', 'name'),
+    ('extends', 'href'),]
+QDIRECTIVES = [
+    ('py:%s' % (k,), v)
+    for k,v in DIRECTIVES ]
+QDIRECTIVES_DICT = dict(QDIRECTIVES)
+

kajiki/template.py

+import types
+from cStringIO import StringIO
+from cgi import escape
+from functools import update_wrapper
+from pprint import pprint
+
+from webhelpers.html import literal
+
+import fastpt
+from .util import flattener
+
+class _obj(object):
+    def __init__(self, **kw):
+        for k,v in kw.iteritems():
+            setattr(self,k,v)
+
+class _Template(object):
+    __methods__=()
+    loader = None
+    base_globals = None
+
+    def __init__(self, context=None):
+        if context is None: context = {}
+        self._context = context
+        base_globals = self.base_globals or {}
+        self.__globals__ = dict(
+            base_globals,
+            local=self,
+            self=self,
+            literal=literal,
+            __builtins__=__builtins__,
+            __fpt__=fastpt.v2)
+        for k,v in self.__methods__:
+            v = v.bind_instance(self)
+            setattr(self, k, v)
+            self.__globals__[k] = v
+        self.__fpt__ = _obj(
+            render=self._render,
+            extend=self._extend,
+            push_switch=self._push_switch,
+            pop_switch=self._pop_switch,
+            case=self._case,
+            import_=self._import,
+            escape=self._escape,
+            iter_attrs=self._iter_attrs)
+        self._switch_stack = []
+        self.__globals__.update(context)
+
+    def __iter__(self):
+        for chunk in self.__call__():
+            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
+
+    def _extend(self, parent):
+        if isinstance(parent, basestring):
+            parent = self.loader.import_(parent)
+        p_inst = parent(self._context)
+        p_globals = p_inst.__globals__
+        # Find overrides
+        for k,v in self.__globals__.iteritems():
+            if k == '__call__': continue
+            if not isinstance(v, TplFunc): continue
+            p_globals[k] = v
+        # Find inherited funcs
+        for k, v in p_inst.__globals__.iteritems():
+            if k == '__call__': continue
+            if not isinstance(v, TplFunc): continue
+            if k not in self.__globals__: 
+                self.__globals__[k] = v
+            if not hasattr(self, k):
+                def _(k=k):
+                    '''Capture the 'k' variable in a closure'''
+                    def trampoline(*a, **kw):
+                        global parent
+                        return getattr(parent, k)(*a, **kw)
+                    return trampoline
+                setattr(self, k, TplFunc(_()).bind_instance(self))
+        p_globals['child'] = self
+        p_globals['local'] = p_inst
+        p_globals['self'] = self.__globals__['self']
+        self.__globals__['parent'] = p_inst
+        self.__globals__['local'] = self
+        return p_inst
+
+    def _push_switch(self, expr):
+        self._switch_stack.append(expr)
+
+    def _pop_switch(self):
+        self._switch_stack.pop()
+
+    def _case(self, obj):
+        return obj == self._switch_stack[-1]
+
+    def _import(self, name, alias, gbls):
+        tpl_cls = self.loader.import_(name)
+        if alias is None:
+            alias = self.loader.default_alias_for(name)
+        r = gbls[alias] = tpl_cls(gbls)
+        return r
+
+    def _escape(self, value):
+        if isinstance(value, flattener):
+            return u''.join(value) # assume flattener results are already escaped
+        if hasattr(value, '__html__'):
+            return value.__html__()
+        else:
+            return escape(unicode(value))
+
+    def _iter_attrs(self, attrs):
+        if hasattr(attrs, 'items'):
+            attrs = attrs.items()
+        for k,v in attrs:
+            yield k, self._escape(v)
+
+def Template(ns):
+    dct = {}
+    methods = dct['__methods__'] = []
+    for name in dir(ns):
+        value = getattr(ns, name)
+        if getattr(value, 'exposed', False):
+            methods.append((name, TplFunc(value.im_func)))
+    return type(ns.__name__,(_Template,), dct)
+
+def from_ir(ir_node):
+    from fastpt import v2 as fpt
+    py_text = '\n'.join(map(str, ir_node.py()))
+    dct = dict(fpt=fpt)
+    try:
+        exec py_text in dct
+    except SyntaxError: # pragma no cover
+        for i, line in enumerate(py_text.splitlines()):
+            print '%3d %s' % (i+1, line)
+        raise
+    tpl = dct['template']
+    tpl.base_globals = dct
+    tpl.py_text = py_text
+    return tpl
+
+class TplFunc(object):
+
+    def __init__(self, func, inst=None):
+        self._func = func
+        self._inst = inst
+        self._bound_func = None
+
+    def bind_instance(self, inst):
+        return TplFunc(self._func, inst)
+
+    def __repr__(self): # pragma no cover
+        if self._inst:
+            return '<bound tpl_function %r of %r>' % (
+                self._func.func_name, self._inst)
+        else:
+            return '<unbound tpl_function %r>' % (self._func.func_name)
+
+    def __call__(self, *args, **kwargs):
+        if self._bound_func is None:
+            self._bound_func = self._bind_globals(
+                self._inst.__globals__)
+        return self._bound_func(*args, **kwargs)
+
+    def _bind_globals(self, globals):
+        '''Return a function which has the globals dict set to 'globals' and which
+        flattens the result of self._func'.
+        '''
+        func = types.FunctionType(
+            self._func.func_code,
+            globals,
+            self._func.func_name,
+            self._func.func_defaults,
+            self._func.func_closure
+            )
+        return update_wrapper(
+            lambda *a,**kw:flattener(func(*a,**kw)),
+            func)
Add a comment to this file

kajiki/tests/__init__.py

Empty file added.

Add a comment to this file

kajiki/tests/data/__init__.py

Empty file added.

Add a comment to this file

kajiki/tests/data/simple.html

Empty file added.

kajiki/tests/test_ir.py

+from unittest import TestCase, main
+
+from fastpt import v2 as fpt
+from fastpt.v2 import ir 
+
+class TestBasic(TestCase):
+
+    def setUp(self):
+        self.tpl = ir.TemplateNode(
+            defs=[ir.DefNode(
+                '__call__()',
+                ir.TextNode('Hello, '),
+                ir.ExprNode('name'),
+                ir.TextNode('\n'))])
+
+    def test(self):
+        tpl = fpt.template.from_ir(self.tpl)
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == 'Hello, Rick\n', rsp
+
+class TestSwitch(TestCase):
+
+    def setUp(self):
+        self.tpl = ir.TemplateNode(
+            defs=[ir.DefNode(
+                    '__call__()',
+                    ir.ForNode(
+                        'i in range(2)',
+                        ir.ExprNode('i'),
+                        ir.TextNode(' is '),
+                        ir.SwitchNode(
+                            'i % 2',
+                            ir.CaseNode(
+                                '0',
+                                ir.TextNode('even\n')),
+                            ir.ElseNode(
+                                ir.TextNode('odd\n')))))])
+            
+    def test_basic(self):
+        tpl = fpt.template.from_ir(self.tpl)
+        rsp = tpl(dict()).__fpt__.render() 
+        assert rsp == '0 is even\n1 is odd\n', rsp
+
+class TestFunction(TestCase):
+
+    def setUp(self):
+        self.tpl = ir.TemplateNode(
+            defs=[ir.DefNode(
+                    'evenness(n)',
+                    ir.IfNode(
+                        'n % 2 == 0',
+                        ir.TextNode('even')),
+                    ir.ElseNode(
+                        ir.TextNode('odd'))),
+                  ir.DefNode(
+                    '__call__()',
+                    ir.ForNode(
+                        'i in range(2)',
+                        ir.ExprNode('i'),
+                        ir.TextNode(' is '),
+                        ir.ExprNode('evenness(i)'),
+                        ir.TextNode('\n')))])
+
+    def test_basic(self):
+        tpl = fpt.template.from_ir(self.tpl)
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '0 is even\n1 is odd\n', rsp
+
+class TestCall(TestCase):
+    
+    def setUp(self):
+        self.tpl = ir.TemplateNode(
+            defs=[ir.DefNode(
+                    'quote(caller, speaker)',
+                    ir.ForNode(
+                        'i in range(2)',
+                        ir.TextNode('Quoth '),
+                        ir.ExprNode('speaker'),
+                        ir.TextNode(', "'),
+                        ir.ExprNode('caller(i)'),
+                        ir.TextNode('."\n'))),
+                  ir.DefNode(
+                    '__call__()',
+                    ir.CallNode(
+                        '$caller(n)',
+                        "quote($caller, 'the raven')",
+                        ir.TextNode('Nevermore '),
+                        ir.ExprNode('n')))])
+            
+    def test_basic(self):
+        tpl = fpt.template.from_ir(self.tpl)
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert (
+            rsp == 'Quoth the raven, "Nevermore 0."\n'
+            'Quoth the raven, "Nevermore 1."\n'), rsp
+
+class TestImport(TestCase):
+    
+    def setUp(self):
+        lib = ir.TemplateNode(
+            defs=[ir.DefNode(
+                    'evenness(n)',
+                    ir.IfNode(
+                        'n % 2 == 0',
+                        ir.TextNode('even')),
+                    ir.ElseNode(
+                        ir.TextNode('odd'))),
+                  ir.DefNode(
+                    'half_evenness(n)',
+                    ir.TextNode(' half of '),
+                    ir.ExprNode('n'),
+                    ir.TextNode(' is '),
+                    ir.ExprNode('evenness(n/2)'))])
+        tpl = ir.TemplateNode(
+            defs=[ir.DefNode(
+                    '__call__()',
+                    ir.ImportNode(
+                        'lib.txt',
+                        'simple_function'),
+                    ir.ForNode(
+                        'i in range(4)',
+                        ir.ExprNode('i'),
+                        ir.TextNode(' is '),
+                        ir.ExprNode('simple_function.evenness(i)'),
+                        ir.ExprNode('simple_function.half_evenness(i)'),
+                        ir.TextNode('\n')))])
+        loader = fpt.loader.MockLoader({
+            'lib.txt':fpt.template.from_ir(lib),
+            'tpl.txt':fpt.template.from_ir(tpl)})
+        self.tpl = loader.import_('tpl.txt')
+
+    def test_import(self):
+        rsp = self.tpl(dict(name='Rick')).__fpt__.render()
+        assert (rsp=='0 is even half of 0 is even\n'
+                '1 is odd half of 1 is even\n'
+                '2 is even half of 2 is odd\n'
+                '3 is odd half of 3 is odd\n'), rsp
+
+class TestInclude(TestCase):
+    
+    def setUp(self):
+        hdr = ir.TemplateNode(
+            defs=[
+                ir.DefNode(
+                    '__call__()',
+                    ir.TextNode('# header\n'))])
+        tpl = ir.TemplateNode(
+            defs=[
+                ir.DefNode(
+                    '__call__()',
+                    ir.TextNode('a\n'),
+                    ir.IncludeNode('hdr.txt'),
+                    ir.TextNode('b\n'))])
+        loader = fpt.loader.MockLoader({
+            'hdr.txt':fpt.template.from_ir(hdr),
+            'tpl.txt':fpt.template.from_ir(tpl)})
+        self.tpl = loader.import_('tpl.txt')
+
+    def test_include(self):
+        rsp = self.tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == 'a\n# header\nb\n', rsp
+
+class TestExtends(TestCase):
+
+    def setUp(self):
+        parent_tpl = ir.TemplateNode(
+            defs=[
+                ir.DefNode(
+                    '__call__()',
+                    ir.ExprNode('header()'),
+                    ir.ExprNode('body()'),
+                    ir.ExprNode('footer()')),
+                ir.DefNode(
+                    'header()',
+                    ir.TextNode('# Header name='),
+                    ir.ExprNode('name'),
+                    ir.TextNode('\n')),
+                ir.DefNode(
+                    'body()',
+                    ir.TextNode('## Parent Body\n'),
+                    ir.TextNode('local.id() = '),
+                    ir.ExprNode('local.id()'),
+                    ir.TextNode('\n'),
+                    ir.TextNode('self.id() = '),
+                    ir.ExprNode('self.id()'),
+                    ir.TextNode('\n'),
+                    ir.TextNode('child.id() = '),
+                    ir.ExprNode('child.id()'),
+                    ir.TextNode('\n')),
+                ir.DefNode(
+                    'footer()',
+                    ir.TextNode('# Footer\n')),
+                ir.DefNode(
+                    'id()',
+                    ir.TextNode('parent'))])
+        mid_tpl = ir.TemplateNode(
+            defs=[
+                ir.DefNode(
+                    '__call__()',
+                    ir.ExtendNode('parent.txt')),
+                ir.DefNode(
+                    'id()',
+                    ir.TextNode('mid'))])
+        child_tpl = ir.TemplateNode(
+            defs=[
+                ir.DefNode(
+                    '__call__()',
+                    ir.ExtendNode('mid.txt')),
+                ir.DefNode(
+                    'body()',
+                    ir.TextNode('## Child Body\n'),
+                    ir.ExprNode('parent.body()')),
+                ir.DefNode(
+                    'id()',
+                    ir.TextNode('child'))])
+        loader = fpt.loader.MockLoader({
+            'parent.txt':fpt.template.from_ir(parent_tpl),
+            'mid.txt':fpt.template.from_ir(mid_tpl),
+            'child.txt':fpt.template.from_ir(child_tpl)})
+        self.loader = loader
+        self.tpl = loader.import_('child.txt')
+        
+    def test_extends(self):
+        rsp = self.tpl(dict(name='Rick')).__fpt__.render()
+        assert (rsp == '# Header name=Rick\n'
+                '## Child Body\n'
+                '## Parent Body\n'
+                'local.id() = parent\n'
+                'self.id() = child\n'
+                'child.id() = mid\n'
+                '# Footer\n'), rsp
+
+class TestDynamicExtends(TestCase):
+    def setUp(self):
+        p0 = ir.TemplateNode(
+            defs=[
+                ir.DefNode(
+                    '__call__()',
+                    ir.TextNode('Parent 0'))])
+        p1 = ir.TemplateNode(
+            defs=[
+                ir.DefNode(
+                    '__call__()',
+                    ir.TextNode('Parent 1'))])
+        child = ir.TemplateNode(
+            defs=[
+                ir.DefNode(
+                    '__call__()',
+                    ir.IfNode(
+                        'p==0',
+                        ir.ExtendNode('parent0.txt')),
+                    ir.ElseNode(
+                        ir.ExtendNode('parent1.txt')))])
+        loader = fpt.loader.MockLoader({
+            'parent0.txt':fpt.template.from_ir(p0),
+            'parent1.txt':fpt.template.from_ir(p1),
+            'child.txt':fpt.template.from_ir(child)})
+        self.loader = loader
+        self.tpl = loader.import_('child.txt')
+
+    def test_extends(self):
+        rsp = self.tpl(dict(p=0)).__fpt__.render()
+        assert rsp == 'Parent 0', rsp
+        rsp = self.tpl(dict(p=1)).__fpt__.render()
+        assert rsp == 'Parent 1', rsp
+
+if __name__ == '__main__':
+    main()

kajiki/tests/test_runtime.py

+from unittest import TestCase, main
+
+from fastpt import v2 as fpt
+
+class TestBasic(TestCase):
+
+    def setUp(self):
+        @fpt.Template
+        class tpl:
+            @fpt.expose
+            def __call__():
+                yield 'Hello,'
+                yield name
+                yield '\n'
+        self.tpl = tpl
+
+    def test_basic(self):
+        rsp = self.tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == 'Hello,Rick\n', rsp
+
+class TestSwitch(TestCase):
+
+    def setUp(self):
+        @fpt.Template
+        class tpl:
+            @fpt.expose
+            def __call__():
+                for i in range(2):
+                    yield i
+                    yield ' is '
+                    local.__fpt__.push_switch(i % 2)
+                    if local.__fpt__.case(0):
+                        yield 'even\n'
+                    else:
+                        yield 'odd\n'
+        self.tpl = tpl
+
+    def test_basic(self):
+        rsp = self.tpl().__fpt__.render()
+        assert rsp == '0 is even\n1 is odd\n', rsp
+
+class TestFunction(TestCase):
+    
+    def setUp(self):
+        @fpt.Template
+        class tpl:
+            @fpt.expose
+            def evenness(n):
+                if n % 2 == 0: yield 'even'
+                else: yield 'odd'
+            @fpt.expose
+            def __call__():
+                for i in range(2):
+                    yield i
+                    yield ' is '
+                    yield evenness(i)
+                    yield '\n'
+        self.tpl = tpl
+
+    def test_basic(self):
+        rsp = self.tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '0 is even\n1 is odd\n', rsp
+
+class TestCall(TestCase):
+    
+    def setUp(self):
+        @fpt.Template
+        class tpl:
+            @fpt.expose
+            def quote(caller, speaker):
+                for i in range(2):
+                    yield 'Quoth '
+                    yield speaker
+                    yield ', "'
+                    yield caller(i)
+                    yield '."\n'
+            @fpt.expose
+            def __call__():
+                @__fpt__.flattener.decorate
+                def _fpt_lambda(n):
+                    yield 'Nevermore '
+                    yield n
+                yield quote(_fpt_lambda, 'the raven')
+                del _fpt_lambda
+        self.tpl = tpl
+
+    def test_basic(self):
+        rsp = self.tpl(dict(name='Rick')).__fpt__.render()
+        assert (
+            rsp == 'Quoth the raven, "Nevermore 0."\n'
+            'Quoth the raven, "Nevermore 1."\n'), rsp
+
+class TestImport(TestCase):
+    def setUp(self):
+        @fpt.Template
+        class lib:
+            @fpt.expose
+            def evenness(n):
+                if n % 2 == 0: yield 'even'
+                else: yield 'odd'
+            @fpt.expose
+            def half_evenness(n):
+                yield ' half of '
+                yield n
+                yield ' is '
+                yield evenness(n/2)
+        @fpt.Template
+        class tpl:
+            @fpt.expose
+            def __call__():
+                simple_function = lib(dict(globals()))
+                for i in range(4):
+                    yield i
+                    yield ' is '
+                    yield simple_function.evenness(i)
+                    yield simple_function.half_evenness(i)
+                    yield '\n'
+        self.tpl = tpl
+
+    def test_import(self):
+        rsp = self.tpl(dict(name='Rick')).__fpt__.render()
+        assert (rsp=='0 is even half of 0 is even\n'
+                '1 is odd half of 1 is even\n'
+                '2 is even half of 2 is odd\n'
+                '3 is odd half of 3 is odd\n'), rsp
+
+class TestInclude(TestCase):
+    def setUp(self):
+        @fpt.Template
+        class hdr:
+            @fpt.expose
+            def __call__():
+                yield '# header\n'
+        @fpt.Template
+        class tpl:
+            @fpt.expose
+            def __call__():
+                yield 'a\n'
+                yield hdr().__call__()
+                yield 'b\n'
+        self.tpl = tpl
+
+    def test_include(self):
+        rsp = self.tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == 'a\n# header\nb\n', rsp
+
+class TestExtends(TestCase):
+    def setUp(_self):
+        @fpt.Template
+        class parent_tpl:
+            @fpt.expose
+            def __call__():
+                yield header()
+                yield body()
+                yield footer()
+            @fpt.expose
+            def header():
+                yield '# Header name='
+                yield name
+                yield '\n'
+            @fpt.expose
+            def body():
+                yield '## Parent Body\n'
+                yield 'local.id() = '
+                yield local.id()
+                yield '\n'
+                yield 'self.id() = '
+                yield self.id()
+                yield '\n'
+                yield 'child.id() = '
+                yield child.id()
+                yield '\n'
+            @fpt.expose
+            def footer():
+                yield '# Footer'
+                yield '\n'
+            @fpt.expose
+            def id():
+                yield 'parent'
+
+        @fpt.Template
+        class mid_tpl:
+            @fpt.expose
+            def __call__():
+                yield local.__fpt__.extend(parent_tpl).__call__()
+            @fpt.expose
+            def id():
+                yield 'mid'
+
+        @fpt.Template
+        class child_tpl:
+            @fpt.expose
+            def __call__():
+                yield local.__fpt__.extend(mid_tpl).__call__()
+            @fpt.expose
+            def body():
+                yield '## Child Body\n'
+                yield parent.body()
+            @fpt.expose
+            def id():
+                yield 'child'
+        _self.parent_tpl = parent_tpl
+        _self.child_tpl = child_tpl
+
+    def test_extends(self):
+        rsp = self.child_tpl(dict(name='Rick')).__fpt__.render()
+        assert (rsp == '# Header name=Rick\n'
+                '## Child Body\n'
+                '## Parent Body\n'
+                'local.id() = parent\n'
+                'self.id() = child\n'
+                'child.id() = mid\n'
+                '# Footer\n'), rsp
+
+class TestDynamicExtends(TestCase):
+    def setUp(_self):
+        @fpt.Template
+        class parent_0:
+            @fpt.expose
+            def __call__():
+                yield 'Parent 0'
+        @fpt.Template
+        class parent_1:
+            @fpt.expose
+            def __call__():
+                yield 'Parent 1'
+        @fpt.Template
+        class child_tpl:
+            @fpt.expose
+            def __call__():
+                if p == 0:
+                    yield local.__fpt__.extend(parent_0).__call__()
+                else:
+                    yield local.__fpt__.extend(parent_1).__call__()
+        _self.child_tpl = child_tpl
+
+    def test_extends(self):
+        rsp = self.child_tpl(dict(p=0)).__fpt__.render()
+        assert rsp == 'Parent 0', rsp
+        rsp = self.child_tpl(dict(p=1)).__fpt__.render()
+        assert rsp == 'Parent 1', rsp
+
+if __name__ == '__main__':
+    main()

kajiki/tests/test_text.py

+from unittest import TestCase, main
+
+from fastpt import v2 as fpt
+from fastpt.v2.text import TextTemplate
+
+class TestBasic(TestCase):
+
+    def test_auto_escape(self):
+        tpl = TextTemplate(source="${'<h1>'}")
+        rsp = tpl().__fpt__.render() 
+        assert rsp == '&lt;h1&gt;', rsp
+
+    def test_auto_escape_disable(self):
+        tpl = TextTemplate(source="${literal('<h1>')}")
+        rsp = tpl().__fpt__.render() 
+        assert rsp == '<h1>', rsp
+
+    def test_expr_brace(self):
+        tpl = TextTemplate(source='Hello, ${name}\n')
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == 'Hello, Rick\n', rsp
+
+    def test_expr_brace_complex(self):
+        tpl = TextTemplate(source="Hello, ${{'name':name}['name']}\n")
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == 'Hello, Rick\n', rsp
+
+    def test_expr_name(self):
+        tpl = TextTemplate(source='Hello, $name\n')
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == 'Hello, Rick\n', rsp
+        tpl = TextTemplate(source='Hello, $obj.name\n')
+        class Empty: pass
+        Empty.name = 'Rick'
+        rsp = tpl(dict(obj=Empty)).__fpt__.render() 
+        assert rsp == 'Hello, Rick\n', rsp
+
+class TestSwitch(TestCase):
+
+    def test_switch(self):
+        tpl = TextTemplate('''%for i in range(2)
+$i is {%switch i % 2 %}{%case 0%}even\n{%else%}odd\n{%end%}\\
+%end''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == '0 is even\n1 is odd\n', rsp
+
+    def test_ljust(self):
+        tpl = TextTemplate('''     %for i in range(2)
+$i is {%switch i % 2 %}{%case 0%}even\n{%else%}odd\n{%end%}\\
+%end''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == '0 is even\n1 is odd\n', rsp
+        tpl = TextTemplate('''     {%-for i in range(2)%}\\
+$i is {%switch i % 2 %}{%case 0%}even{%else%}odd{%end%}
+    {%-end%}''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == '0 is even\n1 is odd\n', rsp
+
+    def test_rstrip(self):
+        tpl = TextTemplate('''     %for i in range(2)
+$i is {%switch i % 2 %}{%case 0-%}    even\n{%else%}odd\n{%end%}\\
+%end''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == '0 is even\n1 is odd\n', rsp
+
+class TestFunction(TestCase):
+
+    def test_function(self):
+        tpl = TextTemplate('''%def evenness(n)
+{%if n % 2 == 0 %}even{%else%}odd{%end%}\\
+%end
+%for i in range(2)
+$i is ${evenness(i)}
+%end
+''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == '0 is even\n1 is odd\n', rsp
+
+class TestCall(TestCase):
+
+    def test_call(self):
+        tpl = TextTemplate('''%def quote(caller, speaker)
+    %for i in range(2)
+Quoth $speaker, "${caller(i)}."
+    %end
+%end
+%call(n) quote(%caller ,'the raven')
+Nevermore $n\\
+%end''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert (
+            rsp == 'Quoth the raven, "Nevermore 0."\n'
+            'Quoth the raven, "Nevermore 1."\n'), rsp
+
+class TestImport(TestCase):
+
+    def test_import(self):
+        lib = TextTemplate('''%def evenness(n)
+%if n % 2 == 0
+even\\
+%else
+odd\\
+%end
+%end
+%def half_evenness(n)
+ half of $n is ${evenness(n/2)}\\
+%end''')
+        tpl = TextTemplate('''%import "lib.txt" as simple_function
+%for i in range(4)
+$i is ${simple_function.evenness(i)}${simple_function.half_evenness(i)}
+%end''')
+        loader = fpt.loader.MockLoader({
+            'lib.txt':lib,
+            'tpl.txt':tpl})
+        tpl = loader.import_('tpl.txt')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert (rsp=='0 is even half of 0 is even\n'
+                '1 is odd half of 1 is even\n'
+                '2 is even half of 2 is odd\n'
+                '3 is odd half of 3 is odd\n'), rsp
+
+    def test_import_auto(self):
+        lib = TextTemplate('''%def evenness(n)
+%if n % 2 == 0
+even\\
+%else
+odd\\
+%end
+%end
+%def half_evenness(n)
+ half of $n is ${evenness(n/2)}\\
+%end''')
+        tpl = TextTemplate('''%import "lib.txt"
+%for i in range(4)
+$i is ${lib.evenness(i)}${lib.half_evenness(i)}
+%end''')
+        loader = fpt.loader.MockLoader({
+            'lib.txt':lib,
+            'tpl.txt':tpl})
+        tpl = loader.import_('tpl.txt')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert (rsp=='0 is even half of 0 is even\n'
+                '1 is odd half of 1 is even\n'
+                '2 is even half of 2 is odd\n'
+                '3 is odd half of 3 is odd\n'), rsp
+
+    def test_include(self):
+        loader = fpt.loader.MockLoader({
+                'hdr.txt': TextTemplate('# header\n'),
+                'tpl.txt': TextTemplate('''a
+%include "hdr.txt"
+b
+''')})
+        tpl = loader.import_('tpl.txt')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == 'a\n# header\nb\n', rsp
+
+class TestExtends(TestCase):
+
+    def test_basic(self):
+        parent = TextTemplate('''
+%def header()
+# Header name=$name
+%end
+%def footer()
+# Footer
+%end
+%def body()
+## Parent Body
+id() = ${id()}
+local.id() = ${local.id()}
+self.id() = ${self.id()}
+child.id() = ${child.id()}
+%end
+%def id()
+parent\\
+%end
+${header()}${body()}${footer()}''')
+        mid = TextTemplate('''%extends "parent.txt"
+%def id()
+mid\\
+%end
+''')
+        child = TextTemplate('''%extends "mid.txt"
+%def id()
+child\\
+%end
+%def body()
+## Child Body
+${parent.body()}\\
+%end
+''')
+        loader = fpt.loader.MockLoader({
+            'parent.txt':parent,
+            'mid.txt':mid,
+            'child.txt':child})
+        tpl = loader.import_('child.txt')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert (rsp == '# Header name=Rick\n'
+                '## Child Body\n'
+                '## Parent Body\n'
+                'id() = child\n'
+                'local.id() = parent\n'
+                'self.id() = child\n'
+                'child.id() = mid\n'
+                '# Footer\n'), rsp
+
+    def test_dynamic(self):
+        loader = fpt.loader.MockLoader({
+                'parent0.txt':TextTemplate('Parent 0'),
+                'parent1.txt':TextTemplate('Parent 1'),
+                'child.txt':TextTemplate('''%if p == 0
+%extends "parent0.txt"
+%else
+%extends "parent1.txt"
+%end
+''')
+                })
+        tpl = loader.import_('child.txt')
+        rsp = tpl(dict(p=0)).__fpt__.render()
+        assert rsp == 'Parent 0', rsp
+        rsp = tpl(dict(p=1)).__fpt__.render()
+        assert rsp == 'Parent 1', rsp
+
+    def test_block(self):
+        loader = fpt.loader.MockLoader({
+                'parent.txt':TextTemplate('''%def greet(name)
+Hello, $name!\\
+%end
+%def sign(name)
+Sincerely,
+$name\\
+%end
+${greet(to)}
+
+%block body
+It was good seeing you last Friday.  Thanks for the gift!
+%end
+
+${sign(from_)}
+'''),
+                'child.txt':TextTemplate('''%extends "parent.txt"
+%def greet(name)
+Dear $name:\\
+%end
+%block body
+${parent_block()}\\
+
+And don't forget you owe me money!
+%end
+''')})
+        child = loader.import_('child.txt')
+        rsp = child({'to':'Mark', 'from_':'Rick'}).__fpt__.render()
+        assert (rsp=='''Dear Mark:
+It was good seeing you last Friday.  Thanks for the gift!
+
+And don't forget you owe me money!
+
+Sincerely,
+Rick
+'''), rsp
+        
+
+class TestClosure(TestCase):
+    
+    def test(self):
+        tpl = TextTemplate('''%def add(x)
+%def inner(y)
+${x+y}\\
+%end
+${inner(x*2)}\\
+%end
+${add(5)}
+''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '15\n', rsp
+
+class TestPython(TestCase):
+
+    def test_basic(self):
+        tpl = TextTemplate('''%py
+import os
+%end
+${os.path.join('a','b','c')}''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == 'a/b/c'
+
+    def test_indent(self):
+        tpl = TextTemplate('''%py
+    import os
+    import re
+%end
+${os.path.join('a','b','c')}''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == 'a/b/c'
+
+    def test_short(self):
+        tpl = TextTemplate('''%py import os
+${os.path.join('a','b','c')}''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == 'a/b/c'
+
+    def test_mod(self):
+        tpl = TextTemplate('''%py% import os
+%def test()
+${os.path.join('a','b','c')}\\
+%end
+${test()}''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == 'a/b/c'
+
+if __name__ == '__main__':
+    main()

kajiki/tests/test_xml.py

+import os
+import xml.dom.minidom
+from unittest import TestCase, main
+
+from fastpt import v2 as fpt
+from fastpt.v2.xml_template import XMLTemplate
+
+DATA = os.path.join(
+    os.path.dirname(__file__),
+    'data')
+
+class TestParser(TestCase):
+
+    def test_parser(self):
+        doc = fpt.xml_template._Parser('<string>', '''<?xml version="1.0"?>
+<!DOCTYPE div PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<div xmlns="http://www.w3.org/1999/xhtml"
+   xmlns:py="http://genshi.edgewall.org/"
+   xmlns:xi="http://www.w3.org/2001/XInclude">
+  <?py import os ?>
+  <!-- This is a comment -->
+  <py:for each="x in range(5)">
+    Hello, $name &lt;&nbsp;&gt; $x
+  </py:for>
+</div>''').parse()
+        xml.dom.minidom.parseString(doc.toxml().encode('utf-8'))
+
+class TestExpand(TestCase):
+
+    def test_expand(self):
+        doc = fpt.xml_template._Parser('<string>', '''<div
+        py:def="def"
+        py:call="call"
+        py:case="case"
+        py:else="else"
+        py:for="for"
+        py:if="if"
+        py:switch="switch"
+        py:with="with"
+        py:replace="replace"
+        py:block="block"
+        py:extends="extends">Foo</div>''').parse()
+        fpt.xml_template.expand(doc)
+        node = doc.childNodes[0]
+        for tagname, attr in fpt.markup_template.QDIRECTIVES:
+            if node.tagName == 'div':
+                node = node.childNodes[0]
+                continue
+            assert node.tagName == tagname, '%s != %s' %(
+                node.tagName, tagname)
+            if attr:
+                assert len(node.attributes) == 1
+                assert node.hasAttribute(attr)
+                assert node.getAttribute(attr) == tagname.split(':')[-1]
+            else:
+                assert len(node.attributes) == 0
+            assert len(node.childNodes)==1
+            node = node.childNodes[0]
+
+class TestSimple(TestCase):
+
+    def test_expr_name(self):
+        tpl = XMLTemplate(source='<div>Hello, $name</div>')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div>Hello, Rick</div>', rsp
+
+    def test_expr_braced(self):
+        tpl = XMLTemplate(source='<div>Hello, ${name}</div>')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div>Hello, Rick</div>', rsp
+
+    def test_expr_brace_complex(self):
+        tpl = XMLTemplate(source="<div>Hello, ${{'name':name}['name']}</div>")
+        rsp = tpl(dict(name='Rick')).__fpt__.render() 
+        assert rsp == '<div>Hello, Rick</div>', rsp
+
+class TestSwitch(TestCase):
+
+    def test_switch(self):
+        tpl = XMLTemplate(source='''<div py:for="i in range(2)">
+$i is <py:switch test="i % 2">
+<py:case value="0">even</py:case>
+<py:else>odd</py:else>
+</py:switch></div>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '''<div>
+0 is even</div><div>
+1 is odd</div>''', rsp
+
+class TestFunction(TestCase):
+
+    def test_function(self):
+        tpl = XMLTemplate(source='''<div
+><div py:def="evenness(n)"><py:if test="n % 2 == 0">even</py:if><py:else>odd</py:else></div>
+<py:for each="i in range(2)">$i is ${evenness(i)}
+</py:for
+></div>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '''<div>
+0 is <div>even</div>
+1 is <div>odd</div>
+</div>''', rsp
+
+class TestCall(TestCase):
+
+    def test_call(self):
+        tpl = XMLTemplate(source='''<div
+><py:def function="quote(caller, speaker)"
+><ul>
+    <li py:for="i in range(2)">Quoth $speaker, ${caller(i)}</li>
+</ul></py:def
+><py:call args="n" function="quote(%caller, 'the raven')"
+>Nevermore $n</py:call></div>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '''<div><ul>
+    <li>Quoth the raven, Nevermore 0</li><li>Quoth the raven, Nevermore 1</li>
+</ul></div>''', rsp
+
+class TestImport(TestCase):
+    
+    def test_import(self):
+        loader = fpt.loader.MockLoader({
+            'lib.html':XMLTemplate(source='''<div>
+<span py:def="evenness(n)"
+    ><py:if test="n % 2 == 0"
+        >even</py:if
+    ><py:else
+        >odd</py:else
+></span>
+<py:def function="half_evenness(n)"
+    >half of $n is ${evenness(n/2)}</py:def>
+</div>'''),
+            'tpl.html':XMLTemplate(source='''<div>
+<py:import href="lib.html" alias="simple_function"
+/><ul>
+    <li py:for="i in range(4)">
+        $i is ${simple_function.evenness(i)} ${simple_function.half_evenness(i)}
+    </li>
+</ul>
+</div>''')
+            })
+        tpl = loader.import_('tpl.html')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '''<div>
+<ul>
+    <li>
+        0 is <span>even</span> half of 0 is <span>even</span>
+    </li><li>
+        1 is <span>odd</span> half of 1 is <span>even</span>
+    </li><li>
+        2 is <span>even</span> half of 2 is <span>odd</span>
+    </li><li>
+        3 is <span>odd</span> half of 3 is <span>odd</span>
+    </li>
+</ul>
+</div>''', rsp
+
+    def test_import_auto(self):
+        loader = fpt.loader.MockLoader({
+            'lib.html':XMLTemplate(source='''<div>
+<span py:def="evenness(n)"
+    ><py:if test="n % 2 == 0"
+        >even</py:if
+    ><py:else
+        >odd</py:else
+></span>
+<py:def function="half_evenness(n)"
+    >half of $n is ${evenness(n/2)}</py:def>
+</div>'''),
+            'tpl.html':XMLTemplate(source='''<div>
+<py:import href="lib.html"
+/><ul>
+    <li py:for="i in range(4)">
+        $i is ${lib.evenness(i)} ${lib.half_evenness(i)}
+    </li>
+</ul>
+</div>''')
+            })
+        tpl = loader.import_('tpl.html')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '''<div>
+<ul>
+    <li>
+        0 is <span>even</span> half of 0 is <span>even</span>
+    </li><li>
+        1 is <span>odd</span> half of 1 is <span>even</span>
+    </li><li>
+        2 is <span>even</span> half of 2 is <span>odd</span>
+    </li><li>
+        3 is <span>odd</span> half of 3 is <span>odd</span>
+    </li>
+</ul>
+</div>''', rsp
+
+    def test_include(self):
+        loader = fpt.loader.MockLoader({
+                'hdr.html':XMLTemplate('<h1>Header</h1>\n'),
+                'tpl.html':XMLTemplate('''<html><body>
+<py:include href="hdr.html"/>
+<p>This is the body</p>
+</body></html>''')
+                })
+        tpl = loader.import_('tpl.html')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '''<html><body>
+<h1>Header</h1>
+<p>This is the body</p>
+</body></html>''', rsp
+
+class TestExtends(TestCase):
+
+    def test_basic(self):
+        loader = fpt.loader.MockLoader({
+                'parent.html':XMLTemplate('''<div
+><h1 py:def="header()">Header name=$name</h1
+><h6 py:def="footer()">Footer</h6
+><div py:def="body()">
+id() = ${id()}
+local.id() = ${local.id()}
+self.id() = ${self.id()}
+child.id() = ${child.id()}
+</div><span py:def="id()">parent</span>
+${header()}
+${body()}
+${footer()}
+</div>'''),
+                'mid.html':XMLTemplate('''<py:extends href="parent.html"
+><span py:def="id()">mid</span
+></py:extends>'''),
+                'child.html':XMLTemplate('''<py:extends href="mid.html"
+><span py:def="id()">child</span
+><div py:def="body()">
+<h2>Child Body</h2>
+${parent.body()}
+</div></py:extends>''')})
+        tpl = loader.import_('child.html')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp=='''<div>
+<h1>Header name=Rick</h1>
+<div>
+<h2>Child Body</h2>
+<div>
+id() = <span>child</span>
+local.id() = <span>parent</span>
+self.id() = <span>child</span>
+child.id() = <span>mid</span>
+</div>
+</div>
+<h6>Footer</h6>
+</div>''', rsp
+
+    def test_dynamic(self):
+        loader = fpt.loader.MockLoader({
+                'parent0.html':XMLTemplate('<span>Parent 0</span>'),
+                'parent1.html':XMLTemplate('<span>Parent 1</span>'),
+                'child.html':XMLTemplate('''<div
+><py:if test="p == 0"><py:extends href="parent0.html"/></py:if
+><py:else><py:extends href="parent1.html"/></py:else
+></div>
+''')
+                })
+        tpl = loader.import_('child.html')
+        rsp = tpl(dict(p=0)).__fpt__.render()
+        assert rsp == '<div><span>Parent 0</span></div>', rsp
+        rsp = tpl(dict(p=1)).__fpt__.render()
+        assert rsp == '<div><span>Parent 1</span></div>', rsp
+
+    def test_block(self):
+        loader = fpt.loader.MockLoader({
+                'parent.html':XMLTemplate('''<div
+><py:def function="greet(name)"
+>Hello, $name!</py:def
+><py:def function="sign(name)"
+>Sincerely,<br/><em>$name</em></py:def
+>${greet(to)}
+
+<p py:block="body">It was good seeing you last Friday.
+Thanks for the gift!</p>
+
+${sign(from_)}
+</div>'''),
+                'child.html':XMLTemplate('''<py:extends href="parent.html"
+><py:def function="greet(name)"
+>Dear $name:</py:def
+><py:block name="body">${parent_block()}
+<p>And don't forget you owe me money!</p>
+</py:block
+></py:extends>
+''')})
+        parent = loader.import_('parent.html')
+        rsp = parent({'to':'Mark', 'from_':'Rick'}).__fpt__.render()
+        assert rsp == '''<div>Hello, Mark!
+
+<p>It was good seeing you last Friday.
+Thanks for the gift!</p>
+
+Sincerely,<br/><em>Rick</em>
+</div>''', rsp
+        child = loader.import_('child.html')
+        rsp = child({'to':'Mark', 'from_':'Rick'}).__fpt__.render()
+        assert rsp=='''<div>Dear Mark:
+
+<p>It was good seeing you last Friday.
+Thanks for the gift!</p>
+<p>And don't forget you owe me money!</p>
+
+
+Sincerely,<br/><em>Rick</em>
+</div>''', rsp
+
+class TestClosure(TestCase):
+    
+    def test(self):
+        tpl = XMLTemplate('''<div
+><py:def function="add(x)"
+    ><py:def function="inner(y)"
+        >${x+y}</py:def
+    >${inner(x*2)}</py:def
+>${add(5)}</div>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div>15</div>', rsp
+
+class TestPython(TestCase):
+
+    def test_basic(self):
+        tpl = XMLTemplate('''<div
+><?py
+import os
+?>${os.path.join('a', 'b', 'c')}</div>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div>a/b/c</div>'
+
+    def test_indent(self):
+        tpl = XMLTemplate('''<div
+><?py #
+    import os
+    import re
+?>${os.path.join('a','b','c')}</div>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div>a/b/c</div>'
+
+    def test_short(self):
+        tpl = XMLTemplate('''<div
+><?py import os
+?>${os.path.join('a', 'b', 'c')}</div>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div>a/b/c</div>'
+
+    def test_mod(self):
+        tpl = XMLTemplate('''<div
+><?py %import os
+?><py:def function="test()"
+>${os.path.join('a', 'b', 'c')}</py:def
+>${test()}</div>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div>a/b/c</div>'
+
+class TestComment(TestCase):
+
+    def test_basic(self):
+        tpl = XMLTemplate('''<div>
+<!-- This comment is preserved. -->
+<!--! This comment is stripped. -->
+</div>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '''<div>
+<!--  This comment is preserved.  -->
+
+</div>''', rsp
+
+class TestAttributes(TestCase):
+
+    def test_basic(self):
+        tpl = XMLTemplate('''<div id="foo"/>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div id="foo"/>', rsp
+        
+    def test_content(self):
+        tpl = XMLTemplate('''<div py:content="'foo'"/>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div>foo</div>', rsp
+        
+    def test_replace(self):
+        tpl = XMLTemplate('''<div py:replace="'foo'"/>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == 'foo', rsp
+
+    def test_attrs(self):
+        tpl = XMLTemplate('''<div py:attrs="dict(a=5, b=6)"/>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div a="5" b="6"/>'
+        tpl = XMLTemplate('''<div py:attrs="[('a', 5), ('b', 6)]"/>''')
+        rsp = tpl(dict(name='Rick')).__fpt__.render()
+        assert rsp == '<div a="5" b="6"/>'
+
+    def test_strip(self):
+        tpl = XMLTemplate('''<div><h1 py:strip="header">Header</h1></div>''')
+        rsp = tpl(dict(header=True)).__fpt__.render()
+        assert rsp == '<div><h1>Header</h1></div>', rsp
+        rsp = tpl(dict(header=False)).__fpt__.render()
+        assert rsp == '<div>Header</div>', rsp
+
+if __name__ == '__main__':
+    main()
+'''Text template compiler
+
+Notable in this module are
+
+TextTemplate - function building a template from text string or filename
+_pattern - the regex used to find the beginnings of tags and expressions
+_Scanner - scans text and generates a stream of tokens
+_Parser - parses a stream of tokens into the internal representation (IR) tree
+_Parser._parse_<tagname> - consumes the body of a tag and returns an ir.Node
+'''
+import re
+import shlex
+from collections import defaultdict
+from itertools import chain
+
+from fastpt import v2 as fpt
+from fastpt.v2 import ir
+
+_pattern = r'''
+\$(?:
+    (?P<expr_escaped>\$) |      # Escape $$
+    (?P<expr_named>[_a-z][_a-z0-9.]*) | # $foo.bar
+    {(?P<expr_braced>) | # ${....
+    (?P<expr_invalid>)
+) |
+^\s*%(?:
+    (?P<tag_bare>[a-z]+) | # %for, %end, etc.
+    (?P<tag_bare_invalid>)
+)|
+^\s*{%-(?P<tag_begin_ljust>[a-z]+)|  # {%-for, {%-end, etc.
+{%(?:
+    (?P<tag_begin>[a-z]+) | # {%for, {%end, etc.
+    (?P<tag_begin_invalid>)
+)
+'''
+_re_pattern = re.compile(_pattern, re.VERBOSE | re.IGNORECASE|re.MULTILINE)
+
+def TextTemplate(
+    source=None,
+    filename=None):
+    if source is None:
+        source = open(filename).read()
+    if filename is None:
+        filename = '<string>'
+    scanner = _Scanner(filename, source)
+    tree = _Parser(scanner).parse()
+    return fpt.template.from_ir(tree)
+
+class _Scanner(object):
+
+    def __init__(self, filename, source):
+        self.filename = filename
+        self.source = source
+        self.lineno = 1
+        self.pos = 0
+
+    def __iter__(self):
+        source = self.source
+        for mo in _re_pattern.finditer(source):
+            start = mo.start()
+            if start > self.pos:
+                yield self.text(source[self.pos:start])
+                self.pos = start
+            groups = mo.groupdict()
+            if groups['expr_braced'] is not None:
+                self.pos = mo.end()
+                yield self._get_braced_expr()
+            elif groups['expr_named'] is not None:
+                self.pos = mo.end()
+                yield self.expr(groups['expr_named'])
+            elif groups['tag_bare'] is not None:
+                self.pos = mo.end()
+                yield self._get_tag_bare(groups['tag_bare'])
+            elif groups['tag_begin'] is not None:
+                self.pos = mo.end()
+                yield self._get_tag(groups['tag_begin'])
+            elif groups['tag_begin_ljust'] is not None:
+                self.pos = mo.end()
+                yield self._get_tag(groups['tag_begin_ljust'])
+            elif groups['tag_bare_invalid'] is not None:
+                continue
+            else:
+                msg = 'Syntax error %s:%s' % (self.filename, self.lineno)
+                for i, line in enumerate(self.source.splitlines()):
+                    print '%3d %s' % (i+1, line)
+                print msg
+                assert False, groups
+        if self.pos != len(source):
+            yield self.text(source[self.pos:])
+
+    def _get_pos(self):
+        return self._pos
+    def _set_pos(self, value):
+        assert value >= getattr(self, '_pos', 0)
+        self._pos = value
+    pos = property(_get_pos, _set_pos)
+
+    def text(self, text):
+        self.lineno += text.count('\n')
+        return _Text(self.filename, self.lineno, text)
+
+    def expr(self, text):
+        self.lineno += text.count('\n')
+        return _Expr(self.filename, self.lineno, text)
+
+    def tag(self, tagname, body):
+        tag = _Tag(self.filename, self.lineno, tagname, body)
+        self.lineno += tag.text.count('\n')
+        return tag
+
+    def _get_tag_bare(self, tagname):
+        end = self.source.find('\n', self.pos)
+        if end == -1:
+            end = len(self.source)
+        body = self.source[self.pos:end]
+        self.lineno += 1
+        self.pos = end+1
+        return self.tag(tagname, body)
+
+    def _get_tag(self, tagname):
+        end = self.source.find('%}', self.pos)
+        assert end > 0
+        body = self.source[self.pos:end]
+        self.pos = end+2
+        if body.endswith('-'):
+            body = body[:-1]
+            while self.source[self.pos] in ' \t':
+                self.pos += 1
+        return self.tag(tagname, body)
+
+    def _get_braced_expr(self):
+        try:
+            compile(self.source[self.pos:], '', 'eval')
+        except SyntaxError, se:
+            end = se.offset+self.pos
+            text = self.source[self.pos:end-1]
+            self.pos = end
+            return self.expr(text)
+    
+class _Parser(object):
+
+    def __init__(self, tokenizer):
+        self.tokenizer = tokenizer
+        self.functions = defaultdict(list)
+        self.functions['__call__()'] = []
+        self.mod_py = [] # module-level python blocks
+        self.iterator = iter(self.tokenizer)
+        self._in_def = False
+        self._is_child = False
+
+    def parse(self):
+        body = list(self._parse_body())
+        self.functions['__call__()'] = body[:-1]
+        defs = [ ir.DefNode(k, *v) for k,v in self.functions.iteritems() ]
+        return ir.TemplateNode(self.mod_py, defs)
+
+    def text(self, token):
+        text = ''.join(_unescape_newlines(token.text))
+        node = ir.TextNode(text)
+        node.filename = token.filename
+        node.lineno = token.lineno
+        return node
+
+    def expr(self, token):
+        node = ir.ExprNode(token.text)
+        node.filename = token.filename
+        node.lineno = token.lineno
+        return node
+
+    def push_tok(self, token):
+        self.iterator = chain([token], self.iterator)
+
+    def _parse_body(self, *stoptags):
+        while True:
+            try:
+                token = self.iterator.next()
+                if isinstance(token, _Text):
+                    yield self.text(token)
+                elif isinstance(token, _Expr):
+                    yield self.expr(token)
+                elif isinstance(token, _Tag):
+                    if token.tagname in stoptags:
+                        yield token
+                        break
+                    parser = getattr(self, '_parse_%s' % token.tagname)
+                    yield parser(token)
+                else:
+                    msg = 'Parse error: %r unexpected' % token
+                    assert False, msg
+            except StopIteration:
+                yield None
+                break
+
+    def _parse_def(self, token):
+        old_in_def, self._in_def = self._in_def, True
+        body = list(self._parse_body('end'))
+        self._in_def = old_in_def
+        if self._in_def:
+            return ir.InnerDefNode(token.body, *body[:-1])
+        else:
+            self.functions[token.body.strip()] = body[:-1]
+            return None
+
+    def _parse_call(self, token):
+        b = token.body.find('(')
+        e = token.body.find(')', b)
+        assert e > b > -1
+        arglist = token.body[b:e+1]
+        call = token.body[e+1:].strip()
+        body = list(self._parse_body('end'))
+        return ir.CallNode(
+            '$caller%s' % arglist,
+            call.replace('%caller', '$caller'),
+            *body[:-1])
+
+    def _parse_if(self, token):
+        body = list(self._parse_body('end', 'else'))
+        stoptok = body[-1]
+        if stoptok.tagname == 'else':
+            self.push_tok(stoptok)
+        return ir.IfNode(token.body, *body[:-1])
+
+    def _parse_for(self, token):
+        body = list(self._parse_body('end'))
+        return ir.ForNode(token.body, *body[:-1])
+
+    def _parse_switch(self, token):
+        body = list(self._parse_body('end'))
+        return ir.SwitchNode(token.body, *body[:-1])
+
+    def _parse_case(self, token):
+        body = list(self._parse_body('case', 'else', 'end'))
+        stoptok = body[-1]
+        self.push_tok(stoptok)
+        return ir.CaseNode(token.body, *body[:-1])
+
+    def _parse_else(self, token):
+        body = list(self._parse_body('end'))
+        return ir.ElseNode(*body[:-1])
+
+    def _parse_extends(self, token):
+        parts = shlex.split(token.body)
+        fn = parts[0]
+        assert len(parts) == 1
+        self._is_child = True
+        return ir.ExtendNode(fn)
+
+    def _parse_import(self, token):
+        parts = shlex.split(token.body)
+        fn = parts[0]
+        if len(parts) > 1:
+            assert parts[1] == 'as'
+            return ir.ImportNode(fn, parts[2])
+        else:
+            return ir.ImportNode(fn)
+
+    def _parse_include(self, token):
+        parts = shlex.split(token.body)
+        fn = parts[0]
+        assert len(parts) == 1
+        return ir.IncludeNode(fn)
+
+    def _parse_py(self, token):
+        body = token.body.strip()
+        if body:
+            body = [ ir.TextNode(body), None ]
+        else:
+            body = list(self._parse_body('end'))
+        node = ir.PythonNode(*body[:-1])
+        if node.module_level:
+            self.mod_py.append(node)
+            return None
+        else:
+            return node
+
+    def _parse_block(self, token):
+        fname = '_fpt_block_' + token.body.strip()
+        decl = fname + '()'
+        body = list(self._parse_body('end'))[:-1]
+        self.functions[decl] = body
+        if self._is_child:
+            parent_block = 'parent.' + fname
+            body.insert(0, ir.PythonNode(ir.TextNode('parent_block=%s' % parent_block)))
+            return None
+        else:
+            return ir.ExprNode(decl)
+
+class _Token(object):
+    def __init__(self, filename, lineno, text):
+        self.filename = filename
+        self.lineno = lineno
+        self.text = text
+
+    def __repr__(self): # pragma no cover
+        return '<%s %r>' % (
+            self.__class__.__name__,
+            self.text)
+
+class _Expr(_Token): pass
+class _Text(_Token): pass
+class _Tag(_Token):
+    def __init__(self, filename, lineno, tagname, body):
+        self.tagname = tagname
+        self.body = body
+        text = tagname + ' ' + body
+        super(_Tag, self).__init__(filename, lineno, text)
+
+def _unescape_newlines(text):
+    i = 0
+    while i < len(text):
+        if text[i] == '\\':
+            if text[i+1] != '\n':
+                yield text[i+1]
+            i += 2
+        else:
+            yield text[i]
+            i += 1
+            
+            
+    
+import sys
+from random import randint
+from threading import local
+
+from webhelpers.html import literal
+
+def debug():# pragma no cover
+    def pm(etype, value, tb): 
+        import pdb, traceback
+        try:
+            from IPython.ipapi import make_session; make_session()
+            from IPython.Debugger import Pdb
+            sys.stderr.write('Entering post-mortem IPDB shell\n')
+            p = Pdb(color_scheme='Linux')
+            p.reset()
+            p.setup(None, tb)
+            p.print_stack_trace()
+            sys.stderr.write('%s: %s\n' % ( etype, value))
+            p.cmdloop()
+            p.forget()
+            # p.interaction(None, tb)
+        except ImportError:
+            sys.stderr.write('Entering post-mortem PDB shell\n')
+            traceback.print_exception(etype, value, tb)
+            pdb.post_mortem(tb)
+    sys.excepthook = pm
+
+def expose(func):
+    func.exposed = True
+    return func
+
+class Undefined(object): pass
+UNDEFINED=Undefined()
+
+class flattener(object):
+
+    def __init__(self, iterator):
+        self.iterator = iterator
+
+    @classmethod
+    def decorate(cls, func):
+        def inner(*args, **kwargs):
+            return cls(func(*args, **kwargs))
+        return inner
+
+    def __iter__(self):
+        for x in self.iterator:
+            if isinstance(x, flattener):
+                for xx in x:
+                    yield xx
+            else:
+                yield x
+
+class NameGen(object):
+    lcl = local()
+    def __init__(self):
+        self.names = set()
+
+    @classmethod
+    def gen(cls, hint):
+        if not hasattr(cls.lcl, 'inst'):
+            cls.lcl.inst = NameGen()
+        return cls.lcl.inst._gen(hint)
+
+    def _gen(self, hint):
+        r = hint
+        while r in self.names:
+            r = '%s_%d' % (hint, randint(0, 999))
+        self.names.add(r)
+        return r
+
+def gen_name(hint='_fpt_'):
+    return NameGen.gen(hint)
+    

kajiki/xml_template.py

+import re
+from collections import defaultdict
+from cStringIO import StringIO
+from xml import sax
+from htmllib import HTMLParser
+from xml.dom import minidom as dom
+
+from . import ir
+from . import template
+from .markup_template import QDIRECTIVES, QDIRECTIVES_DICT
+
+_pattern = r'''
+\$(?:
+    (?P<expr_escaped>\$) |      # Escape $$
+    (?P<expr_named>[_a-z][_a-z0-9.]*) | # $foo.bar
+    {(?P<expr_braced>) | # ${....
+    (?P<expr_invalid>)
+)'''
+_re_pattern = re.compile(_pattern, re.VERBOSE | re.IGNORECASE|re.MULTILINE)
+
+def XMLTemplate(
+    source=None,
+    filename=None):
+    if source is None:
+        source = open(filename).read()
+    if filename is None:
+        filename = '<string>'
+    doc = _Parser(filename, source).parse()
+    expand(doc)
+    ir_ = _Compiler(filename, doc).compile()
+    return template.from_ir(ir_)
+
+class _Compiler(object):
+
+    def __init__(self, filename, doc, mode='xml'):
+        self.filename = filename
+        self.doc = doc
+        self.mode = mode
+        self.functions = defaultdict(list)
+        self.functions['__call__()'] = []
+        self.mod_py = []
+        self.in_def = False
+        self.is_child = False
+
+    def compile(self):
+        body = list(self._compile_node(self.doc.firstChild))
+        self.functions['__call__()'] = body
+        defs = [ ir.DefNode(k, *v) for k,v in self.functions.iteritems() ]