changeset 336:5f2c7782cd8a

Refactoring: `genshi.template` is now a package, it was getting way to crowded in that file.
author cmlenz
date Wed, 08 Nov 2006 15:50:15 +0000
parents e14a0332cfdc
children fd1c77710fec 6c8b7a1fb50d
files ChangeLog UPGRADE.txt genshi/__init__.py genshi/eval.py genshi/plugin.py genshi/template.py genshi/template/__init__.py genshi/template/core.py genshi/template/directives.py genshi/template/eval.py genshi/template/loader.py genshi/template/markup.py genshi/template/plugin.py genshi/template/tests/__init__.py genshi/template/tests/core.py genshi/template/tests/directives.py genshi/template/tests/eval.py genshi/template/tests/loader.py genshi/template/tests/markup.py genshi/template/tests/text.py genshi/template/text.py genshi/tests/__init__.py genshi/tests/eval.py genshi/tests/template.py setup.py
diffstat 24 files changed, 4015 insertions(+), 3784 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -24,6 +24,8 @@
  * Fix XPath traversal in match templates. Previously, `div/p` would be treated 
    the same as `div//p`, i.e. it would match all descendants and not just the
    immediate children.
+ * Split up the `genshi.template` module into multiple modules inside the new
+   `genshi.template` package.
 
 Version 0.3.4
 http://svn.edgewall.org/repos/genshi/tags/0.3.4/
--- a/UPGRADE.txt
+++ b/UPGRADE.txt
@@ -1,6 +1,16 @@
 Upgrading Genshi
 ================
 
+Upgrading from Genshi 0.3.x to 0.4.x
+------------------------------------
+
+The `genshi.template` module has been refactored into a package with
+multiple modules. While code using the normal templating APIs should
+continue to work without problems, you should make sure to remove any
+leftover traces of the `template.py` file on the installation path.
+This is not necessary when Genshi was installed as a Python egg.
+
+
 Upgrading from Markup
 ---------------------
 
--- a/genshi/__init__.py
+++ b/genshi/__init__.py
@@ -17,39 +17,6 @@
 The design is centered around the concept of streams of markup events (similar
 in concept to SAX parsing events) which can be processed in a uniform manner
 independently of where or how they are produced.
-
-
-Generating content
-------------------
-
-Literal XML and HTML text can be used to easily produce markup streams
-via helper functions in the `genshi.input` module:
-
->>> from genshi.input import XML
->>> doc = XML('<html lang="en"><head><title>My document</title></head></html>')
-
-This results in a `Stream` object that can be used in a number of way.
-
->>> doc.render(method='html', encoding='utf-8')
-'<html lang="en"><head><title>My document</title></head></html>'
-
->>> from genshi.input import HTML
->>> doc = HTML('<HTML lang=en><HEAD><TITLE>My document</HTML>')
->>> doc.render(method='html', encoding='utf-8')
-'<html lang="en"><head><title>My document</title></head></html>'
-
->>> title = doc.select('head/title')
->>> title.render(method='html', encoding='utf-8')
-'<title>My document</title>'
-
-
-Markup streams can also be generated programmatically using the
-`genshi.builder` module:
-
->>> from genshi.builder import tag
->>> doc = tag.doc(tag.title('My document'), lang='en')
->>> doc.generate().render(method='html')
-'<doc lang="en"><title>My document</title></doc>'
 """
 
 from genshi.core import *
deleted file mode 100644
--- a/genshi/eval.py
+++ /dev/null
@@ -1,427 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://genshi.edgewall.org/wiki/License.
-#
-# This software consists of voluntary contributions made by many
-# individuals. For the exact contribution history, see the revision
-# history and logs, available at http://genshi.edgewall.org/log/.
-
-"""Support for "safe" evaluation of Python expressions."""
-
-import __builtin__
-from compiler import ast, parse
-from compiler.pycodegen import ExpressionCodeGenerator
-import new
-
-__all__ = ['Expression', 'Undefined']
-
-
-class Expression(object):
-    """Evaluates Python expressions used in templates.
-
-    >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
-    >>> Expression('test').evaluate(data)
-    'Foo'
-
-    >>> Expression('items[0]').evaluate(data)
-    1
-    >>> Expression('items[-1]').evaluate(data)
-    3
-    >>> Expression('dict["some"]').evaluate(data)
-    'thing'
-    
-    Similar to e.g. Javascript, expressions in templates can use the dot
-    notation for attribute access to access items in mappings:
-    
-    >>> Expression('dict.some').evaluate(data)
-    'thing'
-    
-    This also works the other way around: item access can be used to access
-    any object attribute (meaning there's no use for `getattr()` in templates):
-    
-    >>> class MyClass(object):
-    ...     myattr = 'Bar'
-    >>> data = dict(mine=MyClass(), key='myattr')
-    >>> Expression('mine.myattr').evaluate(data)
-    'Bar'
-    >>> Expression('mine["myattr"]').evaluate(data)
-    'Bar'
-    >>> Expression('mine[key]').evaluate(data)
-    'Bar'
-    
-    All of the standard Python operators are available to template expressions.
-    Built-in functions such as `len()` are also available in template
-    expressions:
-    
-    >>> data = dict(items=[1, 2, 3])
-    >>> Expression('len(items)').evaluate(data)
-    3
-    """
-    __slots__ = ['source', 'code']
-
-    def __init__(self, source, filename=None, lineno=-1):
-        """Create the expression, either from a string, or from an AST node.
-        
-        @param source: either a string containing the source code of the
-            expression, or an AST node
-        @param filename: the (preferably absolute) name of the file containing
-            the expression
-        @param lineno: the number of the line on which the expression was found
-        """
-        if isinstance(source, basestring):
-            self.source = source
-            self.code = _compile(_parse(source), self.source, filename=filename,
-                                 lineno=lineno)
-        else:
-            assert isinstance(source, ast.Node)
-            self.source = '?'
-            self.code = _compile(ast.Expression(source), filename=filename,
-                                 lineno=lineno)
-
-    def __repr__(self):
-        return 'Expression(%r)' % self.source
-
-    def evaluate(self, data, nocall=False):
-        """Evaluate the expression against the given data dictionary.
-        
-        @param data: a mapping containing the data to evaluate against
-        @param nocall: if true, the result of the evaluation is not called if
-            if it is a callable
-        @return: the result of the evaluation
-        """
-        retval = eval(self.code, {'data': data,
-                                  '_lookup_name': _lookup_name,
-                                  '_lookup_attr': _lookup_attr,
-                                  '_lookup_item': _lookup_item},
-                                 {'data': data})
-        if not nocall and type(retval) is not Undefined and callable(retval):
-            retval = retval()
-        return retval
-
-
-class Undefined(object):
-    """Represents a reference to an undefined variable.
-    
-    Unlike the Python runtime, template expressions can refer to an undefined
-    variable without causing a `NameError` to be raised. The result will be an
-    instance of the `Undefined´ class, which is treated the same as `False` in
-    conditions, and acts as an empty collection in iterations:
-    
-    >>> foo = Undefined('foo')
-    >>> bool(foo)
-    False
-    >>> list(foo)
-    []
-    >>> print foo
-    undefined
-    
-    However, calling an undefined variable, or trying to access an attribute
-    of that variable, will raise an exception that includes the name used to
-    reference that undefined variable.
-    
-    >>> foo('bar')
-    Traceback (most recent call last):
-        ...
-    NameError: Variable "foo" is not defined
-
-    >>> foo.bar
-    Traceback (most recent call last):
-        ...
-    NameError: Variable "foo" is not defined
-    """
-    __slots__ = ['_name']
-
-    def __init__(self, name):
-        self._name = name
-
-    def __call__(self, *args, **kwargs):
-        __traceback_hide__ = True
-        self.throw()
-
-    def __getattr__(self, name):
-        __traceback_hide__ = True
-        self.throw()
-
-    def __iter__(self):
-        return iter([])
-
-    def __nonzero__(self):
-        return False
-
-    def __repr__(self):
-        return 'undefined'
-
-    def throw(self):
-        __traceback_hide__ = True
-        raise NameError('Variable "%s" is not defined' % self._name)
-
-
-def _parse(source, mode='eval'):
-    if isinstance(source, unicode):
-        source = '\xef\xbb\xbf' + source.encode('utf-8')
-    return parse(source, mode)
-
-def _compile(node, source=None, filename=None, lineno=-1):
-    tree = ExpressionASTTransformer().visit(node)
-    if isinstance(filename, unicode):
-        # unicode file names not allowed for code objects
-        filename = filename.encode('utf-8', 'replace')
-    elif not filename:
-        filename = '<string>'
-    tree.filename = filename
-    if lineno <= 0:
-        lineno = 1
-
-    gen = ExpressionCodeGenerator(tree)
-    gen.optimized = True
-    code = gen.getCode()
-
-    # We'd like to just set co_firstlineno, but it's readonly. So we need to
-    # clone the code object while adjusting the line number
-    return new.code(0, code.co_nlocals, code.co_stacksize,
-                    code.co_flags | 0x0040, code.co_code, code.co_consts,
-                    code.co_names, code.co_varnames, filename,
-                    '<Expression %s>' % (repr(source or '?').replace("'", '"')),
-                    lineno, code.co_lnotab, (), ())
-
-BUILTINS = __builtin__.__dict__.copy()
-BUILTINS['Undefined'] = Undefined
-
-def _lookup_name(data, name, locals_=None):
-    __traceback_hide__ = True
-    val = Undefined
-    if locals_:
-        val = locals_.get(name, val)
-    if val is Undefined:
-        val = data.get(name, val)
-        if val is Undefined:
-            val = BUILTINS.get(name, val)
-            if val is not Undefined or name == 'Undefined':
-                return val
-        else:
-            return val
-    else:
-        return val
-    return val(name)
-
-def _lookup_attr(data, obj, key):
-    __traceback_hide__ = True
-    if type(obj) is Undefined:
-        obj.throw()
-    if hasattr(obj, key):
-        return getattr(obj, key)
-    try:
-        return obj[key]
-    except (KeyError, TypeError):
-        return Undefined(key)
-
-def _lookup_item(data, obj, key):
-    __traceback_hide__ = True
-    if type(obj) is Undefined:
-        obj.throw()
-    if len(key) == 1:
-        key = key[0]
-    try:
-        return obj[key]
-    except (KeyError, IndexError, TypeError), e:
-        if isinstance(key, basestring):
-            val = getattr(obj, key, Undefined)
-            if val is Undefined:
-                val = Undefined(key)
-            return val
-        raise
-
-
-class ASTTransformer(object):
-    """General purpose base class for AST transformations.
-    
-    Every visitor method can be overridden to return an AST node that has been
-    altered or replaced in some way.
-    """
-    _visitors = {}
-
-    def visit(self, node, *args, **kwargs):
-        v = self._visitors.get(node.__class__)
-        if not v:
-            v = getattr(self, 'visit%s' % node.__class__.__name__)
-            self._visitors[node.__class__] = v
-        return v(node, *args, **kwargs)
-
-    def visitExpression(self, node, *args, **kwargs):
-        node.node = self.visit(node.node, *args, **kwargs)
-        return node
-
-    # Functions & Accessors
-
-    def visitCallFunc(self, node, *args, **kwargs):
-        node.node = self.visit(node.node, *args, **kwargs)
-        node.args = [self.visit(x, *args, **kwargs) for x in node.args]
-        if node.star_args:
-            node.star_args = self.visit(node.star_args, *args, **kwargs)
-        if node.dstar_args:
-            node.dstar_args = self.visit(node.dstar_args, *args, **kwargs)
-        return node
-
-    def visitLambda(self, node, *args, **kwargs):
-        node.code = self.visit(node.code, *args, **kwargs)
-        node.filename = '<string>' # workaround for bug in pycodegen
-        return node
-
-    def visitGetattr(self, node, *args, **kwargs):
-        node.expr = self.visit(node.expr, *args, **kwargs)
-        return node
-
-    def visitSubscript(self, node, *args, **kwargs):
-        node.expr = self.visit(node.expr, *args, **kwargs)
-        node.subs = [self.visit(x, *args, **kwargs) for x in node.subs]
-        return node
-
-    # Operators
-
-    def _visitBoolOp(self, node, *args, **kwargs):
-        node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
-        return node
-    visitAnd = visitOr = visitBitand = visitBitor = _visitBoolOp
-
-    def _visitBinOp(self, node, *args, **kwargs):
-        node.left = self.visit(node.left, *args, **kwargs)
-        node.right = self.visit(node.right, *args, **kwargs)
-        return node
-    visitAdd = visitSub = _visitBinOp
-    visitDiv = visitFloorDiv = visitMod = visitMul = visitPower = _visitBinOp
-    visitLeftShift = visitRightShift = _visitBinOp
-
-    def visitCompare(self, node, *args, **kwargs):
-        node.expr = self.visit(node.expr, *args, **kwargs)
-        node.ops = [(op, self.visit(n, *args, **kwargs)) for op, n in  node.ops]
-        return node
-
-    def _visitUnaryOp(self, node, *args, **kwargs):
-        node.expr = self.visit(node.expr, *args, **kwargs)
-        return node
-    visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp
-    visitBackquote = _visitUnaryOp
-
-    # Identifiers, Literals and Comprehensions
-
-    def _visitDefault(self, node, *args, **kwargs):
-        return node
-    visitAssName = visitAssTuple = _visitDefault
-    visitConst = visitName = _visitDefault
-
-    def visitDict(self, node, *args, **kwargs):
-        node.items = [(self.visit(k, *args, **kwargs),
-                       self.visit(v, *args, **kwargs)) for k, v in node.items]
-        return node
-
-    def visitGenExpr(self, node, *args, **kwargs):
-        node.code = self.visit(node.code, *args, **kwargs)
-        node.filename = '<string>' # workaround for bug in pycodegen
-        return node
-
-    def visitGenExprFor(self, node, *args, **kwargs):
-        node.assign = self.visit(node.assign, *args, **kwargs)
-        node.iter = self.visit(node.iter, *args, **kwargs)
-        node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
-        return node
-
-    def visitGenExprIf(self, node, *args, **kwargs):
-        node.test = self.visit(node.test, *args, **kwargs)
-        return node
-
-    def visitGenExprInner(self, node, *args, **kwargs):
-        node.expr = self.visit(node.expr, *args, **kwargs)
-        node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
-        return node
-
-    def visitKeyword(self, node, *args, **kwargs):
-        node.expr = self.visit(node.expr, *args, **kwargs)
-        return node
-
-    def visitList(self, node, *args, **kwargs):
-        node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
-        return node
-
-    def visitListComp(self, node, *args, **kwargs):
-        node.expr = self.visit(node.expr, *args, **kwargs)
-        node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
-        return node
-
-    def visitListCompFor(self, node, *args, **kwargs):
-        node.assign = self.visit(node.assign, *args, **kwargs)
-        node.list = self.visit(node.list, *args, **kwargs)
-        node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
-        return node
-
-    def visitListCompIf(self, node, *args, **kwargs):
-        node.test = self.visit(node.test, *args, **kwargs)
-        return node
-
-    def visitSlice(self, node, *args, **kwargs):
-        node.expr = self.visit(node.expr, locals_=True, *args, **kwargs)
-        if node.lower is not None:
-            node.lower = self.visit(node.lower, *args, **kwargs)
-        if node.upper is not None:
-            node.upper = self.visit(node.upper, *args, **kwargs)
-        return node
-
-    def visitSliceobj(self, node, *args, **kwargs):
-        node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
-        return node
-
-    def visitTuple(self, node, *args, **kwargs):
-        node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
-        return node
-
-
-class ExpressionASTTransformer(ASTTransformer):
-    """Concrete AST transformer that implements the AST transformations needed
-    for template expressions.
-    """
-
-    def visitConst(self, node, locals_=False):
-        if isinstance(node.value, str):
-            return ast.Const(node.value.decode('utf-8'))
-        return node
-
-    def visitGenExprIf(self, node, *args, **kwargs):
-        node.test = self.visit(node.test, locals_=True)
-        return node
-
-    def visitGenExprInner(self, node, *args, **kwargs):
-        node.expr = self.visit(node.expr, locals_=True)
-        node.quals = [self.visit(x) for x in node.quals]
-        return node
-
-    def visitGetattr(self, node, locals_=False):
-        return ast.CallFunc(ast.Name('_lookup_attr'), [
-            ast.Name('data'), self.visit(node.expr, locals_=locals_),
-            ast.Const(node.attrname)
-        ])
-
-    def visitLambda(self, node, locals_=False):
-        node.code = self.visit(node.code, locals_=True)
-        node.filename = '<string>' # workaround for bug in pycodegen
-        return node
-
-    def visitListComp(self, node, locals_=False):
-        node.expr = self.visit(node.expr, locals_=True)
-        node.quals = [self.visit(qual, locals_=True) for qual in node.quals]
-        return node
-
-    def visitName(self, node, locals_=False):
-        func_args = [ast.Name('data'), ast.Const(node.name)]
-        if locals_:
-            func_args.append(ast.CallFunc(ast.Name('locals'), []))
-        return ast.CallFunc(ast.Name('_lookup_name'), func_args)
-
-    def visitSubscript(self, node, locals_=False):
-        return ast.CallFunc(ast.Name('_lookup_item'), [
-            ast.Name('data'), self.visit(node.expr, locals_=locals_),
-            ast.Tuple([self.visit(sub, locals_=locals_) for sub in node.subs])
-        ])
deleted file mode 100644
--- a/genshi/plugin.py
+++ /dev/null
@@ -1,156 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 Edgewall Software
-# Copyright (C) 2006 Matthew Good
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://genshi.edgewall.org/wiki/License.
-#
-# This software consists of voluntary contributions made by many
-# individuals. For the exact contribution history, see the revision
-# history and logs, available at http://genshi.edgewall.org/log/.
-
-"""Basic support for the template engine plugin API used by TurboGears and
-CherryPy/Buffet.
-"""
-
-from pkg_resources import resource_filename
-
-from genshi.eval import Undefined
-from genshi.input import ET, HTML, XML
-from genshi.output import DocType
-from genshi.template import Context, MarkupTemplate, Template, TemplateLoader, \
-                            TextTemplate
-
-
-class ConfigurationError(Exception):
-    """Exception raised when invalid plugin options are encountered."""
-
-
-class AbstractTemplateEnginePlugin(object):
-    """Implementation of the plugin API."""
-
-    template_class = None
-    extension = None
-
-    def __init__(self, extra_vars_func=None, options=None):
-        self.get_extra_vars = extra_vars_func
-        if options is None:
-            options = {}
-        self.options = options
-
-        self.default_encoding = options.get('genshi.default_encoding', 'utf-8')
-        auto_reload = options.get('genshi.auto_reload', '1').lower() \
-                            in ('1', 'yes', 'true')
-        search_path = options.get('genshi.search_path', '').split(':')
-        try:
-            max_cache_size = int(options.get('genshi.max_cache_size', 25))
-        except ValueError:
-            raise ConfigurationError('Invalid value for max_cache_size: "%s"' %
-                                     max_cache_size)
-
-        self.loader = TemplateLoader(filter(None, search_path),
-                                     auto_reload=auto_reload,
-                                     max_cache_size=max_cache_size)
-
-    def load_template(self, templatename, template_string=None):
-        """Find a template specified in python 'dot' notation, or load one from
-        a string.
-        """
-        if template_string is not None:
-            return self.template_class(template_string)
-
-        divider = templatename.rfind('.')
-        if divider >= 0:
-            package = templatename[:divider]
-            basename = templatename[divider + 1:] + self.extension
-            templatename = resource_filename(package, basename)
-
-        return self.loader.load(templatename, cls=self.template_class)
-
-    def _get_render_options(self, format=None):
-        if format is None:
-            format = self.default_format
-        kwargs = {'method': format}
-        if self.default_encoding:
-            kwargs['encoding'] = self.default_encoding
-        return kwargs
-
-    def render(self, info, format=None, fragment=False, template=None):
-        """Render the template to a string using the provided info."""
-        kwargs = self._get_render_options(format=format)
-        return self.transform(info, template).render(**kwargs)
-
-    def transform(self, info, template):
-        """Render the output to an event stream."""
-        if not isinstance(template, Template):
-            template = self.load_template(template)
-        ctxt = Context(**info)
-
-        # Some functions for Kid compatibility
-        def defined(name):
-            return ctxt.get(name, Undefined) is not Undefined
-        ctxt['defined'] = defined
-        def value_of(name, default=None):
-            return ctxt.get(name, default)
-        ctxt['value_of'] = value_of
-
-        return template.generate(ctxt)
-
-
-class MarkupTemplateEnginePlugin(AbstractTemplateEnginePlugin):
-    """Implementation of the plugin API for markup templates."""
-
-    template_class = MarkupTemplate
-    extension = '.html'
-
-    doctypes = {'html': DocType.HTML, 'html-strict': DocType.HTML_STRICT,
-                'html-transitional': DocType.HTML_TRANSITIONAL,
-                'xhtml': DocType.XHTML, 'xhtml-strict': DocType.XHTML_STRICT,
-                'xhtml-transitional': DocType.XHTML_TRANSITIONAL}
-
-    def __init__(self, extra_vars_func=None, options=None):
-        AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
-
-        doctype = options.get('genshi.default_doctype')
-        if doctype and doctype not in self.doctypes:
-            raise ConfigurationError('Unknown doctype "%s"' % doctype)
-        self.default_doctype = self.doctypes.get(doctype)
-
-        format = options.get('genshi.default_format', 'html')
-        if format not in ('html', 'xhtml', 'xml', 'text'):
-            raise ConfigurationError('Unknown output format "%s"' % format)
-        self.default_format = format
-
-    def _get_render_options(self, format=None):
-        kwargs = super(MarkupTemplateEnginePlugin,
-                       self)._get_render_options(format)
-        if self.default_doctype:
-            kwargs['doctype'] = self.default_doctype
-        return kwargs
-
-    def transform(self, info, template):
-        """Render the output to an event stream."""
-        data = {'ET': ET, 'HTML': HTML, 'XML': XML}
-        if self.get_extra_vars:
-            data.update(self.get_extra_vars())
-        data.update(info)
-        return super(MarkupTemplateEnginePlugin, self).transform(data, template)
-
-
-class TextTemplateEnginePlugin(AbstractTemplateEnginePlugin):
-    """Implementation of the plugin API for text templates."""
-
-    template_class = TextTemplate
-    extension = '.txt'
-    default_format = 'text'
-
-    def transform(self, info, template):
-        """Render the output to an event stream."""
-        data = {}
-        if self.get_extra_vars:
-            data.update(self.get_extra_vars())
-        data.update(info)
-        return super(TextTemplateEnginePlugin, self).transform(data, template)
rename from genshi/template.py
rename to genshi/template/__init__.py
--- a/genshi/template.py
+++ b/genshi/template/__init__.py
@@ -13,1396 +13,9 @@
 
 """Implementation of the template engine."""
 
-from itertools import chain
-try:
-    from collections import deque
-except ImportError:
-    class deque(list):
-        def appendleft(self, x): self.insert(0, x)
-        def popleft(self): return self.pop(0)
-import compiler
-import os
-import re
-from StringIO import StringIO
-try:
-    import threading
-except ImportError:
-    import dummy_threading as threading
-
-from genshi.core import Attrs, Namespace, Stream, StreamEventKind, _ensure
-from genshi.core import START, END, START_NS, END_NS, TEXT, COMMENT
-from genshi.eval import Expression, _parse
-from genshi.input import XMLParser
-from genshi.path import Path
-from genshi.util import LRUCache
-
-__all__ = ['BadDirectiveError', 'MarkupTemplate', 'Template', 'TemplateError',
-           'TemplateSyntaxError', 'TemplateNotFound', 'TemplateLoader',
-           'TextTemplate']
-
-
-class TemplateError(Exception):
-    """Base exception class for errors related to template processing."""
-
-
-class TemplateSyntaxError(TemplateError):
-    """Exception raised when an expression in a template causes a Python syntax
-    error."""
-
-    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
-        if isinstance(message, SyntaxError) and message.lineno is not None:
-            message = str(message).replace(' (line %d)' % message.lineno, '')
-        self.msg = message
-        message = '%s (%s, line %d)' % (self.msg, filename, lineno)
-        TemplateError.__init__(self, message)
-        self.filename = filename
-        self.lineno = lineno
-        self.offset = offset
-
-
-class BadDirectiveError(TemplateSyntaxError):
-    """Exception raised when an unknown directive is encountered when parsing
-    a template.
-    
-    An unknown directive is any attribute using the namespace for directives,
-    with a local name that doesn't match any registered directive.
-    """
-
-    def __init__(self, name, filename='<string>', lineno=-1):
-        message = 'bad directive "%s"' % name
-        TemplateSyntaxError.__init__(self, message, filename, lineno)
-
-
-class TemplateRuntimeError(TemplateError):
-    """Exception raised when an the evualation of a Python expression in a
-    template causes an error."""
-
-    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
-        self.msg = message
-        message = '%s (%s, line %d)' % (self.msg, filename, lineno)
-        TemplateError.__init__(self, message)
-        self.filename = filename
-        self.lineno = lineno
-        self.offset = offset
-
-
-class TemplateNotFound(TemplateError):
-    """Exception raised when a specific template file could not be found."""
-
-    def __init__(self, name, search_path):
-        TemplateError.__init__(self, 'Template "%s" not found' % name)
-        self.search_path = search_path
-
-
-class Context(object):
-    """Container for template input data.
-    
-    A context provides a stack of scopes (represented by dictionaries).
-    
-    Template directives such as loops can push a new scope on the stack with
-    data that should only be available inside the loop. When the loop
-    terminates, that scope can get popped off the stack again.
-    
-    >>> ctxt = Context(one='foo', other=1)
-    >>> ctxt.get('one')
-    'foo'
-    >>> ctxt.get('other')
-    1
-    >>> ctxt.push(dict(one='frost'))
-    >>> ctxt.get('one')
-    'frost'
-    >>> ctxt.get('other')
-    1
-    >>> ctxt.pop()
-    {'one': 'frost'}
-    >>> ctxt.get('one')
-    'foo'
-    """
-
-    def __init__(self, **data):
-        self.frames = deque([data])
-        self.pop = self.frames.popleft
-        self.push = self.frames.appendleft
-        self._match_templates = []
-
-    def __repr__(self):
-        return repr(list(self.frames))
-
-    def __setitem__(self, key, value):
-        """Set a variable in the current scope."""
-        self.frames[0][key] = value
-
-    def _find(self, key, default=None):
-        """Retrieve a given variable's value and the frame it was found in.
-
-        Intented for internal use by directives.
-        """
-        for frame in self.frames:
-            if key in frame:
-                return frame[key], frame
-        return default, None
-
-    def get(self, key, default=None):
-        """Get a variable's value, starting at the current scope and going
-        upward.
-        """
-        for frame in self.frames:
-            if key in frame:
-                return frame[key]
-        return default
-    __getitem__ = get
-
-    def push(self, data):
-        """Push a new scope on the stack."""
-
-    def pop(self):
-        """Pop the top-most scope from the stack."""
-
-
-class Directive(object):
-    """Abstract base class for template directives.
-    
-    A directive is basically a callable that takes three positional arguments:
-    `ctxt` is the template data context, `stream` is an iterable over the
-    events that the directive applies to, and `directives` is is a list of
-    other directives on the same stream that need to be applied.
-    
-    Directives can be "anonymous" or "registered". Registered directives can be
-    applied by the template author using an XML attribute with the
-    corresponding name in the template. Such directives should be subclasses of
-    this base class that can  be instantiated with the value of the directive
-    attribute as parameter.
-    
-    Anonymous directives are simply functions conforming to the protocol
-    described above, and can only be applied programmatically (for example by
-    template filters).
-    """
-    __slots__ = ['expr']
-
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        try:
-            self.expr = value and Expression(value, filename, lineno) or None
-        except SyntaxError, err:
-            err.msg += ' in expression "%s" of "%s" directive' % (value,
-                                                                  self.tagname)
-            raise TemplateSyntaxError(err, filename, lineno,
-                                      offset + (err.offset or 0))
-
-    def __call__(self, stream, ctxt, directives):
-        raise NotImplementedError
-
-    def __repr__(self):
-        expr = ''
-        if self.expr is not None:
-            expr = ' "%s"' % self.expr.source
-        return '<%s%s>' % (self.__class__.__name__, expr)
-
-    def tagname(self):
-        """Return the local tag name of the directive as it is used in
-        templates.
-        """
-        return self.__class__.__name__.lower().replace('directive', '')
-    tagname = property(tagname)
-
-
-def _apply_directives(stream, ctxt, directives):
-    """Apply the given directives to the stream."""
-    if directives:
-        stream = directives[0](iter(stream), ctxt, directives[1:])
-    return stream
-
-def _assignment(ast):
-    """Takes the AST representation of an assignment, and returns a function
-    that applies the assignment of a given value to a dictionary.
-    """
-    def _names(node):
-        if isinstance(node, (compiler.ast.AssTuple, compiler.ast.Tuple)):
-            return tuple([_names(child) for child in node.nodes])
-        elif isinstance(node, (compiler.ast.AssName, compiler.ast.Name)):
-            return node.name
-    def _assign(data, value, names=_names(ast)):
-        if type(names) is tuple:
-            for idx in range(len(names)):
-                _assign(data, value[idx], names[idx])
-        else:
-            data[names] = value
-    return _assign
-
-
-class AttrsDirective(Directive):
-    """Implementation of the `py:attrs` template directive.
-    
-    The value of the `py:attrs` attribute should be a dictionary or a sequence
-    of `(name, value)` tuples. The items in that dictionary or sequence are
-    added as attributes to the element:
-    
-    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
-    ...   <li py:attrs="foo">Bar</li>
-    ... </ul>''')
-    >>> print tmpl.generate(foo={'class': 'collapse'})
-    <ul>
-      <li class="collapse">Bar</li>
-    </ul>
-    >>> print tmpl.generate(foo=[('class', 'collapse')])
-    <ul>
-      <li class="collapse">Bar</li>
-    </ul>
-    
-    If the value evaluates to `None` (or any other non-truth value), no
-    attributes are added:
-    
-    >>> print tmpl.generate(foo=None)
-    <ul>
-      <li>Bar</li>
-    </ul>
-    """
-    __slots__ = []
-
-    def __call__(self, stream, ctxt, directives):
-        def _generate():
-            kind, (tag, attrib), pos  = stream.next()
-            attrs = self.expr.evaluate(ctxt)
-            if attrs:
-                attrib = Attrs(attrib[:])
-                if isinstance(attrs, Stream):
-                    try:
-                        attrs = iter(attrs).next()
-                    except StopIteration:
-                        attrs = []
-                elif not isinstance(attrs, list): # assume it's a dict
-                    attrs = attrs.items()
-                for name, value in attrs:
-                    if value is None:
-                        attrib.remove(name)
-                    else:
-                        attrib.set(name, unicode(value).strip())
-            yield kind, (tag, attrib), pos
-            for event in stream:
-                yield event
-
-        return _apply_directives(_generate(), ctxt, directives)
-
-
-class ContentDirective(Directive):
-    """Implementation of the `py:content` template directive.
-    
-    This directive replaces the content of the element with the result of
-    evaluating the value of the `py:content` attribute:
-    
-    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
-    ...   <li py:content="bar">Hello</li>
-    ... </ul>''')
-    >>> print tmpl.generate(bar='Bye')
-    <ul>
-      <li>Bye</li>
-    </ul>
-    """
-    __slots__ = []
-
-    def __call__(self, stream, ctxt, directives):
-        def _generate():
-            yield stream.next()
-            yield EXPR, self.expr, (None, -1, -1)
-            event = stream.next()
-            for next in stream:
-                event = next
-            yield event
-
-        return _apply_directives(_generate(), ctxt, directives)
-
-
-class DefDirective(Directive):
-    """Implementation of the `py:def` template directive.
-    
-    This directive can be used to create "Named Template Functions", which
-    are template snippets that are not actually output during normal
-    processing, but rather can be expanded from expressions in other places
-    in the template.
-    
-    A named template function can be used just like a normal Python function
-    from template expressions:
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
-    ...   <p py:def="echo(greeting, name='world')" class="message">
-    ...     ${greeting}, ${name}!
-    ...   </p>
-    ...   ${echo('Hi', name='you')}
-    ... </div>''')
-    >>> print tmpl.generate(bar='Bye')
-    <div>
-      <p class="message">
-        Hi, you!
-      </p>
-    </div>
-    
-    If a function does not require parameters, the parenthesis can be omitted
-    both when defining and when calling it:
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
-    ...   <p py:def="helloworld" class="message">
-    ...     Hello, world!
-    ...   </p>
-    ...   ${helloworld}
-    ... </div>''')
-    >>> print tmpl.generate(bar='Bye')
-    <div>
-      <p class="message">
-        Hello, world!
-      </p>
-    </div>
-    """
-    __slots__ = ['name', 'args', 'defaults']
-
-    ATTRIBUTE = 'function'
-
-    def __init__(self, args, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
-        ast = _parse(args).node
-        self.args = []
-        self.defaults = {}
-        if isinstance(ast, compiler.ast.CallFunc):
-            self.name = ast.node.name
-            for arg in ast.args:
-                if isinstance(arg, compiler.ast.Keyword):
-                    self.args.append(arg.name)
-                    self.defaults[arg.name] = Expression(arg.expr, filename,
-                                                         lineno)
-                else:
-                    self.args.append(arg.name)
-        else:
-            self.name = ast.name
-
-    def __call__(self, stream, ctxt, directives):
-        stream = list(stream)
-
-        def function(*args, **kwargs):
-            scope = {}
-            args = list(args) # make mutable
-            for name in self.args:
-                if args:
-                    scope[name] = args.pop(0)
-                else:
-                    if name in kwargs:
-                        val = kwargs.pop(name)
-                    else:
-                        val = self.defaults.get(name).evaluate(ctxt)
-                    scope[name] = val
-            ctxt.push(scope)
-            for event in _apply_directives(stream, ctxt, directives):
-                yield event
-            ctxt.pop()
-        try:
-            function.__name__ = self.name
-        except TypeError:
-            # Function name can't be set in Python 2.3 
-            pass
-
-        # Store the function reference in the bottom context frame so that it
-        # doesn't get popped off before processing the template has finished
-        # FIXME: this makes context data mutable as a side-effect
-        ctxt.frames[-1][self.name] = function
-
-        return []
-
-    def __repr__(self):
-        return '<%s "%s">' % (self.__class__.__name__, self.name)
-
-
-class ForDirective(Directive):
-    """Implementation of the `py:for` template directive for repeating an
-    element based on an iterable in the context data.
-    
-    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
-    ...   <li py:for="item in items">${item}</li>
-    ... </ul>''')
-    >>> print tmpl.generate(items=[1, 2, 3])
-    <ul>
-      <li>1</li><li>2</li><li>3</li>
-    </ul>
-    """
-    __slots__ = ['assign', 'filename']
-
-    ATTRIBUTE = 'each'
-
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        if ' in ' not in value:
-            raise TemplateSyntaxError('"in" keyword missing in "for" directive',
-                                      filename, lineno, offset)
-        assign, value = value.split(' in ', 1)
-        ast = _parse(assign, 'exec')
-        self.assign = _assignment(ast.node.nodes[0].expr)
-        self.filename = filename
-        Directive.__init__(self, value.strip(), namespaces, filename, lineno,
-                           offset)
-
-    def __call__(self, stream, ctxt, directives):
-        iterable = self.expr.evaluate(ctxt)
-        if iterable is None:
-            return
-
-        assign = self.assign
-        scope = {}
-        stream = list(stream)
-        try:
-            iterator = iter(iterable)
-            for item in iterator:
-                assign(scope, item)
-                ctxt.push(scope)
-                for event in _apply_directives(stream, ctxt, directives):
-                    yield event
-                ctxt.pop()
-        except TypeError, e:
-            raise TemplateRuntimeError(str(e), self.filename, *stream[0][2][1:])
-
-    def __repr__(self):
-        return '<%s>' % self.__class__.__name__
-
-
-class IfDirective(Directive):
-    """Implementation of the `py:if` template directive for conditionally
-    excluding elements from being output.
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
-    ...   <b py:if="foo">${bar}</b>
-    ... </div>''')
-    >>> print tmpl.generate(foo=True, bar='Hello')
-    <div>
-      <b>Hello</b>
-    </div>
-    """
-    __slots__ = []
-
-    ATTRIBUTE = 'test'
-
-    def __call__(self, stream, ctxt, directives):
-        if self.expr.evaluate(ctxt):
-            return _apply_directives(stream, ctxt, directives)
-        return []
-
-
-class MatchDirective(Directive):
-    """Implementation of the `py:match` template directive.
-
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
-    ...   <span py:match="greeting">
-    ...     Hello ${select('@name')}
-    ...   </span>
-    ...   <greeting name="Dude" />
-    ... </div>''')
-    >>> print tmpl.generate()
-    <div>
-      <span>
-        Hello Dude
-      </span>
-    </div>
-    """
-    __slots__ = ['path', 'namespaces']
-
-    ATTRIBUTE = 'path'
-
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
-        self.path = Path(value, filename, lineno)
-        if namespaces is None:
-            namespaces = {}
-        self.namespaces = namespaces.copy()
-
-    def __call__(self, stream, ctxt, directives):
-        ctxt._match_templates.append((self.path.test(ignore_context=True),
-                                      self.path, list(stream), self.namespaces,
-                                      directives))
-        return []
-
-    def __repr__(self):
-        return '<%s "%s">' % (self.__class__.__name__, self.path.source)
-
-
-class ReplaceDirective(Directive):
-    """Implementation of the `py:replace` template directive.
-    
-    This directive replaces the element with the result of evaluating the
-    value of the `py:replace` attribute:
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
-    ...   <span py:replace="bar">Hello</span>
-    ... </div>''')
-    >>> print tmpl.generate(bar='Bye')
-    <div>
-      Bye
-    </div>
-    
-    This directive is equivalent to `py:content` combined with `py:strip`,
-    providing a less verbose way to achieve the same effect:
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
-    ...   <span py:content="bar" py:strip="">Hello</span>
-    ... </div>''')
-    >>> print tmpl.generate(bar='Bye')
-    <div>
-      Bye
-    </div>
-    """
-    __slots__ = []
-
-    def __call__(self, stream, ctxt, directives):
-        yield EXPR, self.expr, (None, -1, -1)
-
-
-class StripDirective(Directive):
-    """Implementation of the `py:strip` template directive.
-    
-    When the value of the `py:strip` attribute evaluates to `True`, the element
-    is stripped from the output
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
-    ...   <div py:strip="True"><b>foo</b></div>
-    ... </div>''')
-    >>> print tmpl.generate()
-    <div>
-      <b>foo</b>
-    </div>
-    
-    Leaving the attribute value empty is equivalent to a truth value.
-    
-    This directive is particulary interesting for named template functions or
-    match templates that do not generate a top-level element:
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
-    ...   <div py:def="echo(what)" py:strip="">
-    ...     <b>${what}</b>
-    ...   </div>
-    ...   ${echo('foo')}
-    ... </div>''')
-    >>> print tmpl.generate()
-    <div>
-        <b>foo</b>
-    </div>
-    """
-    __slots__ = []
-
-    def __call__(self, stream, ctxt, directives):
-        def _generate():
-            if self.expr:
-                strip = self.expr.evaluate(ctxt)
-            else:
-                strip = True
-            if strip:
-                stream.next() # skip start tag
-                previous = stream.next()
-                for event in stream:
-                    yield previous
-                    previous = event
-            else:
-                for event in stream:
-                    yield event
-
-        return _apply_directives(_generate(), ctxt, directives)
-
-
-class ChooseDirective(Directive):
-    """Implementation of the `py:choose` directive for conditionally selecting
-    one of several body elements to display.
-    
-    If the `py:choose` expression is empty the expressions of nested `py:when`
-    directives are tested for truth.  The first true `py:when` body is output.
-    If no `py:when` directive is matched then the fallback directive
-    `py:otherwise` will be used.
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"
-    ...   py:choose="">
-    ...   <span py:when="0 == 1">0</span>
-    ...   <span py:when="1 == 1">1</span>
-    ...   <span py:otherwise="">2</span>
-    ... </div>''')
-    >>> print tmpl.generate()
-    <div>
-      <span>1</span>
-    </div>
-    
-    If the `py:choose` directive contains an expression, the nested `py:when`
-    directives are tested for equality to the `py:choose` expression:
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"
-    ...   py:choose="2">
-    ...   <span py:when="1">1</span>
-    ...   <span py:when="2">2</span>
-    ... </div>''')
-    >>> print tmpl.generate()
-    <div>
-      <span>2</span>
-    </div>
-    
-    Behavior is undefined if a `py:choose` block contains content outside a
-    `py:when` or `py:otherwise` block.  Behavior is also undefined if a
-    `py:otherwise` occurs before `py:when` blocks.
-    """
-    __slots__ = ['matched', 'value']
-
-    ATTRIBUTE = 'test'
-
-    def __call__(self, stream, ctxt, directives):
-        frame = dict({'_choose.matched': False})
-        if self.expr:
-            frame['_choose.value'] = self.expr.evaluate(ctxt)
-        ctxt.push(frame)
-        for event in _apply_directives(stream, ctxt, directives):
-            yield event
-        ctxt.pop()
-
-
-class WhenDirective(Directive):
-    """Implementation of the `py:when` directive for nesting in a parent with
-    the `py:choose` directive.
-    
-    See the documentation of `py:choose` for usage.
-    """
-    __slots__ = ['filename']
-
-    ATTRIBUTE = 'test'
-
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, value, namespaces, filename, lineno, offset)
-        self.filename = filename
-
-    def __call__(self, stream, ctxt, directives):
-        matched, frame = ctxt._find('_choose.matched')
-        if not frame:
-            raise TemplateRuntimeError('"when" directives can only be used '
-                                       'inside a "choose" directive',
-                                       self.filename, *stream.next()[2][1:])
-        if matched:
-            return []
-        if not self.expr and '_choose.value' not in frame:
-            raise TemplateRuntimeError('either "choose" or "when" directive '
-                                       'must have a test expression',
-                                       self.filename, *stream.next()[2][1:])
-        if '_choose.value' in frame:
-            value = frame['_choose.value']
-            if self.expr:
-                matched = value == self.expr.evaluate(ctxt)
-            else:
-                matched = bool(value)
-        else:
-            matched = bool(self.expr.evaluate(ctxt))
-        frame['_choose.matched'] = matched
-        if not matched:
-            return []
-
-        return _apply_directives(stream, ctxt, directives)
-
-
-class OtherwiseDirective(Directive):
-    """Implementation of the `py:otherwise` directive for nesting in a parent
-    with the `py:choose` directive.
-    
-    See the documentation of `py:choose` for usage.
-    """
-    __slots__ = ['filename']
-
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
-        self.filename = filename
-
-    def __call__(self, stream, ctxt, directives):
-        matched, frame = ctxt._find('_choose.matched')
-        if not frame:
-            raise TemplateRuntimeError('an "otherwise" directive can only be '
-                                       'used inside a "choose" directive',
-                                       self.filename, *stream.next()[2][1:])
-        if matched:
-            return []
-        frame['_choose.matched'] = True
-
-        return _apply_directives(stream, ctxt, directives)
-
-
-class WithDirective(Directive):
-    """Implementation of the `py:with` template directive, which allows
-    shorthand access to variables and expressions.
-    
-    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
-    ...   <span py:with="y=7; z=x+10">$x $y $z</span>
-    ... </div>''')
-    >>> print tmpl.generate(x=42)
-    <div>
-      <span>42 7 52</span>
-    </div>
-    """
-    __slots__ = ['vars']
-
-    ATTRIBUTE = 'vars'
-
-    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
-                 offset=-1):
-        Directive.__init__(self, None, namespaces, filename, lineno, offset)
-        self.vars = []
-        value = value.strip()
-        try:
-            ast = _parse(value, 'exec').node
-            for node in ast.nodes:
-                if isinstance(node, compiler.ast.Discard):
-                    continue
-                elif not isinstance(node, compiler.ast.Assign):
-                    raise TemplateSyntaxError('only assignment allowed in '
-                                              'value of the "with" directive',
-                                              filename, lineno, offset)
-                self.vars.append(([_assignment(n) for n in node.nodes],
-                                  Expression(node.expr, filename, lineno)))
-        except SyntaxError, err:
-            err.msg += ' in expression "%s" of "%s" directive' % (value,
-                                                                  self.tagname)
-            raise TemplateSyntaxError(err, filename, lineno,
-                                      offset + (err.offset or 0))
-
-    def __call__(self, stream, ctxt, directives):
-        frame = {}
-        ctxt.push(frame)
-        for targets, expr in self.vars:
-            value = expr.evaluate(ctxt, nocall=True)
-            for assign in targets:
-                assign(frame, value)
-        for event in _apply_directives(stream, ctxt, directives):
-            yield event
-        ctxt.pop()
-
-    def __repr__(self):
-        return '<%s>' % (self.__class__.__name__)
-
-
-class TemplateMeta(type):
-    """Meta class for templates."""
-
-    def __new__(cls, name, bases, d):
-        if 'directives' in d:
-            d['_dir_by_name'] = dict(d['directives'])
-            d['_dir_order'] = [directive[1] for directive in d['directives']]
-
-        return type.__new__(cls, name, bases, d)
-
-
-class Template(object):
-    """Abstract template base class.
-    
-    This class implements most of the template processing model, but does not
-    specify the syntax of templates.
-    """
-    __metaclass__ = TemplateMeta
-
-    EXPR = StreamEventKind('EXPR') # an expression
-    SUB = StreamEventKind('SUB') # a "subprogram"
-
-    def __init__(self, source, basedir=None, filename=None, loader=None,
-                 encoding=None):
-        """Initialize a template from either a string or a file-like object."""
-        if isinstance(source, basestring):
-            self.source = StringIO(source)
-        else:
-            self.source = source
-        self.basedir = basedir
-        self.filename = filename
-        if basedir and filename:
-            self.filepath = os.path.join(basedir, filename)
-        else:
-            self.filepath = filename
-
-        self.filters = [self._flatten, self._eval]
-
-        self.stream = self._parse(encoding)
-
-    def __repr__(self):
-        return '<%s "%s">' % (self.__class__.__name__, self.filename)
-
-    def _parse(self, encoding):
-        """Parse the template.
-        
-        The parsing stage parses the template and constructs a list of
-        directives that will be executed in the render stage. The input is
-        split up into literal output (text that does not depend on the context
-        data) and directives or expressions.
-        """
-        raise NotImplementedError
-
-    _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}', re.DOTALL)
-    _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z_][a-zA-Z0-9_\.]*)')
-
-    def _interpolate(cls, text, basedir=None, filename=None, lineno=-1,
-                     offset=0):
-        """Parse the given string and extract expressions.
-        
-        This method returns a list containing both literal text and `Expression`
-        objects.
-        
-        @param text: the text to parse
-        @param lineno: the line number at which the text was found (optional)
-        @param offset: the column number at which the text starts in the source
-            (optional)
-        """
-        filepath = filename
-        if filepath and basedir:
-            filepath = os.path.join(basedir, filepath)
-        def _interpolate(text, patterns, lineno=lineno, offset=offset):
-            for idx, grp in enumerate(patterns.pop(0).split(text)):
-                if idx % 2:
-                    try:
-                        yield EXPR, Expression(grp.strip(), filepath, lineno), \
-                              (filename, lineno, offset)
-                    except SyntaxError, err:
-                        raise TemplateSyntaxError(err, filepath, lineno,
-                                                  offset + (err.offset or 0))
-                elif grp:
-                    if patterns:
-                        for result in _interpolate(grp, patterns[:]):
-                            yield result
-                    else:
-                        yield TEXT, grp.replace('$$', '$'), \
-                              (filename, lineno, offset)
-                if '\n' in grp:
-                    lines = grp.splitlines()
-                    lineno += len(lines) - 1
-                    offset += len(lines[-1])
-                else:
-                    offset += len(grp)
-        return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE])
-    _interpolate = classmethod(_interpolate)
-
-    def generate(self, *args, **kwargs):
-        """Apply the template to the given context data.
-        
-        Any keyword arguments are made available to the template as context
-        data.
-        
-        Only one positional argument is accepted: if it is provided, it must be
-        an instance of the `Context` class, and keyword arguments are ignored.
-        This calling style is used for internal processing.
-        
-        @return: a markup event stream representing the result of applying
-            the template to the context data.
-        """
-        if args:
-            assert len(args) == 1
-            ctxt = args[0]
-            if ctxt is None:
-                ctxt = Context(**kwargs)
-            assert isinstance(ctxt, Context)
-        else:
-            ctxt = Context(**kwargs)
-
-        stream = self.stream
-        for filter_ in self.filters:
-            stream = filter_(iter(stream), ctxt)
-        return Stream(stream)
-
-    def _eval(self, stream, ctxt):
-        """Internal stream filter that evaluates any expressions in `START` and
-        `TEXT` events.
-        """
-        filters = (self._flatten, self._eval)
-
-        for kind, data, pos in stream:
-
-            if kind is START and data[1]:
-                # Attributes may still contain expressions in start tags at
-                # this point, so do some evaluation
-                tag, attrib = data
-                new_attrib = []
-                for name, substream in attrib:
-                    if isinstance(substream, basestring):
-                        value = substream
-                    else:
-                        values = []
-                        for subkind, subdata, subpos in self._eval(substream,
-                                                                   ctxt):
-                            if subkind is TEXT:
-                                values.append(subdata)
-                        value = [x for x in values if x is not None]
-                        if not value:
-                            continue
-                    new_attrib.append((name, u''.join(value)))
-                yield kind, (tag, Attrs(new_attrib)), pos
-
-            elif kind is EXPR:
-                result = data.evaluate(ctxt)
-                if result is not None:
-                    # First check for a string, otherwise the iterable test below
-                    # succeeds, and the string will be chopped up into individual
-                    # characters
-                    if isinstance(result, basestring):
-                        yield TEXT, result, pos
-                    elif hasattr(result, '__iter__'):
-                        substream = _ensure(result)
-                        for filter_ in filters:
-                            substream = filter_(substream, ctxt)
-                        for event in substream:
-                            yield event
-                    else:
-                        yield TEXT, unicode(result), pos
-
-            else:
-                yield kind, data, pos
-
-    def _flatten(self, stream, ctxt):
-        """Internal stream filter that expands `SUB` events in the stream."""
-        for event in stream:
-            if event[0] is SUB:
-                # This event is a list of directives and a list of nested
-                # events to which those directives should be applied
-                directives, substream = event[1]
-                substream = _apply_directives(substream, ctxt, directives)
-                for event in self._flatten(substream, ctxt):
-                    yield event
-            else:
-                yield event
-
-
-EXPR = Template.EXPR
-SUB = Template.SUB
-
-
-class MarkupTemplate(Template):
-    """Implementation of the template language for XML-based templates.
-    
-    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
-    ...   <li py:for="item in items">${item}</li>
-    ... </ul>''')
-    >>> print tmpl.generate(items=[1, 2, 3])
-    <ul>
-      <li>1</li><li>2</li><li>3</li>
-    </ul>
-    """
-    NAMESPACE = Namespace('http://genshi.edgewall.org/')
-
-    directives = [('def', DefDirective),
-                  ('match', MatchDirective),
-                  ('when', WhenDirective),
-                  ('otherwise', OtherwiseDirective),
-                  ('for', ForDirective),
-                  ('if', IfDirective),
-                  ('choose', ChooseDirective),
-                  ('with', WithDirective),
-                  ('replace', ReplaceDirective),
-                  ('content', ContentDirective),
-                  ('attrs', AttrsDirective),
-                  ('strip', StripDirective)]
-
-    def __init__(self, source, basedir=None, filename=None, loader=None,
-                 encoding=None):
-        """Initialize a template from either a string or a file-like object."""
-        Template.__init__(self, source, basedir=basedir, filename=filename,
-                          loader=loader, encoding=encoding)
-
-        self.filters.append(self._match)
-        if loader:
-            from genshi.filters import IncludeFilter
-            self.filters.append(IncludeFilter(loader))
-
-    def _parse(self, encoding):
-        """Parse the template from an XML document."""
-        stream = [] # list of events of the "compiled" template
-        dirmap = {} # temporary mapping of directives to elements
-        ns_prefix = {}
-        depth = 0
-
-        for kind, data, pos in XMLParser(self.source, filename=self.filename,
-                                         encoding=encoding):
-
-            if kind is START_NS:
-                # Strip out the namespace declaration for template directives
-                prefix, uri = data
-                ns_prefix[prefix] = uri
-                if uri != self.NAMESPACE:
-                    stream.append((kind, data, pos))
-
-            elif kind is END_NS:
-                uri = ns_prefix.pop(data, None)
-                if uri and uri != self.NAMESPACE:
-                    stream.append((kind, data, pos))
-
-            elif kind is START:
-                # Record any directive attributes in start tags
-                tag, attrib = data
-                directives = []
-                strip = False
-
-                if tag in self.NAMESPACE:
-                    cls = self._dir_by_name.get(tag.localname)
-                    if cls is None:
-                        raise BadDirectiveError(tag.localname, self.filepath,
-                                                pos[1])
-                    value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '')
-                    directives.append(cls(value, ns_prefix, self.filepath,
-                                          pos[1], pos[2]))
-                    strip = True
-
-                new_attrib = []
-                for name, value in attrib:
-                    if name in self.NAMESPACE:
-                        cls = self._dir_by_name.get(name.localname)
-                        if cls is None:
-                            raise BadDirectiveError(name.localname,
-                                                    self.filepath, pos[1])
-                        directives.append(cls(value, ns_prefix, self.filepath,
-                                              pos[1], pos[2]))
-                    else:
-                        if value:
-                            value = list(self._interpolate(value, self.basedir,
-                                                           *pos))
-                            if len(value) == 1 and value[0][0] is TEXT:
-                                value = value[0][1]
-                        else:
-                            value = [(TEXT, u'', pos)]
-                        new_attrib.append((name, value))
-
-                if directives:
-                    index = self._dir_order.index
-                    directives.sort(lambda a, b: cmp(index(a.__class__),
-                                                     index(b.__class__)))
-                    dirmap[(depth, tag)] = (directives, len(stream), strip)
-
-                stream.append((kind, (tag, Attrs(new_attrib)), pos))
-                depth += 1
-
-            elif kind is END:
-                depth -= 1
-                stream.append((kind, data, pos))
-
-                # If there have have directive attributes with the corresponding
-                # start tag, move the events inbetween into a "subprogram"
-                if (depth, data) in dirmap:
-                    directives, start_offset, strip = dirmap.pop((depth, data))
-                    substream = stream[start_offset:]
-                    if strip:
-                        substream = substream[1:-1]
-                    stream[start_offset:] = [(SUB, (directives, substream),
-                                              pos)]
-
-            elif kind is TEXT:
-                for kind, data, pos in self._interpolate(data, self.basedir,
-                                                         *pos):
-                    stream.append((kind, data, pos))
-
-            elif kind is COMMENT:
-                if not data.lstrip().startswith('!'):
-                    stream.append((kind, data, pos))
-
-            else:
-                stream.append((kind, data, pos))
-
-        return stream
-
-    def _match(self, stream, ctxt, match_templates=None):
-        """Internal stream filter that applies any defined match templates
-        to the stream.
-        """
-        if match_templates is None:
-            match_templates = ctxt._match_templates
-
-        tail = []
-        def _strip(stream):
-            depth = 1
-            while 1:
-                event = stream.next()
-                if event[0] is START:
-                    depth += 1
-                elif event[0] is END:
-                    depth -= 1
-                if depth > 0:
-                    yield event
-                else:
-                    tail[:] = [event]
-                    break
-
-        for event in stream:
-
-            # We (currently) only care about start and end events for matching
-            # We might care about namespace events in the future, though
-            if not match_templates or (event[0] is not START and
-                                       event[0] is not END):
-                yield event
-                continue
-
-            for idx, (test, path, template, namespaces, directives) in \
-                    enumerate(match_templates):
-
-                if test(event, namespaces, ctxt) is True:
-
-                    # Let the remaining match templates know about the event so
-                    # they get a chance to update their internal state
-                    for test in [mt[0] for mt in match_templates[idx + 1:]]:
-                        test(event, namespaces, ctxt, updateonly=True)
-
-                    # Consume and store all events until an end event
-                    # corresponding to this start event is encountered
-                    content = chain([event], self._match(_strip(stream), ctxt),
-                                    tail)
-                    for filter_ in self.filters[3:]:
-                        content = filter_(content, ctxt)
-                    content = list(content)
-
-                    for test in [mt[0] for mt in match_templates]:
-                        test(tail[0], namespaces, ctxt, updateonly=True)
-
-                    # Make the select() function available in the body of the
-                    # match template
-                    def select(path):
-                        return Stream(content).select(path, namespaces, ctxt)
-                    ctxt.push(dict(select=select))
-
-                    # Recursively process the output
-                    template = _apply_directives(template, ctxt, directives)
-                    for event in self._match(self._eval(self._flatten(template,
-                                                                      ctxt),
-                                                        ctxt), ctxt,
-                                             match_templates[:idx] +
-                                             match_templates[idx + 1:]):
-                        yield event
-
-                    ctxt.pop()
-                    break
-
-            else: # no matches
-                yield event
-
-
-class TextTemplate(Template):
-    """Implementation of a simple text-based template engine.
-    
-    >>> tmpl = TextTemplate('''Dear $name,
-    ... 
-    ... We have the following items for you:
-    ... #for item in items
-    ...  * $item
-    ... #end
-    ... 
-    ... All the best,
-    ... Foobar''')
-    >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
-    Dear Joe,
-    <BLANKLINE>
-    We have the following items for you:
-     * 1
-     * 2
-     * 3
-    <BLANKLINE>
-    All the best,
-    Foobar
-    """
-    directives = [('def', DefDirective),
-                  ('when', WhenDirective),
-                  ('otherwise', OtherwiseDirective),
-                  ('for', ForDirective),
-                  ('if', IfDirective),
-                  ('choose', ChooseDirective),
-                  ('with', WithDirective)]
-
-    _DIRECTIVE_RE = re.compile(r'^\s*(?<!\\)#((?:\w+|#).*)\n?', re.MULTILINE)
-
-    def _parse(self, encoding):
-        """Parse the template from text input."""
-        stream = [] # list of events of the "compiled" template
-        dirmap = {} # temporary mapping of directives to elements
-        depth = 0
-        if not encoding:
-            encoding = 'utf-8'
-
-        source = self.source.read().decode(encoding, 'replace')
-        offset = 0
-        lineno = 1
-
-        for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)):
-            start, end = mo.span()
-            if start > offset:
-                text = source[offset:start]
-                for kind, data, pos in self._interpolate(text, self.basedir,
-                                                         self.filename, lineno):
-                    stream.append((kind, data, pos))
-                lineno += len(text.splitlines())
-
-            text = source[start:end].lstrip()[1:]
-            lineno += len(text.splitlines())
-            directive = text.split(None, 1)
-            if len(directive) > 1:
-                command, value = directive
-            else:
-                command, value = directive[0], None
-
-            if command == 'end':
-                depth -= 1
-                if depth in dirmap:
-                    directive, start_offset = dirmap.pop(depth)
-                    substream = stream[start_offset:]
-                    stream[start_offset:] = [(SUB, ([directive], substream),
-                                              (self.filepath, lineno, 0))]
-            elif command != '#':
-                cls = self._dir_by_name.get(command)
-                if cls is None:
-                    raise BadDirectiveError(command)
-                directive = cls(value, None, self.filepath, lineno, 0)
-                dirmap[depth] = (directive, len(stream))
-                depth += 1
-
-            offset = end
-
-        if offset < len(source):
-            text = source[offset:].replace('\\#', '#')
-            for kind, data, pos in self._interpolate(text, self.basedir,
-                                                     self.filename, lineno):
-                stream.append((kind, data, pos))
-
-        return stream
-
-
-class TemplateLoader(object):
-    """Responsible for loading templates from files on the specified search
-    path.
-    
-    >>> import tempfile
-    >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
-    >>> os.write(fd, '<p>$var</p>')
-    11
-    >>> os.close(fd)
-    
-    The template loader accepts a list of directory paths that are then used
-    when searching for template files, in the given order:
-    
-    >>> loader = TemplateLoader([os.path.dirname(path)])
-    
-    The `load()` method first checks the template cache whether the requested
-    template has already been loaded. If not, it attempts to locate the
-    template file, and returns the corresponding `Template` object:
-    
-    >>> template = loader.load(os.path.basename(path))
-    >>> isinstance(template, MarkupTemplate)
-    True
-    
-    Template instances are cached: requesting a template with the same name
-    results in the same instance being returned:
-    
-    >>> loader.load(os.path.basename(path)) is template
-    True
-    
-    >>> os.remove(path)
-    """
-    def __init__(self, search_path=None, auto_reload=False,
-                 default_encoding=None, max_cache_size=25):
-        """Create the template laoder.
-        
-        @param search_path: a list of absolute path names that should be
-            searched for template files, or a string containing a single
-            absolute path
-        @param auto_reload: whether to check the last modification time of
-            template files, and reload them if they have changed
-        @param default_encoding: the default encoding to assume when loading
-            templates; defaults to UTF-8
-        @param max_cache_size: the maximum number of templates to keep in the
-            cache
-        """
-        self.search_path = search_path
-        if self.search_path is None:
-            self.search_path = []
-        elif isinstance(self.search_path, basestring):
-            self.search_path = [self.search_path]
-        self.auto_reload = auto_reload
-        self.default_encoding = default_encoding
-        self._cache = LRUCache(max_cache_size)
-        self._mtime = {}
-        self._lock = threading.Lock()
-
-    def load(self, filename, relative_to=None, cls=MarkupTemplate,
-             encoding=None):
-        """Load the template with the given name.
-        
-        If the `filename` parameter is relative, this method searches the search
-        path trying to locate a template matching the given name. If the file
-        name is an absolute path, the search path is not bypassed.
-        
-        If requested template is not found, a `TemplateNotFound` exception is
-        raised. Otherwise, a `Template` object is returned that represents the
-        parsed template.
-        
-        Template instances are cached to avoid having to parse the same
-        template file more than once. Thus, subsequent calls of this method
-        with the same template file name will return the same `Template`
-        object (unless the `auto_reload` option is enabled and the file was
-        changed since the last parse.)
-        
-        If the `relative_to` parameter is provided, the `filename` is
-        interpreted as being relative to that path.
-        
-        @param filename: the relative path of the template file to load
-        @param relative_to: the filename of the template from which the new
-            template is being loaded, or `None` if the template is being loaded
-            directly
-        @param cls: the class of the template object to instantiate
-        @param encoding: the encoding of the template to load; defaults to the
-            `default_encoding` of the loader instance
-        """
-        if encoding is None:
-            encoding = self.default_encoding
-        if relative_to and not os.path.isabs(relative_to):
-            filename = os.path.join(os.path.dirname(relative_to), filename)
-        filename = os.path.normpath(filename)
-
-        self._lock.acquire()
-        try:
-            # First check the cache to avoid reparsing the same file
-            try:
-                tmpl = self._cache[filename]
-                if not self.auto_reload or \
-                        os.path.getmtime(tmpl.filepath) == self._mtime[filename]:
-                    return tmpl
-            except KeyError:
-                pass
-
-            search_path = self.search_path
-            isabs = False
-
-            if os.path.isabs(filename):
-                # Bypass the search path if the requested filename is absolute
-                search_path = [os.path.dirname(filename)]
-                isabs = True
-
-            elif relative_to and os.path.isabs(relative_to):
-                # Make sure that the directory containing the including
-                # template is on the search path
-                dirname = os.path.dirname(relative_to)
-                if dirname not in search_path:
-                    search_path = search_path + [dirname]
-                isabs = True
-
-            elif not search_path:
-                # Uh oh, don't know where to look for the template
-                raise TemplateError('Search path for templates not configured')
-
-            for dirname in search_path:
-                filepath = os.path.join(dirname, filename)
-                try:
-                    fileobj = open(filepath, 'U')
-                    try:
-                        if isabs:
-                            # If the filename of either the included or the 
-                            # including template is absolute, make sure the
-                            # included template gets an absolute path, too,
-                            # so that nested include work properly without a
-                            # search path
-                            filename = os.path.join(dirname, filename)
-                            dirname = ''
-                        tmpl = cls(fileobj, basedir=dirname, filename=filename,
-                                   loader=self, encoding=encoding)
-                    finally:
-                        fileobj.close()
-                    self._cache[filename] = tmpl
-                    self._mtime[filename] = os.path.getmtime(filepath)
-                    return tmpl
-                except IOError:
-                    continue
-
-            raise TemplateNotFound(filename, search_path)
-
-        finally:
-            self._lock.release()
+from genshi.template.core import Context, Template, TemplateError, \
+                                 TemplateRuntimeError, TemplateSyntaxError, \
+                                 BadDirectiveError
+from genshi.template.loader import TemplateLoader, TemplateNotFound
+from genshi.template.markup import MarkupTemplate
+from genshi.template.text import TextTemplate
new file mode 100644
--- /dev/null
+++ b/genshi/template/core.py
@@ -0,0 +1,381 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+try:
+    from collections import deque
+except ImportError:
+    class deque(list):
+        def appendleft(self, x): self.insert(0, x)
+        def popleft(self): return self.pop(0)
+import os
+import re
+from StringIO import StringIO
+
+from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
+from genshi.template.eval import Expression
+
+__all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError',
+           'TemplateSyntaxError', 'BadDirectiveError']
+
+
+class TemplateError(Exception):
+    """Base exception class for errors related to template processing."""
+
+
+class TemplateRuntimeError(TemplateError):
+    """Exception raised when an the evualation of a Python expression in a
+    template causes an error."""
+
+    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
+        self.msg = message
+        message = '%s (%s, line %d)' % (self.msg, filename, lineno)
+        TemplateError.__init__(self, message)
+        self.filename = filename
+        self.lineno = lineno
+        self.offset = offset
+
+
+class TemplateSyntaxError(TemplateError):
+    """Exception raised when an expression in a template causes a Python syntax
+    error."""
+
+    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
+        if isinstance(message, SyntaxError) and message.lineno is not None:
+            message = str(message).replace(' (line %d)' % message.lineno, '')
+        self.msg = message
+        message = '%s (%s, line %d)' % (self.msg, filename, lineno)
+        TemplateError.__init__(self, message)
+        self.filename = filename
+        self.lineno = lineno
+        self.offset = offset
+
+
+class BadDirectiveError(TemplateSyntaxError):
+    """Exception raised when an unknown directive is encountered when parsing
+    a template.
+    
+    An unknown directive is any attribute using the namespace for directives,
+    with a local name that doesn't match any registered directive.
+    """
+
+    def __init__(self, name, filename='<string>', lineno=-1):
+        message = 'bad directive "%s"' % name
+        TemplateSyntaxError.__init__(self, message, filename, lineno)
+
+
+class Context(object):
+    """Container for template input data.
+    
+    A context provides a stack of scopes (represented by dictionaries).
+    
+    Template directives such as loops can push a new scope on the stack with
+    data that should only be available inside the loop. When the loop
+    terminates, that scope can get popped off the stack again.
+    
+    >>> ctxt = Context(one='foo', other=1)
+    >>> ctxt.get('one')
+    'foo'
+    >>> ctxt.get('other')
+    1
+    >>> ctxt.push(dict(one='frost'))
+    >>> ctxt.get('one')
+    'frost'
+    >>> ctxt.get('other')
+    1
+    >>> ctxt.pop()
+    {'one': 'frost'}
+    >>> ctxt.get('one')
+    'foo'
+    """
+
+    def __init__(self, **data):
+        self.frames = deque([data])
+        self.pop = self.frames.popleft
+        self.push = self.frames.appendleft
+        self._match_templates = []
+
+    def __repr__(self):
+        return repr(list(self.frames))
+
+    def __setitem__(self, key, value):
+        """Set a variable in the current scope."""
+        self.frames[0][key] = value
+
+    def _find(self, key, default=None):
+        """Retrieve a given variable's value and the frame it was found in.
+
+        Intented for internal use by directives.
+        """
+        for frame in self.frames:
+            if key in frame:
+                return frame[key], frame
+        return default, None
+
+    def get(self, key, default=None):
+        """Get a variable's value, starting at the current scope and going
+        upward.
+        """
+        for frame in self.frames:
+            if key in frame:
+                return frame[key]
+        return default
+    __getitem__ = get
+
+    def push(self, data):
+        """Push a new scope on the stack."""
+
+    def pop(self):
+        """Pop the top-most scope from the stack."""
+
+
+class Directive(object):
+    """Abstract base class for template directives.
+    
+    A directive is basically a callable that takes three positional arguments:
+    `ctxt` is the template data context, `stream` is an iterable over the
+    events that the directive applies to, and `directives` is is a list of
+    other directives on the same stream that need to be applied.
+    
+    Directives can be "anonymous" or "registered". Registered directives can be
+    applied by the template author using an XML attribute with the
+    corresponding name in the template. Such directives should be subclasses of
+    this base class that can  be instantiated with the value of the directive
+    attribute as parameter.
+    
+    Anonymous directives are simply functions conforming to the protocol
+    described above, and can only be applied programmatically (for example by
+    template filters).
+    """
+    __slots__ = ['expr']
+
+    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+                 offset=-1):
+        try:
+            self.expr = value and Expression(value, filename, lineno) or None
+        except SyntaxError, err:
+            err.msg += ' in expression "%s" of "%s" directive' % (value,
+                                                                  self.tagname)
+            raise TemplateSyntaxError(err, filename, lineno,
+                                      offset + (err.offset or 0))
+
+    def __call__(self, stream, ctxt, directives):
+        raise NotImplementedError
+
+    def __repr__(self):
+        expr = ''
+        if self.expr is not None:
+            expr = ' "%s"' % self.expr.source
+        return '<%s%s>' % (self.__class__.__name__, expr)
+
+    def tagname(self):
+        """Return the local tag name of the directive as it is used in
+        templates.
+        """
+        return self.__class__.__name__.lower().replace('directive', '')
+    tagname = property(tagname)
+
+
+def _apply_directives(stream, ctxt, directives):
+    """Apply the given directives to the stream."""
+    if directives:
+        stream = directives[0](iter(stream), ctxt, directives[1:])
+    return stream
+
+
+class TemplateMeta(type):
+    """Meta class for templates."""
+
+    def __new__(cls, name, bases, d):
+        if 'directives' in d:
+            d['_dir_by_name'] = dict(d['directives'])
+            d['_dir_order'] = [directive[1] for directive in d['directives']]
+
+        return type.__new__(cls, name, bases, d)
+
+
+class Template(object):
+    """Abstract template base class.
+    
+    This class implements most of the template processing model, but does not
+    specify the syntax of templates.
+    """
+    __metaclass__ = TemplateMeta
+
+    EXPR = StreamEventKind('EXPR') # an expression
+    SUB = StreamEventKind('SUB') # a "subprogram"
+
+    def __init__(self, source, basedir=None, filename=None, loader=None,
+                 encoding=None):
+        """Initialize a template from either a string or a file-like object."""
+        if isinstance(source, basestring):
+            self.source = StringIO(source)
+        else:
+            self.source = source
+        self.basedir = basedir
+        self.filename = filename
+        if basedir and filename:
+            self.filepath = os.path.join(basedir, filename)
+        else:
+            self.filepath = filename
+
+        self.filters = [self._flatten, self._eval]
+
+        self.stream = self._parse(encoding)
+
+    def __repr__(self):
+        return '<%s "%s">' % (self.__class__.__name__, self.filename)
+
+    def _parse(self, encoding):
+        """Parse the template.
+        
+        The parsing stage parses the template and constructs a list of
+        directives that will be executed in the render stage. The input is
+        split up into literal output (text that does not depend on the context
+        data) and directives or expressions.
+        """
+        raise NotImplementedError
+
+    _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}', re.DOTALL)
+    _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z_][a-zA-Z0-9_\.]*)')
+
+    def _interpolate(cls, text, basedir=None, filename=None, lineno=-1,
+                     offset=0):
+        """Parse the given string and extract expressions.
+        
+        This method returns a list containing both literal text and `Expression`
+        objects.
+        
+        @param text: the text to parse
+        @param lineno: the line number at which the text was found (optional)
+        @param offset: the column number at which the text starts in the source
+            (optional)
+        """
+        filepath = filename
+        if filepath and basedir:
+            filepath = os.path.join(basedir, filepath)
+        def _interpolate(text, patterns, lineno=lineno, offset=offset):
+            for idx, grp in enumerate(patterns.pop(0).split(text)):
+                if idx % 2:
+                    try:
+                        yield EXPR, Expression(grp.strip(), filepath, lineno), \
+                              (filename, lineno, offset)
+                    except SyntaxError, err:
+                        raise TemplateSyntaxError(err, filepath, lineno,
+                                                  offset + (err.offset or 0))
+                elif grp:
+                    if patterns:
+                        for result in _interpolate(grp, patterns[:]):
+                            yield result
+                    else:
+                        yield TEXT, grp.replace('$$', '$'), \
+                              (filename, lineno, offset)
+                if '\n' in grp:
+                    lines = grp.splitlines()
+                    lineno += len(lines) - 1
+                    offset += len(lines[-1])
+                else:
+                    offset += len(grp)
+        return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE])
+    _interpolate = classmethod(_interpolate)
+
+    def generate(self, *args, **kwargs):
+        """Apply the template to the given context data.
+        
+        Any keyword arguments are made available to the template as context
+        data.
+        
+        Only one positional argument is accepted: if it is provided, it must be
+        an instance of the `Context` class, and keyword arguments are ignored.
+        This calling style is used for internal processing.
+        
+        @return: a markup event stream representing the result of applying
+            the template to the context data.
+        """
+        if args:
+            assert len(args) == 1
+            ctxt = args[0]
+            if ctxt is None:
+                ctxt = Context(**kwargs)
+            assert isinstance(ctxt, Context)
+        else:
+            ctxt = Context(**kwargs)
+
+        stream = self.stream
+        for filter_ in self.filters:
+            stream = filter_(iter(stream), ctxt)
+        return Stream(stream)
+
+    def _eval(self, stream, ctxt):
+        """Internal stream filter that evaluates any expressions in `START` and
+        `TEXT` events.
+        """
+        filters = (self._flatten, self._eval)
+
+        for kind, data, pos in stream:
+
+            if kind is START and data[1]:
+                # Attributes may still contain expressions in start tags at
+                # this point, so do some evaluation
+                tag, attrib = data
+                new_attrib = []
+                for name, substream in attrib:
+                    if isinstance(substream, basestring):
+                        value = substream
+                    else:
+                        values = []
+                        for subkind, subdata, subpos in self._eval(substream,
+                                                                   ctxt):
+                            if subkind is TEXT:
+                                values.append(subdata)
+                        value = [x for x in values if x is not None]
+                        if not value:
+                            continue
+                    new_attrib.append((name, u''.join(value)))
+                yield kind, (tag, Attrs(new_attrib)), pos
+
+            elif kind is EXPR:
+                result = data.evaluate(ctxt)
+                if result is not None:
+                    # First check for a string, otherwise the iterable test below
+                    # succeeds, and the string will be chopped up into individual
+                    # characters
+                    if isinstance(result, basestring):
+                        yield TEXT, result, pos
+                    elif hasattr(result, '__iter__'):
+                        substream = _ensure(result)
+                        for filter_ in filters:
+                            substream = filter_(substream, ctxt)
+                        for event in substream:
+                            yield event
+                    else:
+                        yield TEXT, unicode(result), pos
+
+            else:
+                yield kind, data, pos
+
+    def _flatten(self, stream, ctxt):
+        """Internal stream filter that expands `SUB` events in the stream."""
+        for event in stream:
+            if event[0] is SUB:
+                # This event is a list of directives and a list of nested
+                # events to which those directives should be applied
+                directives, substream = event[1]
+                substream = _apply_directives(substream, ctxt, directives)
+                for event in self._flatten(substream, ctxt):
+                    yield event
+            else:
+                yield event
+
+
+EXPR = Template.EXPR
+SUB = Template.SUB
new file mode 100644
--- /dev/null
+++ b/genshi/template/directives.py
@@ -0,0 +1,600 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Implementation of the various template directives."""
+
+import compiler
+
+from genshi.core import Attrs, Stream
+from genshi.path import Path
+from genshi.template.core import EXPR, Directive, TemplateRuntimeError, \
+                                 TemplateSyntaxError, _apply_directives
+from genshi.template.eval import Expression, _parse
+
+__all__ = ['AttrsDirective', 'ChooseDirective', 'ContentDirective',
+           'DefDirective', 'ForDirective', 'IfDirective', 'MatchDirective',
+           'OtherwiseDirective', 'ReplaceDirective', 'StripDirective',
+           'WhenDirective', 'WithDirective']
+
+
+def _assignment(ast):
+    """Takes the AST representation of an assignment, and returns a function
+    that applies the assignment of a given value to a dictionary.
+    """
+    def _names(node):
+        if isinstance(node, (compiler.ast.AssTuple, compiler.ast.Tuple)):
+            return tuple([_names(child) for child in node.nodes])
+        elif isinstance(node, (compiler.ast.AssName, compiler.ast.Name)):
+            return node.name
+    def _assign(data, value, names=_names(ast)):
+        if type(names) is tuple:
+            for idx in range(len(names)):
+                _assign(data, value[idx], names[idx])
+        else:
+            data[names] = value
+    return _assign
+
+
+class AttrsDirective(Directive):
+    """Implementation of the `py:attrs` template directive.
+    
+    The value of the `py:attrs` attribute should be a dictionary or a sequence
+    of `(name, value)` tuples. The items in that dictionary or sequence are
+    added as attributes to the element:
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
+    ...   <li py:attrs="foo">Bar</li>
+    ... </ul>''')
+    >>> print tmpl.generate(foo={'class': 'collapse'})
+    <ul>
+      <li class="collapse">Bar</li>
+    </ul>
+    >>> print tmpl.generate(foo=[('class', 'collapse')])
+    <ul>
+      <li class="collapse">Bar</li>
+    </ul>
+    
+    If the value evaluates to `None` (or any other non-truth value), no
+    attributes are added:
+    
+    >>> print tmpl.generate(foo=None)
+    <ul>
+      <li>Bar</li>
+    </ul>
+    """
+    __slots__ = []
+
+    def __call__(self, stream, ctxt, directives):
+        def _generate():
+            kind, (tag, attrib), pos  = stream.next()
+            attrs = self.expr.evaluate(ctxt)
+            if attrs:
+                attrib = Attrs(attrib[:])
+                if isinstance(attrs, Stream):
+                    try:
+                        attrs = iter(attrs).next()
+                    except StopIteration:
+                        attrs = []
+                elif not isinstance(attrs, list): # assume it's a dict
+                    attrs = attrs.items()
+                for name, value in attrs:
+                    if value is None:
+                        attrib.remove(name)
+                    else:
+                        attrib.set(name, unicode(value).strip())
+            yield kind, (tag, attrib), pos
+            for event in stream:
+                yield event
+
+        return _apply_directives(_generate(), ctxt, directives)
+
+
+class ContentDirective(Directive):
+    """Implementation of the `py:content` template directive.
+    
+    This directive replaces the content of the element with the result of
+    evaluating the value of the `py:content` attribute:
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
+    ...   <li py:content="bar">Hello</li>
+    ... </ul>''')
+    >>> print tmpl.generate(bar='Bye')
+    <ul>
+      <li>Bye</li>
+    </ul>
+    """
+    __slots__ = []
+
+    def __call__(self, stream, ctxt, directives):
+        def _generate():
+            yield stream.next()
+            yield EXPR, self.expr, (None, -1, -1)
+            event = stream.next()
+            for next in stream:
+                event = next
+            yield event
+
+        return _apply_directives(_generate(), ctxt, directives)
+
+
+class DefDirective(Directive):
+    """Implementation of the `py:def` template directive.
+    
+    This directive can be used to create "Named Template Functions", which
+    are template snippets that are not actually output during normal
+    processing, but rather can be expanded from expressions in other places
+    in the template.
+    
+    A named template function can be used just like a normal Python function
+    from template expressions:
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+    ...   <p py:def="echo(greeting, name='world')" class="message">
+    ...     ${greeting}, ${name}!
+    ...   </p>
+    ...   ${echo('Hi', name='you')}
+    ... </div>''')
+    >>> print tmpl.generate(bar='Bye')
+    <div>
+      <p class="message">
+        Hi, you!
+      </p>
+    </div>
+    
+    If a function does not require parameters, the parenthesis can be omitted
+    both when defining and when calling it:
+    
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+    ...   <p py:def="helloworld" class="message">
+    ...     Hello, world!
+    ...   </p>
+    ...   ${helloworld}
+    ... </div>''')
+    >>> print tmpl.generate(bar='Bye')
+    <div>
+      <p class="message">
+        Hello, world!
+      </p>
+    </div>
+    """
+    __slots__ = ['name', 'args', 'defaults']
+
+    ATTRIBUTE = 'function'
+
+    def __init__(self, args, namespaces=None, filename=None, lineno=-1,
+                 offset=-1):
+        Directive.__init__(self, None, namespaces, filename, lineno, offset)
+        ast = _parse(args).node
+        self.args = []
+        self.defaults = {}
+        if isinstance(ast, compiler.ast.CallFunc):
+            self.name = ast.node.name
+            for arg in ast.args:
+                if isinstance(arg, compiler.ast.Keyword):
+                    self.args.append(arg.name)
+                    self.defaults[arg.name] = Expression(arg.expr, filename,
+                                                         lineno)
+                else:
+                    self.args.append(arg.name)
+        else:
+            self.name = ast.name
+
+    def __call__(self, stream, ctxt, directives):
+        stream = list(stream)
+
+        def function(*args, **kwargs):
+            scope = {}
+            args = list(args) # make mutable
+            for name in self.args:
+                if args:
+                    scope[name] = args.pop(0)
+                else:
+                    if name in kwargs:
+                        val = kwargs.pop(name)
+                    else:
+                        val = self.defaults.get(name).evaluate(ctxt)
+                    scope[name] = val
+            ctxt.push(scope)
+            for event in _apply_directives(stream, ctxt, directives):
+                yield event
+            ctxt.pop()
+        try:
+            function.__name__ = self.name
+        except TypeError:
+            # Function name can't be set in Python 2.3 
+            pass
+
+        # Store the function reference in the bottom context frame so that it
+        # doesn't get popped off before processing the template has finished
+        # FIXME: this makes context data mutable as a side-effect
+        ctxt.frames[-1][self.name] = function
+
+        return []
+
+    def __repr__(self):
+        return '<%s "%s">' % (self.__class__.__name__, self.name)
+
+
+class ForDirective(Directive):
+    """Implementation of the `py:for` template directive for repeating an
+    element based on an iterable in the context data.
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
+    ...   <li py:for="item in items">${item}</li>
+    ... </ul>''')
+    >>> print tmpl.generate(items=[1, 2, 3])
+    <ul>
+      <li>1</li><li>2</li><li>3</li>
+    </ul>
+    """
+    __slots__ = ['assign', 'filename']
+
+    ATTRIBUTE = 'each'
+
+    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+                 offset=-1):
+        if ' in ' not in value:
+            raise TemplateSyntaxError('"in" keyword missing in "for" directive',
+                                      filename, lineno, offset)
+        assign, value = value.split(' in ', 1)
+        ast = _parse(assign, 'exec')
+        self.assign = _assignment(ast.node.nodes[0].expr)
+        self.filename = filename
+        Directive.__init__(self, value.strip(), namespaces, filename, lineno,
+                           offset)
+
+    def __call__(self, stream, ctxt, directives):
+        iterable = self.expr.evaluate(ctxt)
+        if iterable is None:
+            return
+
+        assign = self.assign
+        scope = {}
+        stream = list(stream)
+        try:
+            iterator = iter(iterable)
+            for item in iterator:
+                assign(scope, item)
+                ctxt.push(scope)
+                for event in _apply_directives(stream, ctxt, directives):
+                    yield event
+                ctxt.pop()
+        except TypeError, e:
+            raise TemplateRuntimeError(str(e), self.filename, *stream[0][2][1:])
+
+    def __repr__(self):
+        return '<%s>' % self.__class__.__name__
+
+
+class IfDirective(Directive):
+    """Implementation of the `py:if` template directive for conditionally
+    excluding elements from being output.
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+    ...   <b py:if="foo">${bar}</b>
+    ... </div>''')
+    >>> print tmpl.generate(foo=True, bar='Hello')
+    <div>
+      <b>Hello</b>
+    </div>
+    """
+    __slots__ = []
+
+    ATTRIBUTE = 'test'
+
+    def __call__(self, stream, ctxt, directives):
+        if self.expr.evaluate(ctxt):
+            return _apply_directives(stream, ctxt, directives)
+        return []
+
+
+class MatchDirective(Directive):
+    """Implementation of the `py:match` template directive.
+
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+    ...   <span py:match="greeting">
+    ...     Hello ${select('@name')}
+    ...   </span>
+    ...   <greeting name="Dude" />
+    ... </div>''')
+    >>> print tmpl.generate()
+    <div>
+      <span>
+        Hello Dude
+      </span>
+    </div>
+    """
+    __slots__ = ['path', 'namespaces']
+
+    ATTRIBUTE = 'path'
+
+    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+                 offset=-1):
+        Directive.__init__(self, None, namespaces, filename, lineno, offset)
+        self.path = Path(value, filename, lineno)
+        if namespaces is None:
+            namespaces = {}
+        self.namespaces = namespaces.copy()
+
+    def __call__(self, stream, ctxt, directives):
+        ctxt._match_templates.append((self.path.test(ignore_context=True),
+                                      self.path, list(stream), self.namespaces,
+                                      directives))
+        return []
+
+    def __repr__(self):
+        return '<%s "%s">' % (self.__class__.__name__, self.path.source)
+
+
+class ReplaceDirective(Directive):
+    """Implementation of the `py:replace` template directive.
+    
+    This directive replaces the element with the result of evaluating the
+    value of the `py:replace` attribute:
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+    ...   <span py:replace="bar">Hello</span>
+    ... </div>''')
+    >>> print tmpl.generate(bar='Bye')
+    <div>
+      Bye
+    </div>
+    
+    This directive is equivalent to `py:content` combined with `py:strip`,
+    providing a less verbose way to achieve the same effect:
+    
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+    ...   <span py:content="bar" py:strip="">Hello</span>
+    ... </div>''')
+    >>> print tmpl.generate(bar='Bye')
+    <div>
+      Bye
+    </div>
+    """
+    __slots__ = []
+
+    def __call__(self, stream, ctxt, directives):
+        yield EXPR, self.expr, (None, -1, -1)
+
+
+class StripDirective(Directive):
+    """Implementation of the `py:strip` template directive.
+    
+    When the value of the `py:strip` attribute evaluates to `True`, the element
+    is stripped from the output
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+    ...   <div py:strip="True"><b>foo</b></div>
+    ... </div>''')
+    >>> print tmpl.generate()
+    <div>
+      <b>foo</b>
+    </div>
+    
+    Leaving the attribute value empty is equivalent to a truth value.
+    
+    This directive is particulary interesting for named template functions or
+    match templates that do not generate a top-level element:
+    
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+    ...   <div py:def="echo(what)" py:strip="">
+    ...     <b>${what}</b>
+    ...   </div>
+    ...   ${echo('foo')}
+    ... </div>''')
+    >>> print tmpl.generate()
+    <div>
+        <b>foo</b>
+    </div>
+    """
+    __slots__ = []
+
+    def __call__(self, stream, ctxt, directives):
+        def _generate():
+            if self.expr:
+                strip = self.expr.evaluate(ctxt)
+            else:
+                strip = True
+            if strip:
+                stream.next() # skip start tag
+                previous = stream.next()
+                for event in stream:
+                    yield previous
+                    previous = event
+            else:
+                for event in stream:
+                    yield event
+
+        return _apply_directives(_generate(), ctxt, directives)
+
+
+class ChooseDirective(Directive):
+    """Implementation of the `py:choose` directive for conditionally selecting
+    one of several body elements to display.
+    
+    If the `py:choose` expression is empty the expressions of nested `py:when`
+    directives are tested for truth.  The first true `py:when` body is output.
+    If no `py:when` directive is matched then the fallback directive
+    `py:otherwise` will be used.
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"
+    ...   py:choose="">
+    ...   <span py:when="0 == 1">0</span>
+    ...   <span py:when="1 == 1">1</span>
+    ...   <span py:otherwise="">2</span>
+    ... </div>''')
+    >>> print tmpl.generate()
+    <div>
+      <span>1</span>
+    </div>
+    
+    If the `py:choose` directive contains an expression, the nested `py:when`
+    directives are tested for equality to the `py:choose` expression:
+    
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"
+    ...   py:choose="2">
+    ...   <span py:when="1">1</span>
+    ...   <span py:when="2">2</span>
+    ... </div>''')
+    >>> print tmpl.generate()
+    <div>
+      <span>2</span>
+    </div>
+    
+    Behavior is undefined if a `py:choose` block contains content outside a
+    `py:when` or `py:otherwise` block.  Behavior is also undefined if a
+    `py:otherwise` occurs before `py:when` blocks.
+    """
+    __slots__ = ['matched', 'value']
+
+    ATTRIBUTE = 'test'
+
+    def __call__(self, stream, ctxt, directives):
+        frame = dict({'_choose.matched': False})
+        if self.expr:
+            frame['_choose.value'] = self.expr.evaluate(ctxt)
+        ctxt.push(frame)
+        for event in _apply_directives(stream, ctxt, directives):
+            yield event
+        ctxt.pop()
+
+
+class WhenDirective(Directive):
+    """Implementation of the `py:when` directive for nesting in a parent with
+    the `py:choose` directive.
+    
+    See the documentation of `py:choose` for usage.
+    """
+    __slots__ = ['filename']
+
+    ATTRIBUTE = 'test'
+
+    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+                 offset=-1):
+        Directive.__init__(self, value, namespaces, filename, lineno, offset)
+        self.filename = filename
+
+    def __call__(self, stream, ctxt, directives):
+        matched, frame = ctxt._find('_choose.matched')
+        if not frame:
+            raise TemplateRuntimeError('"when" directives can only be used '
+                                       'inside a "choose" directive',
+                                       self.filename, *stream.next()[2][1:])
+        if matched:
+            return []
+        if not self.expr and '_choose.value' not in frame:
+            raise TemplateRuntimeError('either "choose" or "when" directive '
+                                       'must have a test expression',
+                                       self.filename, *stream.next()[2][1:])
+        if '_choose.value' in frame:
+            value = frame['_choose.value']
+            if self.expr:
+                matched = value == self.expr.evaluate(ctxt)
+            else:
+                matched = bool(value)
+        else:
+            matched = bool(self.expr.evaluate(ctxt))
+        frame['_choose.matched'] = matched
+        if not matched:
+            return []
+
+        return _apply_directives(stream, ctxt, directives)
+
+
+class OtherwiseDirective(Directive):
+    """Implementation of the `py:otherwise` directive for nesting in a parent
+    with the `py:choose` directive.
+    
+    See the documentation of `py:choose` for usage.
+    """
+    __slots__ = ['filename']
+
+    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+                 offset=-1):
+        Directive.__init__(self, None, namespaces, filename, lineno, offset)
+        self.filename = filename
+
+    def __call__(self, stream, ctxt, directives):
+        matched, frame = ctxt._find('_choose.matched')
+        if not frame:
+            raise TemplateRuntimeError('an "otherwise" directive can only be '
+                                       'used inside a "choose" directive',
+                                       self.filename, *stream.next()[2][1:])
+        if matched:
+            return []
+        frame['_choose.matched'] = True
+
+        return _apply_directives(stream, ctxt, directives)
+
+
+class WithDirective(Directive):
+    """Implementation of the `py:with` template directive, which allows
+    shorthand access to variables and expressions.
+    
+    >>> from genshi.template import MarkupTemplate
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+    ...   <span py:with="y=7; z=x+10">$x $y $z</span>
+    ... </div>''')
+    >>> print tmpl.generate(x=42)
+    <div>
+      <span>42 7 52</span>
+    </div>
+    """
+    __slots__ = ['vars']
+
+    ATTRIBUTE = 'vars'
+
+    def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+                 offset=-1):
+        Directive.__init__(self, None, namespaces, filename, lineno, offset)
+        self.vars = []
+        value = value.strip()
+        try:
+            ast = _parse(value, 'exec').node
+            for node in ast.nodes:
+                if isinstance(node, compiler.ast.Discard):
+                    continue
+                elif not isinstance(node, compiler.ast.Assign):
+                    raise TemplateSyntaxError('only assignment allowed in '
+                                              'value of the "with" directive',
+                                              filename, lineno, offset)
+                self.vars.append(([_assignment(n) for n in node.nodes],
+                                  Expression(node.expr, filename, lineno)))
+        except SyntaxError, err:
+            err.msg += ' in expression "%s" of "%s" directive' % (value,
+                                                                  self.tagname)
+            raise TemplateSyntaxError(err, filename, lineno,
+                                      offset + (err.offset or 0))
+
+    def __call__(self, stream, ctxt, directives):
+        frame = {}
+        ctxt.push(frame)
+        for targets, expr in self.vars:
+            value = expr.evaluate(ctxt, nocall=True)
+            for assign in targets:
+                assign(frame, value)
+        for event in _apply_directives(stream, ctxt, directives):
+            yield event
+        ctxt.pop()
+
+    def __repr__(self):
+        return '<%s>' % (self.__class__.__name__)
new file mode 100644
--- /dev/null
+++ b/genshi/template/eval.py
@@ -0,0 +1,427 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Support for "safe" evaluation of Python expressions."""
+
+import __builtin__
+from compiler import ast, parse
+from compiler.pycodegen import ExpressionCodeGenerator
+import new
+
+__all__ = ['Expression', 'Undefined']
+
+
+class Expression(object):
+    """Evaluates Python expressions used in templates.
+
+    >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
+    >>> Expression('test').evaluate(data)
+    'Foo'
+
+    >>> Expression('items[0]').evaluate(data)
+    1
+    >>> Expression('items[-1]').evaluate(data)
+    3
+    >>> Expression('dict["some"]').evaluate(data)
+    'thing'
+    
+    Similar to e.g. Javascript, expressions in templates can use the dot
+    notation for attribute access to access items in mappings:
+    
+    >>> Expression('dict.some').evaluate(data)
+    'thing'
+    
+    This also works the other way around: item access can be used to access
+    any object attribute (meaning there's no use for `getattr()` in templates):
+    
+    >>> class MyClass(object):
+    ...     myattr = 'Bar'
+    >>> data = dict(mine=MyClass(), key='myattr')
+    >>> Expression('mine.myattr').evaluate(data)
+    'Bar'
+    >>> Expression('mine["myattr"]').evaluate(data)
+    'Bar'
+    >>> Expression('mine[key]').evaluate(data)
+    'Bar'
+    
+    All of the standard Python operators are available to template expressions.
+    Built-in functions such as `len()` are also available in template
+    expressions:
+    
+    >>> data = dict(items=[1, 2, 3])
+    >>> Expression('len(items)').evaluate(data)
+    3
+    """
+    __slots__ = ['source', 'code']
+
+    def __init__(self, source, filename=None, lineno=-1):
+        """Create the expression, either from a string, or from an AST node.
+        
+        @param source: either a string containing the source code of the
+            expression, or an AST node
+        @param filename: the (preferably absolute) name of the file containing
+            the expression
+        @param lineno: the number of the line on which the expression was found
+        """
+        if isinstance(source, basestring):
+            self.source = source
+            self.code = _compile(_parse(source), self.source, filename=filename,
+                                 lineno=lineno)
+        else:
+            assert isinstance(source, ast.Node)
+            self.source = '?'
+            self.code = _compile(ast.Expression(source), filename=filename,
+                                 lineno=lineno)
+
+    def __repr__(self):
+        return 'Expression(%r)' % self.source
+
+    def evaluate(self, data, nocall=False):
+        """Evaluate the expression against the given data dictionary.
+        
+        @param data: a mapping containing the data to evaluate against
+        @param nocall: if true, the result of the evaluation is not called if
+            if it is a callable
+        @return: the result of the evaluation
+        """
+        retval = eval(self.code, {'data': data,
+                                  '_lookup_name': _lookup_name,
+                                  '_lookup_attr': _lookup_attr,
+                                  '_lookup_item': _lookup_item},
+                                 {'data': data})
+        if not nocall and type(retval) is not Undefined and callable(retval):
+            retval = retval()
+        return retval
+
+
+class Undefined(object):
+    """Represents a reference to an undefined variable.
+    
+    Unlike the Python runtime, template expressions can refer to an undefined
+    variable without causing a `NameError` to be raised. The result will be an
+    instance of the `Undefined´ class, which is treated the same as `False` in
+    conditions, and acts as an empty collection in iterations:
+    
+    >>> foo = Undefined('foo')
+    >>> bool(foo)
+    False
+    >>> list(foo)
+    []
+    >>> print foo
+    undefined
+    
+    However, calling an undefined variable, or trying to access an attribute
+    of that variable, will raise an exception that includes the name used to
+    reference that undefined variable.
+    
+    >>> foo('bar')
+    Traceback (most recent call last):
+        ...
+    NameError: Variable "foo" is not defined
+
+    >>> foo.bar
+    Traceback (most recent call last):
+        ...
+    NameError: Variable "foo" is not defined
+    """
+    __slots__ = ['_name']
+
+    def __init__(self, name):
+        self._name = name
+
+    def __call__(self, *args, **kwargs):
+        __traceback_hide__ = True
+        self.throw()
+
+    def __getattr__(self, name):
+        __traceback_hide__ = True
+        self.throw()
+
+    def __iter__(self):
+        return iter([])
+
+    def __nonzero__(self):
+        return False
+
+    def __repr__(self):
+        return 'undefined'
+
+    def throw(self):
+        __traceback_hide__ = True
+        raise NameError('Variable "%s" is not defined' % self._name)
+
+
+def _parse(source, mode='eval'):
+    if isinstance(source, unicode):
+        source = '\xef\xbb\xbf' + source.encode('utf-8')
+    return parse(source, mode)
+
+def _compile(node, source=None, filename=None, lineno=-1):
+    tree = ExpressionASTTransformer().visit(node)
+    if isinstance(filename, unicode):
+        # unicode file names not allowed for code objects
+        filename = filename.encode('utf-8', 'replace')
+    elif not filename:
+        filename = '<string>'
+    tree.filename = filename
+    if lineno <= 0:
+        lineno = 1
+
+    gen = ExpressionCodeGenerator(tree)
+    gen.optimized = True
+    code = gen.getCode()
+
+    # We'd like to just set co_firstlineno, but it's readonly. So we need to
+    # clone the code object while adjusting the line number
+    return new.code(0, code.co_nlocals, code.co_stacksize,
+                    code.co_flags | 0x0040, code.co_code, code.co_consts,
+                    code.co_names, code.co_varnames, filename,
+                    '<Expression %s>' % (repr(source or '?').replace("'", '"')),
+                    lineno, code.co_lnotab, (), ())
+
+BUILTINS = __builtin__.__dict__.copy()
+BUILTINS['Undefined'] = Undefined
+
+def _lookup_name(data, name, locals_=None):
+    __traceback_hide__ = True
+    val = Undefined
+    if locals_:
+        val = locals_.get(name, val)
+    if val is Undefined:
+        val = data.get(name, val)
+        if val is Undefined:
+            val = BUILTINS.get(name, val)
+            if val is not Undefined or name == 'Undefined':
+                return val
+        else:
+            return val
+    else:
+        return val
+    return val(name)
+
+def _lookup_attr(data, obj, key):
+    __traceback_hide__ = True
+    if type(obj) is Undefined:
+        obj.throw()
+    if hasattr(obj, key):
+        return getattr(obj, key)
+    try:
+        return obj[key]
+    except (KeyError, TypeError):
+        return Undefined(key)
+
+def _lookup_item(data, obj, key):
+    __traceback_hide__ = True
+    if type(obj) is Undefined:
+        obj.throw()
+    if len(key) == 1:
+        key = key[0]
+    try:
+        return obj[key]
+    except (KeyError, IndexError, TypeError), e:
+        if isinstance(key, basestring):
+            val = getattr(obj, key, Undefined)
+            if val is Undefined:
+                val = Undefined(key)
+            return val
+        raise
+
+
+class ASTTransformer(object):
+    """General purpose base class for AST transformations.
+    
+    Every visitor method can be overridden to return an AST node that has been
+    altered or replaced in some way.
+    """
+    _visitors = {}
+
+    def visit(self, node, *args, **kwargs):
+        v = self._visitors.get(node.__class__)
+        if not v:
+            v = getattr(self, 'visit%s' % node.__class__.__name__)
+            self._visitors[node.__class__] = v
+        return v(node, *args, **kwargs)
+
+    def visitExpression(self, node, *args, **kwargs):
+        node.node = self.visit(node.node, *args, **kwargs)
+        return node
+
+    # Functions & Accessors
+
+    def visitCallFunc(self, node, *args, **kwargs):
+        node.node = self.visit(node.node, *args, **kwargs)
+        node.args = [self.visit(x, *args, **kwargs) for x in node.args]
+        if node.star_args:
+            node.star_args = self.visit(node.star_args, *args, **kwargs)
+        if node.dstar_args:
+            node.dstar_args = self.visit(node.dstar_args, *args, **kwargs)
+        return node
+
+    def visitLambda(self, node, *args, **kwargs):
+        node.code = self.visit(node.code, *args, **kwargs)
+        node.filename = '<string>' # workaround for bug in pycodegen
+        return node
+
+    def visitGetattr(self, node, *args, **kwargs):
+        node.expr = self.visit(node.expr, *args, **kwargs)
+        return node
+
+    def visitSubscript(self, node, *args, **kwargs):
+        node.expr = self.visit(node.expr, *args, **kwargs)
+        node.subs = [self.visit(x, *args, **kwargs) for x in node.subs]
+        return node
+
+    # Operators
+
+    def _visitBoolOp(self, node, *args, **kwargs):
+        node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
+        return node
+    visitAnd = visitOr = visitBitand = visitBitor = _visitBoolOp
+
+    def _visitBinOp(self, node, *args, **kwargs):
+        node.left = self.visit(node.left, *args, **kwargs)
+        node.right = self.visit(node.right, *args, **kwargs)
+        return node
+    visitAdd = visitSub = _visitBinOp
+    visitDiv = visitFloorDiv = visitMod = visitMul = visitPower = _visitBinOp
+    visitLeftShift = visitRightShift = _visitBinOp
+
+    def visitCompare(self, node, *args, **kwargs):
+        node.expr = self.visit(node.expr, *args, **kwargs)
+        node.ops = [(op, self.visit(n, *args, **kwargs)) for op, n in  node.ops]
+        return node
+
+    def _visitUnaryOp(self, node, *args, **kwargs):
+        node.expr = self.visit(node.expr, *args, **kwargs)
+        return node
+    visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp
+    visitBackquote = _visitUnaryOp
+
+    # Identifiers, Literals and Comprehensions
+
+    def _visitDefault(self, node, *args, **kwargs):
+        return node
+    visitAssName = visitAssTuple = _visitDefault
+    visitConst = visitName = _visitDefault
+
+    def visitDict(self, node, *args, **kwargs):
+        node.items = [(self.visit(k, *args, **kwargs),
+                       self.visit(v, *args, **kwargs)) for k, v in node.items]
+        return node
+
+    def visitGenExpr(self, node, *args, **kwargs):
+        node.code = self.visit(node.code, *args, **kwargs)
+        node.filename = '<string>' # workaround for bug in pycodegen
+        return node
+
+    def visitGenExprFor(self, node, *args, **kwargs):
+        node.assign = self.visit(node.assign, *args, **kwargs)
+        node.iter = self.visit(node.iter, *args, **kwargs)
+        node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
+        return node
+
+    def visitGenExprIf(self, node, *args, **kwargs):
+        node.test = self.visit(node.test, *args, **kwargs)
+        return node
+
+    def visitGenExprInner(self, node, *args, **kwargs):
+        node.expr = self.visit(node.expr, *args, **kwargs)
+        node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
+        return node
+
+    def visitKeyword(self, node, *args, **kwargs):
+        node.expr = self.visit(node.expr, *args, **kwargs)
+        return node
+
+    def visitList(self, node, *args, **kwargs):
+        node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
+        return node
+
+    def visitListComp(self, node, *args, **kwargs):
+        node.expr = self.visit(node.expr, *args, **kwargs)
+        node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
+        return node
+
+    def visitListCompFor(self, node, *args, **kwargs):
+        node.assign = self.visit(node.assign, *args, **kwargs)
+        node.list = self.visit(node.list, *args, **kwargs)
+        node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
+        return node
+
+    def visitListCompIf(self, node, *args, **kwargs):
+        node.test = self.visit(node.test, *args, **kwargs)
+        return node
+
+    def visitSlice(self, node, *args, **kwargs):
+        node.expr = self.visit(node.expr, locals_=True, *args, **kwargs)
+        if node.lower is not None:
+            node.lower = self.visit(node.lower, *args, **kwargs)
+        if node.upper is not None:
+            node.upper = self.visit(node.upper, *args, **kwargs)
+        return node
+
+    def visitSliceobj(self, node, *args, **kwargs):
+        node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
+        return node
+
+    def visitTuple(self, node, *args, **kwargs):
+        node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
+        return node
+
+
+class ExpressionASTTransformer(ASTTransformer):
+    """Concrete AST transformer that implements the AST transformations needed
+    for template expressions.
+    """
+
+    def visitConst(self, node, locals_=False):
+        if isinstance(node.value, str):
+            return ast.Const(node.value.decode('utf-8'))
+        return node
+
+    def visitGenExprIf(self, node, *args, **kwargs):
+        node.test = self.visit(node.test, locals_=True)
+        return node
+
+    def visitGenExprInner(self, node, *args, **kwargs):
+        node.expr = self.visit(node.expr, locals_=True)
+        node.quals = [self.visit(x) for x in node.quals]
+        return node
+
+    def visitGetattr(self, node, locals_=False):
+        return ast.CallFunc(ast.Name('_lookup_attr'), [
+            ast.Name('data'), self.visit(node.expr, locals_=locals_),
+            ast.Const(node.attrname)
+        ])
+
+    def visitLambda(self, node, locals_=False):
+        node.code = self.visit(node.code, locals_=True)
+        node.filename = '<string>' # workaround for bug in pycodegen
+        return node
+
+    def visitListComp(self, node, locals_=False):
+        node.expr = self.visit(node.expr, locals_=True)
+        node.quals = [self.visit(qual, locals_=True) for qual in node.quals]
+        return node
+
+    def visitName(self, node, locals_=False):
+        func_args = [ast.Name('data'), ast.Const(node.name)]
+        if locals_:
+            func_args.append(ast.CallFunc(ast.Name('locals'), []))
+        return ast.CallFunc(ast.Name('_lookup_name'), func_args)
+
+    def visitSubscript(self, node, locals_=False):
+        return ast.CallFunc(ast.Name('_lookup_item'), [
+            ast.Name('data'), self.visit(node.expr, locals_=locals_),
+            ast.Tuple([self.visit(sub, locals_=locals_) for sub in node.subs])
+        ])
new file mode 100644
--- /dev/null
+++ b/genshi/template/loader.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Template loading and caching."""
+
+import os
+try:
+    import threading
+except ImportError:
+    import dummy_threading as threading
+
+from genshi.template.core import TemplateError
+from genshi.template.markup import MarkupTemplate
+from genshi.util import LRUCache
+
+__all__ = ['TemplateLoader', 'TemplateNotFound']
+
+
+class TemplateNotFound(TemplateError):
+    """Exception raised when a specific template file could not be found."""
+
+    def __init__(self, name, search_path):
+        TemplateError.__init__(self, 'Template "%s" not found' % name)
+        self.search_path = search_path
+
+
+class TemplateLoader(object):
+    """Responsible for loading templates from files on the specified search
+    path.
+    
+    >>> import tempfile
+    >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
+    >>> os.write(fd, '<p>$var</p>')
+    11
+    >>> os.close(fd)
+    
+    The template loader accepts a list of directory paths that are then used
+    when searching for template files, in the given order:
+    
+    >>> loader = TemplateLoader([os.path.dirname(path)])
+    
+    The `load()` method first checks the template cache whether the requested
+    template has already been loaded. If not, it attempts to locate the
+    template file, and returns the corresponding `Template` object:
+    
+    >>> template = loader.load(os.path.basename(path))
+    >>> isinstance(template, MarkupTemplate)
+    True
+    
+    Template instances are cached: requesting a template with the same name
+    results in the same instance being returned:
+    
+    >>> loader.load(os.path.basename(path)) is template
+    True
+    
+    >>> os.remove(path)
+    """
+    def __init__(self, search_path=None, auto_reload=False,
+                 default_encoding=None, max_cache_size=25):
+        """Create the template laoder.
+        
+        @param search_path: a list of absolute path names that should be
+            searched for template files, or a string containing a single
+            absolute path
+        @param auto_reload: whether to check the last modification time of
+            template files, and reload them if they have changed
+        @param default_encoding: the default encoding to assume when loading
+            templates; defaults to UTF-8
+        @param max_cache_size: the maximum number of templates to keep in the
+            cache
+        """
+        self.search_path = search_path
+        if self.search_path is None:
+            self.search_path = []
+        elif isinstance(self.search_path, basestring):
+            self.search_path = [self.search_path]
+        self.auto_reload = auto_reload
+        self.default_encoding = default_encoding
+        self._cache = LRUCache(max_cache_size)
+        self._mtime = {}
+        self._lock = threading.Lock()
+
+    def load(self, filename, relative_to=None, cls=MarkupTemplate,
+             encoding=None):
+        """Load the template with the given name.
+        
+        If the `filename` parameter is relative, this method searches the search
+        path trying to locate a template matching the given name. If the file
+        name is an absolute path, the search path is not bypassed.
+        
+        If requested template is not found, a `TemplateNotFound` exception is
+        raised. Otherwise, a `Template` object is returned that represents the
+        parsed template.
+        
+        Template instances are cached to avoid having to parse the same
+        template file more than once. Thus, subsequent calls of this method
+        with the same template file name will return the same `Template`
+        object (unless the `auto_reload` option is enabled and the file was
+        changed since the last parse.)
+        
+        If the `relative_to` parameter is provided, the `filename` is
+        interpreted as being relative to that path.
+        
+        @param filename: the relative path of the template file to load
+        @param relative_to: the filename of the template from which the new
+            template is being loaded, or `None` if the template is being loaded
+            directly
+        @param cls: the class of the template object to instantiate
+        @param encoding: the encoding of the template to load; defaults to the
+            `default_encoding` of the loader instance
+        """
+        if encoding is None:
+            encoding = self.default_encoding
+        if relative_to and not os.path.isabs(relative_to):
+            filename = os.path.join(os.path.dirname(relative_to), filename)
+        filename = os.path.normpath(filename)
+
+        self._lock.acquire()
+        try:
+            # First check the cache to avoid reparsing the same file
+            try:
+                tmpl = self._cache[filename]
+                if not self.auto_reload or \
+                        os.path.getmtime(tmpl.filepath) == self._mtime[filename]:
+                    return tmpl
+            except KeyError:
+                pass
+
+            search_path = self.search_path
+            isabs = False
+
+            if os.path.isabs(filename):
+                # Bypass the search path if the requested filename is absolute
+                search_path = [os.path.dirname(filename)]
+                isabs = True
+
+            elif relative_to and os.path.isabs(relative_to):
+                # Make sure that the directory containing the including
+                # template is on the search path
+                dirname = os.path.dirname(relative_to)
+                if dirname not in search_path:
+                    search_path = search_path + [dirname]
+                isabs = True
+
+            elif not search_path:
+                # Uh oh, don't know where to look for the template
+                raise TemplateError('Search path for templates not configured')
+
+            for dirname in search_path:
+                filepath = os.path.join(dirname, filename)
+                try:
+                    fileobj = open(filepath, 'U')
+                    try:
+                        if isabs:
+                            # If the filename of either the included or the 
+                            # including template is absolute, make sure the
+                            # included template gets an absolute path, too,
+                            # so that nested include work properly without a
+                            # search path
+                            filename = os.path.join(dirname, filename)
+                            dirname = ''
+                        tmpl = cls(fileobj, basedir=dirname, filename=filename,
+                                   loader=self, encoding=encoding)
+                    finally:
+                        fileobj.close()
+                    self._cache[filename] = tmpl
+                    self._mtime[filename] = os.path.getmtime(filepath)
+                    return tmpl
+                except IOError:
+                    continue
+
+            raise TemplateNotFound(filename, search_path)
+
+        finally:
+            self._lock.release()
new file mode 100644
--- /dev/null
+++ b/genshi/template/markup.py
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Markup templating engine."""
+
+from itertools import chain
+
+from genshi.core import Attrs, Namespace, Stream
+from genshi.core import START, END, START_NS, END_NS, TEXT, COMMENT
+from genshi.filters import IncludeFilter
+from genshi.input import XMLParser
+from genshi.template.core import BadDirectiveError, Template, _apply_directives
+from genshi.template.core import SUB
+from genshi.template.directives import *
+
+
+class MarkupTemplate(Template):
+    """Implementation of the template language for XML-based templates.
+    
+    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
+    ...   <li py:for="item in items">${item}</li>
+    ... </ul>''')
+    >>> print tmpl.generate(items=[1, 2, 3])
+    <ul>
+      <li>1</li><li>2</li><li>3</li>
+    </ul>
+    """
+    NAMESPACE = Namespace('http://genshi.edgewall.org/')
+
+    directives = [('def', DefDirective),
+                  ('match', MatchDirective),
+                  ('when', WhenDirective),
+                  ('otherwise', OtherwiseDirective),
+                  ('for', ForDirective),
+                  ('if', IfDirective),
+                  ('choose', ChooseDirective),
+                  ('with', WithDirective),
+                  ('replace', ReplaceDirective),
+                  ('content', ContentDirective),
+                  ('attrs', AttrsDirective),
+                  ('strip', StripDirective)]
+
+    def __init__(self, source, basedir=None, filename=None, loader=None,
+                 encoding=None):
+        """Initialize a template from either a string or a file-like object."""
+        Template.__init__(self, source, basedir=basedir, filename=filename,
+                          loader=loader, encoding=encoding)
+
+        self.filters.append(self._match)
+        if loader:
+            self.filters.append(IncludeFilter(loader))
+
+    def _parse(self, encoding):
+        """Parse the template from an XML document."""
+        stream = [] # list of events of the "compiled" template
+        dirmap = {} # temporary mapping of directives to elements
+        ns_prefix = {}
+        depth = 0
+
+        for kind, data, pos in XMLParser(self.source, filename=self.filename,
+                                         encoding=encoding):
+
+            if kind is START_NS:
+                # Strip out the namespace declaration for template directives
+                prefix, uri = data
+                ns_prefix[prefix] = uri
+                if uri != self.NAMESPACE:
+                    stream.append((kind, data, pos))
+
+            elif kind is END_NS:
+                uri = ns_prefix.pop(data, None)
+                if uri and uri != self.NAMESPACE:
+                    stream.append((kind, data, pos))
+
+            elif kind is START:
+                # Record any directive attributes in start tags
+                tag, attrib = data
+                directives = []
+                strip = False
+
+                if tag in self.NAMESPACE:
+                    cls = self._dir_by_name.get(tag.localname)
+                    if cls is None:
+                        raise BadDirectiveError(tag.localname, self.filepath,
+                                                pos[1])
+                    value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '')
+                    directives.append(cls(value, ns_prefix, self.filepath,
+                                          pos[1], pos[2]))
+                    strip = True
+
+                new_attrib = []
+                for name, value in attrib:
+                    if name in self.NAMESPACE:
+                        cls = self._dir_by_name.get(name.localname)
+                        if cls is None:
+                            raise BadDirectiveError(name.localname,
+                                                    self.filepath, pos[1])
+                        directives.append(cls(value, ns_prefix, self.filepath,
+                                              pos[1], pos[2]))
+                    else:
+                        if value:
+                            value = list(self._interpolate(value, self.basedir,
+                                                           *pos))
+                            if len(value) == 1 and value[0][0] is TEXT:
+                                value = value[0][1]
+                        else:
+                            value = [(TEXT, u'', pos)]
+                        new_attrib.append((name, value))
+
+                if directives:
+                    index = self._dir_order.index
+                    directives.sort(lambda a, b: cmp(index(a.__class__),
+                                                     index(b.__class__)))
+                    dirmap[(depth, tag)] = (directives, len(stream), strip)
+
+                stream.append((kind, (tag, Attrs(new_attrib)), pos))
+                depth += 1
+
+            elif kind is END:
+                depth -= 1
+                stream.append((kind, data, pos))
+
+                # If there have have directive attributes with the corresponding
+                # start tag, move the events inbetween into a "subprogram"
+                if (depth, data) in dirmap:
+                    directives, start_offset, strip = dirmap.pop((depth, data))
+                    substream = stream[start_offset:]
+                    if strip:
+                        substream = substream[1:-1]
+                    stream[start_offset:] = [(SUB, (directives, substream),
+                                              pos)]
+
+            elif kind is TEXT:
+                for kind, data, pos in self._interpolate(data, self.basedir,
+                                                         *pos):
+                    stream.append((kind, data, pos))
+
+            elif kind is COMMENT:
+                if not data.lstrip().startswith('!'):
+                    stream.append((kind, data, pos))
+
+            else:
+                stream.append((kind, data, pos))
+
+        return stream
+
+    def _match(self, stream, ctxt, match_templates=None):
+        """Internal stream filter that applies any defined match templates
+        to the stream.
+        """
+        if match_templates is None:
+            match_templates = ctxt._match_templates
+
+        tail = []
+        def _strip(stream):
+            depth = 1
+            while 1:
+                event = stream.next()
+                if event[0] is START:
+                    depth += 1
+                elif event[0] is END:
+                    depth -= 1
+                if depth > 0:
+                    yield event
+                else:
+                    tail[:] = [event]
+                    break
+
+        for event in stream:
+
+            # We (currently) only care about start and end events for matching
+            # We might care about namespace events in the future, though
+            if not match_templates or (event[0] is not START and
+                                       event[0] is not END):
+                yield event
+                continue
+
+            for idx, (test, path, template, namespaces, directives) in \
+                    enumerate(match_templates):
+
+                if test(event, namespaces, ctxt) is True:
+
+                    # Let the remaining match templates know about the event so
+                    # they get a chance to update their internal state
+                    for test in [mt[0] for mt in match_templates[idx + 1:]]:
+                        test(event, namespaces, ctxt, updateonly=True)
+
+                    # Consume and store all events until an end event
+                    # corresponding to this start event is encountered
+                    content = chain([event], self._match(_strip(stream), ctxt),
+                                    tail)
+                    for filter_ in self.filters[3:]:
+                        content = filter_(content, ctxt)
+                    content = list(content)
+
+                    for test in [mt[0] for mt in match_templates]:
+                        test(tail[0], namespaces, ctxt, updateonly=True)
+
+                    # Make the select() function available in the body of the
+                    # match template
+                    def select(path):
+                        return Stream(content).select(path, namespaces, ctxt)
+                    ctxt.push(dict(select=select))
+
+                    # Recursively process the output
+                    template = _apply_directives(template, ctxt, directives)
+                    for event in self._match(self._eval(self._flatten(template,
+                                                                      ctxt),
+                                                        ctxt), ctxt,
+                                             match_templates[:idx] +
+                                             match_templates[idx + 1:]):
+                        yield event
+
+                    ctxt.pop()
+                    break
+
+            else: # no matches
+                yield event
new file mode 100644
--- /dev/null
+++ b/genshi/template/plugin.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006 Matthew Good
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Basic support for the template engine plugin API used by TurboGears and
+CherryPy/Buffet.
+"""
+
+from pkg_resources import resource_filename
+
+from genshi.eval import Undefined
+from genshi.input import ET, HTML, XML
+from genshi.output import DocType
+from genshi.template.core import Context, Template
+from genshi.template.loader import TemplateLoader
+from genshi.template.markup import MarkupTemplate
+from genshi.template.text import TextTemplate
+
+
+class ConfigurationError(Exception):
+    """Exception raised when invalid plugin options are encountered."""
+
+
+class AbstractTemplateEnginePlugin(object):
+    """Implementation of the plugin API."""
+
+    template_class = None
+    extension = None
+
+    def __init__(self, extra_vars_func=None, options=None):
+        self.get_extra_vars = extra_vars_func
+        if options is None:
+            options = {}
+        self.options = options
+
+        self.default_encoding = options.get('genshi.default_encoding', 'utf-8')
+        auto_reload = options.get('genshi.auto_reload', '1').lower() \
+                            in ('1', 'yes', 'true')
+        search_path = options.get('genshi.search_path', '').split(':')
+        try:
+            max_cache_size = int(options.get('genshi.max_cache_size', 25))
+        except ValueError:
+            raise ConfigurationError('Invalid value for max_cache_size: "%s"' %
+                                     max_cache_size)
+
+        self.loader = TemplateLoader(filter(None, search_path),
+                                     auto_reload=auto_reload,
+                                     max_cache_size=max_cache_size)
+
+    def load_template(self, templatename, template_string=None):
+        """Find a template specified in python 'dot' notation, or load one from
+        a string.
+        """
+        if template_string is not None:
+            return self.template_class(template_string)
+
+        divider = templatename.rfind('.')
+        if divider >= 0:
+            package = templatename[:divider]
+            basename = templatename[divider + 1:] + self.extension
+            templatename = resource_filename(package, basename)
+
+        return self.loader.load(templatename, cls=self.template_class)
+
+    def _get_render_options(self, format=None):
+        if format is None:
+            format = self.default_format
+        kwargs = {'method': format}
+        if self.default_encoding:
+            kwargs['encoding'] = self.default_encoding
+        return kwargs
+
+    def render(self, info, format=None, fragment=False, template=None):
+        """Render the template to a string using the provided info."""
+        kwargs = self._get_render_options(format=format)
+        return self.transform(info, template).render(**kwargs)
+
+    def transform(self, info, template):
+        """Render the output to an event stream."""
+        if not isinstance(template, Template):
+            template = self.load_template(template)
+        ctxt = Context(**info)
+
+        # Some functions for Kid compatibility
+        def defined(name):
+            return ctxt.get(name, Undefined) is not Undefined
+        ctxt['defined'] = defined
+        def value_of(name, default=None):
+            return ctxt.get(name, default)
+        ctxt['value_of'] = value_of
+
+        return template.generate(ctxt)
+
+
+class MarkupTemplateEnginePlugin(AbstractTemplateEnginePlugin):
+    """Implementation of the plugin API for markup templates."""
+
+    template_class = MarkupTemplate
+    extension = '.html'
+
+    doctypes = {'html': DocType.HTML, 'html-strict': DocType.HTML_STRICT,
+                'html-transitional': DocType.HTML_TRANSITIONAL,
+                'xhtml': DocType.XHTML, 'xhtml-strict': DocType.XHTML_STRICT,
+                'xhtml-transitional': DocType.XHTML_TRANSITIONAL}
+
+    def __init__(self, extra_vars_func=None, options=None):
+        AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
+
+        doctype = options.get('genshi.default_doctype')
+        if doctype and doctype not in self.doctypes:
+            raise ConfigurationError('Unknown doctype "%s"' % doctype)
+        self.default_doctype = self.doctypes.get(doctype)
+
+        format = options.get('genshi.default_format', 'html')
+        if format not in ('html', 'xhtml', 'xml', 'text'):
+            raise ConfigurationError('Unknown output format "%s"' % format)
+        self.default_format = format
+
+    def _get_render_options(self, format=None):
+        kwargs = super(MarkupTemplateEnginePlugin,
+                       self)._get_render_options(format)
+        if self.default_doctype:
+            kwargs['doctype'] = self.default_doctype
+        return kwargs
+
+    def transform(self, info, template):
+        """Render the output to an event stream."""
+        data = {'ET': ET, 'HTML': HTML, 'XML': XML}
+        if self.get_extra_vars:
+            data.update(self.get_extra_vars())
+        data.update(info)
+        return super(MarkupTemplateEnginePlugin, self).transform(data, template)
+
+
+class TextTemplateEnginePlugin(AbstractTemplateEnginePlugin):
+    """Implementation of the plugin API for text templates."""
+
+    template_class = TextTemplate
+    extension = '.txt'
+    default_format = 'text'
+
+    def transform(self, info, template):
+        """Render the output to an event stream."""
+        data = {}
+        if self.get_extra_vars:
+            data.update(self.get_extra_vars())
+        data.update(info)
+        return super(TextTemplateEnginePlugin, self).transform(data, template)
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/__init__.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+import doctest
+import unittest
+
+
+def suite():
+    from genshi.template.tests import core, directives, eval, loader, markup, \
+                                      text
+    suite = unittest.TestSuite()
+    suite.addTest(core.suite())
+    suite.addTest(directives.suite())
+    suite.addTest(eval.suite())
+    suite.addTest(loader.suite())
+    suite.addTest(markup.suite())
+    suite.addTest(text.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/core.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from genshi.core import Stream
+from genshi.template.core import Template
+
+
+class TemplateTestCase(unittest.TestCase):
+    """Tests for basic template processing, expression evaluation and error
+    reporting.
+    """
+
+    def test_interpolate_string(self):
+        parts = list(Template._interpolate('bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Stream.TEXT, parts[0][0])
+        self.assertEqual('bla', parts[0][1])
+
+    def test_interpolate_simple(self):
+        parts = list(Template._interpolate('${bla}'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Template.EXPR, parts[0][0])
+        self.assertEqual('bla', parts[0][1].source)
+
+    def test_interpolate_escaped(self):
+        parts = list(Template._interpolate('$${bla}'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Stream.TEXT, parts[0][0])
+        self.assertEqual('${bla}', parts[0][1])
+
+    def test_interpolate_short(self):
+        parts = list(Template._interpolate('$bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Template.EXPR, parts[0][0])
+        self.assertEqual('bla', parts[0][1].source)
+
+    def test_interpolate_short_starting_with_underscore(self):
+        parts = list(Template._interpolate('$_bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Template.EXPR, parts[0][0])
+        self.assertEqual('_bla', parts[0][1].source)
+
+    def test_interpolate_short_containing_underscore(self):
+        parts = list(Template._interpolate('$foo_bar'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Template.EXPR, parts[0][0])
+        self.assertEqual('foo_bar', parts[0][1].source)
+
+    def test_interpolate_short_starting_with_dot(self):
+        parts = list(Template._interpolate('$.bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Stream.TEXT, parts[0][0])
+        self.assertEqual('$.bla', parts[0][1])
+
+    def test_interpolate_short_containing_dot(self):
+        parts = list(Template._interpolate('$foo.bar'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Template.EXPR, parts[0][0])
+        self.assertEqual('foo.bar', parts[0][1].source)
+
+    def test_interpolate_short_starting_with_digit(self):
+        parts = list(Template._interpolate('$0bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Stream.TEXT, parts[0][0])
+        self.assertEqual('$0bla', parts[0][1])
+
+    def test_interpolate_short_containing_digit(self):
+        parts = list(Template._interpolate('$foo0'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Template.EXPR, parts[0][0])
+        self.assertEqual('foo0', parts[0][1].source)
+
+    def test_interpolate_mixed1(self):
+        parts = list(Template._interpolate('$foo bar $baz'))
+        self.assertEqual(3, len(parts))
+        self.assertEqual(Template.EXPR, parts[0][0])
+        self.assertEqual('foo', parts[0][1].source)
+        self.assertEqual(Stream.TEXT, parts[1][0])
+        self.assertEqual(' bar ', parts[1][1])
+        self.assertEqual(Template.EXPR, parts[2][0])
+        self.assertEqual('baz', parts[2][1].source)
+
+    def test_interpolate_mixed2(self):
+        parts = list(Template._interpolate('foo $bar baz'))
+        self.assertEqual(3, len(parts))
+        self.assertEqual(Stream.TEXT, parts[0][0])
+        self.assertEqual('foo ', parts[0][1])
+        self.assertEqual(Template.EXPR, parts[1][0])
+        self.assertEqual('bar', parts[1][1].source)
+        self.assertEqual(Stream.TEXT, parts[2][0])
+        self.assertEqual(' baz', parts[2][1])
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(Template.__module__))
+    suite.addTest(unittest.makeSuite(TemplateTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/directives.py
@@ -0,0 +1,910 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+import doctest
+import sys
+import unittest
+
+from genshi.template import directives, MarkupTemplate, TextTemplate, \
+                            TemplateRuntimeError
+
+
+class AttrsDirectiveTestCase(unittest.TestCase):
+    """Tests for the `py:attrs` template directive."""
+
+    def test_combined_with_loop(self):
+        """
+        Verify that the directive has access to the loop variables.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <elem py:for="item in items" py:attrs="item"/>
+        </doc>""")
+        items = [{'id': 1, 'class': 'foo'}, {'id': 2, 'class': 'bar'}]
+        self.assertEqual("""<doc>
+          <elem id="1" class="foo"/><elem id="2" class="bar"/>
+        </doc>""", str(tmpl.generate(items=items)))
+
+    def test_update_existing_attr(self):
+        """
+        Verify that an attribute value that evaluates to `None` removes an
+        existing attribute of that name.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <elem class="foo" py:attrs="{'class': 'bar'}"/>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <elem class="bar"/>
+        </doc>""", str(tmpl.generate()))
+
+    def test_remove_existing_attr(self):
+        """
+        Verify that an attribute value that evaluates to `None` removes an
+        existing attribute of that name.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <elem class="foo" py:attrs="{'class': None}"/>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <elem/>
+        </doc>""", str(tmpl.generate()))
+
+
+class ChooseDirectiveTestCase(unittest.TestCase):
+    """Tests for the `py:choose` template directive and the complementary
+    directives `py:when` and `py:otherwise`."""
+
+    def test_multiple_true_whens(self):
+        """
+        Verify that, if multiple `py:when` bodies match, only the first is
+        output.
+        """
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/" py:choose="">
+          <span py:when="1 == 1">1</span>
+          <span py:when="2 == 2">2</span>
+          <span py:when="3 == 3">3</span>
+        </div>""")
+        self.assertEqual("""<div>
+          <span>1</span>
+        </div>""", str(tmpl.generate()))
+
+    def test_otherwise(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/" py:choose="">
+          <span py:when="False">hidden</span>
+          <span py:otherwise="">hello</span>
+        </div>""")
+        self.assertEqual("""<div>
+          <span>hello</span>
+        </div>""", str(tmpl.generate()))
+
+    def test_nesting(self):
+        """
+        Verify that `py:choose` blocks can be nested:
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:choose="1">
+            <div py:when="1" py:choose="3">
+              <span py:when="2">2</span>
+              <span py:when="3">3</span>
+            </div>
+          </div>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <div>
+            <div>
+              <span>3</span>
+            </div>
+          </div>
+        </doc>""", str(tmpl.generate()))
+
+    def test_complex_nesting(self):
+        """
+        Verify more complex nesting.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:choose="1">
+            <div py:when="1" py:choose="">
+              <span py:when="2">OK</span>
+              <span py:when="1">FAIL</span>
+            </div>
+          </div>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <div>
+            <div>
+              <span>OK</span>
+            </div>
+          </div>
+        </doc>""", str(tmpl.generate()))
+
+    def test_complex_nesting_otherwise(self):
+        """
+        Verify more complex nesting using otherwise.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:choose="1">
+            <div py:when="1" py:choose="2">
+              <span py:when="1">FAIL</span>
+              <span py:otherwise="">OK</span>
+            </div>
+          </div>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <div>
+            <div>
+              <span>OK</span>
+            </div>
+          </div>
+        </doc>""", str(tmpl.generate()))
+
+    def test_when_with_strip(self):
+        """
+        Verify that a when directive with a strip directive actually strips of
+        the outer element.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:choose="" py:strip="">
+            <span py:otherwise="">foo</span>
+          </div>
+        </doc>""")
+        self.assertEqual("""<doc>
+            <span>foo</span>
+        </doc>""", str(tmpl.generate()))
+
+    def test_when_outside_choose(self):
+        """
+        Verify that a `when` directive outside of a `choose` directive is
+        reported as an error.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:when="xy" />
+        </doc>""")
+        self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
+
+    def test_otherwise_outside_choose(self):
+        """
+        Verify that an `otherwise` directive outside of a `choose` directive is
+        reported as an error.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:otherwise="" />
+        </doc>""")
+        self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
+
+    def test_when_without_test(self):
+        """
+        Verify that an `when` directive that doesn't have a `test` attribute
+        is reported as an error.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:choose="" py:strip="">
+            <py:when>foo</py:when>
+          </div>
+        </doc>""")
+        self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
+
+    def test_when_without_test_but_with_choose_value(self):
+        """
+        Verify that an `when` directive that doesn't have a `test` attribute
+        works as expected as long as the parent `choose` directive has a test
+        expression.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:choose="foo" py:strip="">
+            <py:when>foo</py:when>
+          </div>
+        </doc>""")
+        self.assertEqual("""<doc>
+            foo
+        </doc>""", str(tmpl.generate(foo='Yeah')))
+
+    def test_otherwise_without_test(self):
+        """
+        Verify that an `otherwise` directive can be used without a `test`
+        attribute.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:choose="" py:strip="">
+            <py:otherwise>foo</py:otherwise>
+          </div>
+        </doc>""")
+        self.assertEqual("""<doc>
+            foo
+        </doc>""", str(tmpl.generate()))
+
+    def test_as_element(self):
+        """
+        Verify that the directive can also be used as an element.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:choose>
+            <py:when test="1 == 1">1</py:when>
+            <py:when test="2 == 2">2</py:when>
+            <py:when test="3 == 3">3</py:when>
+          </py:choose>
+        </doc>""")
+        self.assertEqual("""<doc>
+            1
+        </doc>""", str(tmpl.generate()))
+
+    def test_in_text_template(self):
+        """
+        Verify that the directive works as expected in a text template.
+        """
+        tmpl = TextTemplate("""#choose
+          #when 1 == 1
+            1
+          #end
+          #when 2 == 2
+            2
+          #end
+          #when 3 == 3
+            3
+          #end
+        #end""")
+        self.assertEqual("""            1\n""", str(tmpl.generate()))
+
+
+class DefDirectiveTestCase(unittest.TestCase):
+    """Tests for the `py:def` template directive."""
+
+    def test_function_with_strip(self):
+        """
+        Verify that a named template function with a strip directive actually
+        strips of the outer element.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:def="echo(what)" py:strip="">
+            <b>${what}</b>
+          </div>
+          ${echo('foo')}
+        </doc>""")
+        self.assertEqual("""<doc>
+            <b>foo</b>
+        </doc>""", str(tmpl.generate()))
+
+    def test_exec_in_replace(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <p py:def="echo(greeting, name='world')" class="message">
+            ${greeting}, ${name}!
+          </p>
+          <div py:replace="echo('hello')"></div>
+        </div>""")
+        self.assertEqual("""<div>
+          <p class="message">
+            hello, world!
+          </p>
+        </div>""", str(tmpl.generate()))
+
+    def test_as_element(self):
+        """
+        Verify that the directive can also be used as an element.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:def function="echo(what)">
+            <b>${what}</b>
+          </py:def>
+          ${echo('foo')}
+        </doc>""")
+        self.assertEqual("""<doc>
+            <b>foo</b>
+        </doc>""", str(tmpl.generate()))
+
+    def test_nested_defs(self):
+        """
+        Verify that a template function defined inside a conditional block can
+        be called from outside that block.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:if test="semantic">
+            <strong py:def="echo(what)">${what}</strong>
+          </py:if>
+          <py:if test="not semantic">
+            <b py:def="echo(what)">${what}</b>
+          </py:if>
+          ${echo('foo')}
+        </doc>""")
+        self.assertEqual("""<doc>
+          <strong>foo</strong>
+        </doc>""", str(tmpl.generate(semantic=True)))
+
+    def test_function_with_default_arg(self):
+        """
+        Verify that keyword arguments work with `py:def` directives.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <b py:def="echo(what, bold=False)" py:strip="not bold">${what}</b>
+          ${echo('foo')}
+        </doc>""")
+        self.assertEqual("""<doc>
+          foo
+        </doc>""", str(tmpl.generate()))
+
+    def test_invocation_in_attribute(self):
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:def function="echo(what)">${what or 'something'}</py:def>
+          <p class="${echo('foo')}">bar</p>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <p class="foo">bar</p>
+        </doc>""", str(tmpl.generate()))
+
+    def test_invocation_in_attribute_none(self):
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:def function="echo()">${None}</py:def>
+          <p class="${echo()}">bar</p>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <p>bar</p>
+        </doc>""", str(tmpl.generate()))
+
+    def test_function_raising_typeerror(self):
+        def badfunc():
+            raise TypeError
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <div py:def="dobadfunc()">
+            ${badfunc()}
+          </div>
+          <div py:content="dobadfunc()"/>
+        </html>""")
+        self.assertRaises(TypeError, list, tmpl.generate(badfunc=badfunc))
+
+    def test_def_in_matched(self):
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <head py:match="head">${select('*')}</head>
+          <head>
+            <py:def function="maketitle(test)"><b py:replace="test" /></py:def>
+            <title>${maketitle(True)}</title>
+          </head>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <head><title>True</title></head>
+        </doc>""", str(tmpl.generate()))
+
+    def test_in_text_template(self):
+        """
+        Verify that the directive works as expected in a text template.
+        """
+        tmpl = TextTemplate("""
+          #def echo(greeting, name='world')
+            ${greeting}, ${name}!
+          #end
+          ${echo('Hi', name='you')}
+        """)
+        self.assertEqual("""                      Hi, you!
+        """, str(tmpl.generate()))
+
+
+class ForDirectiveTestCase(unittest.TestCase):
+    """Tests for the `py:for` template directive."""
+
+    def test_loop_with_strip(self):
+        """
+        Verify that the combining the `py:for` directive with `py:strip` works
+        correctly.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:for="item in items" py:strip="">
+            <b>${item}</b>
+          </div>
+        </doc>""")
+        self.assertEqual("""<doc>
+            <b>1</b>
+            <b>2</b>
+            <b>3</b>
+            <b>4</b>
+            <b>5</b>
+        </doc>""", str(tmpl.generate(items=range(1, 6))))
+
+    def test_as_element(self):
+        """
+        Verify that the directive can also be used as an element.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:for each="item in items">
+            <b>${item}</b>
+          </py:for>
+        </doc>""")
+        self.assertEqual("""<doc>
+            <b>1</b>
+            <b>2</b>
+            <b>3</b>
+            <b>4</b>
+            <b>5</b>
+        </doc>""", str(tmpl.generate(items=range(1, 6))))
+
+    def test_multi_assignment(self):
+        """
+        Verify that assignment to tuples works correctly.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:for each="k, v in items">
+            <p>key=$k, value=$v</p>
+          </py:for>
+        </doc>""")
+        self.assertEqual("""<doc>
+            <p>key=a, value=1</p>
+            <p>key=b, value=2</p>
+        </doc>""", str(tmpl.generate(items=dict(a=1, b=2).items())))
+
+    def test_nested_assignment(self):
+        """
+        Verify that assignment to nested tuples works correctly.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:for each="idx, (k, v) in items">
+            <p>$idx: key=$k, value=$v</p>
+          </py:for>
+        </doc>""")
+        self.assertEqual("""<doc>
+            <p>0: key=a, value=1</p>
+            <p>1: key=b, value=2</p>
+        </doc>""", str(tmpl.generate(items=enumerate(dict(a=1, b=2).items()))))
+
+    def test_not_iterable(self):
+        """
+        Verify that assignment to nested tuples works correctly.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:for each="item in foo">
+            $item
+          </py:for>
+        </doc>""", filename='test.html')
+        try:
+            list(tmpl.generate(foo=12))
+        except TemplateRuntimeError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(2, e.lineno)
+
+
+class IfDirectiveTestCase(unittest.TestCase):
+    """Tests for the `py:if` template directive."""
+
+    def test_loop_with_strip(self):
+        """
+        Verify that the combining the `py:if` directive with `py:strip` works
+        correctly.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <b py:if="foo" py:strip="">${bar}</b>
+        </doc>""")
+        self.assertEqual("""<doc>
+          Hello
+        </doc>""", str(tmpl.generate(foo=True, bar='Hello')))
+
+    def test_as_element(self):
+        """
+        Verify that the directive can also be used as an element.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:if test="foo">${bar}</py:if>
+        </doc>""")
+        self.assertEqual("""<doc>
+          Hello
+        </doc>""", str(tmpl.generate(foo=True, bar='Hello')))
+
+
+class MatchDirectiveTestCase(unittest.TestCase):
+    """Tests for the `py:match` template directive."""
+
+    def test_with_strip(self):
+        """
+        Verify that a match template can produce the same kind of element that
+        it matched without entering an infinite recursion.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <elem py:match="elem" py:strip="">
+            <div class="elem">${select('text()')}</div>
+          </elem>
+          <elem>Hey Joe</elem>
+        </doc>""")
+        self.assertEqual("""<doc>
+            <div class="elem">Hey Joe</div>
+        </doc>""", str(tmpl.generate()))
+
+    def test_without_strip(self):
+        """
+        Verify that a match template can produce the same kind of element that
+        it matched without entering an infinite recursion.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <elem py:match="elem">
+            <div class="elem">${select('text()')}</div>
+          </elem>
+          <elem>Hey Joe</elem>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <elem>
+            <div class="elem">Hey Joe</div>
+          </elem>
+        </doc>""", str(tmpl.generate()))
+
+    def test_as_element(self):
+        """
+        Verify that the directive can also be used as an element.
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:match path="elem">
+            <div class="elem">${select('text()')}</div>
+          </py:match>
+          <elem>Hey Joe</elem>
+        </doc>""")
+        self.assertEqual("""<doc>
+            <div class="elem">Hey Joe</div>
+        </doc>""", str(tmpl.generate()))
+
+    def test_recursive_match_1(self):
+        """
+        Match directives are applied recursively, meaning that they are also
+        applied to any content they may have produced themselves:
+        """
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <elem py:match="elem">
+            <div class="elem">
+              ${select('*')}
+            </div>
+          </elem>
+          <elem>
+            <subelem>
+              <elem/>
+            </subelem>
+          </elem>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <elem>
+            <div class="elem">
+              <subelem>
+              <elem>
+            <div class="elem">
+            </div>
+          </elem>
+            </subelem>
+            </div>
+          </elem>
+        </doc>""", str(tmpl.generate()))
+
+    def test_recursive_match_2(self):
+        """
+        When two or more match templates match the same element and also
+        themselves output the element they match, avoiding recursion is even
+        more complex, but should work.
+        """
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <body py:match="body">
+            <div id="header"/>
+            ${select('*')}
+          </body>
+          <body py:match="body">
+            ${select('*')}
+            <div id="footer"/>
+          </body>
+          <body>
+            <h1>Foo</h1>
+          </body>
+        </html>""")
+        self.assertEqual("""<html>
+          <body>
+            <div id="header"/><h1>Foo</h1>
+            <div id="footer"/>
+          </body>
+        </html>""", str(tmpl.generate()))
+
+    def test_select_all_attrs(self):
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:match="elem" py:attrs="select('@*')">
+            ${select('text()')}
+          </div>
+          <elem id="joe">Hey Joe</elem>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <div id="joe">
+            Hey Joe
+          </div>
+        </doc>""", str(tmpl.generate()))
+
+    def test_select_all_attrs_empty(self):
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:match="elem" py:attrs="select('@*')">
+            ${select('text()')}
+          </div>
+          <elem>Hey Joe</elem>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <div>
+            Hey Joe
+          </div>
+        </doc>""", str(tmpl.generate()))
+
+    def test_select_all_attrs_in_body(self):
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <div py:match="elem">
+            Hey ${select('text()')} ${select('@*')}
+          </div>
+          <elem title="Cool">Joe</elem>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <div>
+            Hey Joe Cool
+          </div>
+        </doc>""", str(tmpl.generate()))
+
+    def test_def_in_match(self):
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
+          <py:def function="maketitle(test)"><b py:replace="test" /></py:def>
+          <head py:match="head">${select('*')}</head>
+          <head><title>${maketitle(True)}</title></head>
+        </doc>""")
+        self.assertEqual("""<doc>
+          <head><title>True</title></head>
+        </doc>""", str(tmpl.generate()))
+
+    def test_match_with_xpath_variable(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <span py:match="*[name()=$tagname]">
+            Hello ${select('@name')}
+          </span>
+          <greeting name="Dude"/>
+        </div>""")
+        self.assertEqual("""<div>
+          <span>
+            Hello Dude
+          </span>
+        </div>""", str(tmpl.generate(tagname='greeting')))
+        self.assertEqual("""<div>
+          <greeting name="Dude"/>
+        </div>""", str(tmpl.generate(tagname='sayhello')))
+
+    def test_content_directive_in_match(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <div py:match="foo">I said <q py:content="select('text()')">something</q>.</div>
+          <foo>bar</foo>
+        </html>""")
+        self.assertEqual("""<html>
+          <div>I said <q>bar</q>.</div>
+        </html>""", str(tmpl.generate()))
+
+    def test_cascaded_matches(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <body py:match="body">${select('*')}</body>
+          <head py:match="head">${select('title')}</head>
+          <body py:match="body">${select('*')}<hr /></body>
+          <head><title>Welcome to Markup</title></head>
+          <body><h2>Are you ready to mark up?</h2></body>
+        </html>""")
+        self.assertEqual("""<html>
+          <head><title>Welcome to Markup</title></head>
+          <body><h2>Are you ready to mark up?</h2><hr/></body>
+        </html>""", str(tmpl.generate()))
+
+    def test_multiple_matches(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <input py:match="form//input" py:attrs="select('@*')"
+                 value="${values[str(select('@name'))]}" />
+          <form><p py:for="field in fields">
+            <label>${field.capitalize()}</label>
+            <input type="text" name="${field}" />
+          </p></form>
+        </html>""")
+        fields = ['hello_%s' % i for i in range(5)]
+        values = dict([('hello_%s' % i, i) for i in range(5)])
+        self.assertEqual("""<html>
+          <form><p>
+            <label>Hello_0</label>
+            <input value="0" type="text" name="hello_0"/>
+          </p><p>
+            <label>Hello_1</label>
+            <input value="1" type="text" name="hello_1"/>
+          </p><p>
+            <label>Hello_2</label>
+            <input value="2" type="text" name="hello_2"/>
+          </p><p>
+            <label>Hello_3</label>
+            <input value="3" type="text" name="hello_3"/>
+          </p><p>
+            <label>Hello_4</label>
+            <input value="4" type="text" name="hello_4"/>
+          </p></form>
+        </html>""", str(tmpl.generate(fields=fields, values=values)))
+
+    def test_namespace_context(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+                                       xmlns:x="http://www.example.org/">
+          <div py:match="x:foo">Foo</div>
+          <foo xmlns="http://www.example.org/"/>
+        </html>""")
+        # FIXME: there should be a way to strip out unwanted/unused namespaces,
+        #        such as the "x" in this example
+        self.assertEqual("""<html xmlns:x="http://www.example.org/">
+          <div>Foo</div>
+        </html>""", str(tmpl.generate()))
+
+    def test_match_with_position_predicate(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <p py:match="body/p[1]" class="first">${select('*|text()')}</p>
+          <body>
+            <p>Foo</p>
+            <p>Bar</p>
+          </body>
+        </html>""")
+        self.assertEqual("""<html>
+          <body>
+            <p class="first">Foo</p>
+            <p>Bar</p>
+          </body>
+        </html>""", str(tmpl.generate()))
+
+    def test_match_with_closure(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <p py:match="body//p" class="para">${select('*|text()')}</p>
+          <body>
+            <p>Foo</p>
+            <div><p>Bar</p></div>
+          </body>
+        </html>""")
+        self.assertEqual("""<html>
+          <body>
+            <p class="para">Foo</p>
+            <div><p class="para">Bar</p></div>
+          </body>
+        </html>""", str(tmpl.generate()))
+
+    def test_match_without_closure(self):
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
+          <p py:match="body/p" class="para">${select('*|text()')}</p>
+          <body>
+            <p>Foo</p>
+            <div><p>Bar</p></div>
+          </body>
+        </html>""")
+        self.assertEqual("""<html>
+          <body>
+            <p class="para">Foo</p>
+            <div><p>Bar</p></div>
+          </body>
+        </html>""", str(tmpl.generate()))
+
+    # FIXME
+    #def test_match_after_step(self):
+    #    tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+    #      <span py:match="div/greeting">
+    #        Hello ${select('@name')}
+    #      </span>
+    #      <greeting name="Dude" />
+    #    </div>""")
+    #    self.assertEqual("""<div>
+    #      <span>
+    #        Hello Dude
+    #      </span>
+    #    </div>""", str(tmpl.generate()))
+
+
+class StripDirectiveTestCase(unittest.TestCase):
+    """Tests for the `py:strip` template directive."""
+
+    def test_strip_false(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <div py:strip="False"><b>foo</b></div>
+        </div>""")
+        self.assertEqual("""<div>
+          <div><b>foo</b></div>
+        </div>""", str(tmpl.generate()))
+
+    def test_strip_empty(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <div py:strip=""><b>foo</b></div>
+        </div>""")
+        self.assertEqual("""<div>
+          <b>foo</b>
+        </div>""", str(tmpl.generate()))
+
+
+class WithDirectiveTestCase(unittest.TestCase):
+    """Tests for the `py:with` template directive."""
+
+    def test_shadowing(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          ${x}
+          <span py:with="x = x * 2" py:replace="x"/>
+          ${x}
+        </div>""")
+        self.assertEqual("""<div>
+          42
+          84
+          42
+        </div>""", str(tmpl.generate(x=42)))
+
+    def test_as_element(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <py:with vars="x = x * 2">${x}</py:with>
+        </div>""")
+        self.assertEqual("""<div>
+          84
+        </div>""", str(tmpl.generate(x=42)))
+
+    def test_multiple_vars_same_name(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <py:with vars="
+            foo = 'bar';
+            foo = foo.replace('r', 'z')
+          ">
+            $foo
+          </py:with>
+        </div>""")
+        self.assertEqual("""<div>
+            baz
+        </div>""", str(tmpl.generate(x=42)))
+
+    def test_multiple_vars_single_assignment(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <py:with vars="x = y = z = 1">${x} ${y} ${z}</py:with>
+        </div>""")
+        self.assertEqual("""<div>
+          1 1 1
+        </div>""", str(tmpl.generate(x=42)))
+
+    def test_nested_vars_single_assignment(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <py:with vars="x, (y, z) = (1, (2, 3))">${x} ${y} ${z}</py:with>
+        </div>""")
+        self.assertEqual("""<div>
+          1 2 3
+        </div>""", str(tmpl.generate(x=42)))
+
+    def test_multiple_vars_trailing_semicolon(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <py:with vars="x = x * 2; y = x / 2;">${x} ${y}</py:with>
+        </div>""")
+        self.assertEqual("""<div>
+          84 42
+        </div>""", str(tmpl.generate(x=42)))
+
+    def test_semicolon_escape(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <py:with vars="x = 'here is a semicolon: ;'; y = 'here are two semicolons: ;;' ;">
+            ${x}
+            ${y}
+          </py:with>
+        </div>""")
+        self.assertEqual("""<div>
+            here is a semicolon: ;
+            here are two semicolons: ;;
+        </div>""", str(tmpl.generate()))
+
+    def test_unicode_expr(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <span py:with="weeks=(u'一', u'二', u'三', u'四', u'五', u'六', u'日')">
+            $weeks
+          </span>
+        </div>""")
+        self.assertEqual("""<div>
+          <span>
+            一二三四五六日
+          </span>
+        </div>""", str(tmpl.generate()))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(directives))
+    suite.addTest(unittest.makeSuite(AttrsDirectiveTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(ChooseDirectiveTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(DefDirectiveTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(ForDirectiveTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(IfDirectiveTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(MatchDirectiveTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(StripDirectiveTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(WithDirectiveTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/eval.py
@@ -0,0 +1,389 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+import doctest
+import sys
+import unittest
+
+from genshi.template.eval import Expression, Undefined
+
+
+class ExpressionTestCase(unittest.TestCase):
+
+    def test_name_lookup(self):
+        self.assertEqual('bar', Expression('foo').evaluate({'foo': 'bar'}))
+        self.assertEqual(id, Expression('id').evaluate({}, nocall=True))
+        self.assertEqual('bar', Expression('id').evaluate({'id': 'bar'}))
+        self.assertEqual(None, Expression('id').evaluate({'id': None},
+                                                         nocall=True))
+
+    def test_str_literal(self):
+        self.assertEqual('foo', Expression('"foo"').evaluate({}))
+        self.assertEqual('foo', Expression('"""foo"""').evaluate({}))
+        self.assertEqual('foo', Expression("'foo'").evaluate({}))
+        self.assertEqual('foo', Expression("'''foo'''").evaluate({}))
+        self.assertEqual('foo', Expression("u'foo'").evaluate({}))
+        self.assertEqual('foo', Expression("r'foo'").evaluate({}))
+
+    def test_str_literal_non_ascii(self):
+        expr = Expression(u"u'\xfe'")
+        self.assertEqual(u'þ', expr.evaluate({}))
+        expr = Expression("u'\xfe'")
+        self.assertEqual(u'þ', expr.evaluate({}))
+        expr = Expression("'\xc3\xbe'")
+        self.assertEqual(u'þ', expr.evaluate({}))
+
+    def test_num_literal(self):
+        self.assertEqual(42, Expression("42").evaluate({}))
+        self.assertEqual(42L, Expression("42L").evaluate({}))
+        self.assertEqual(.42, Expression(".42").evaluate({}))
+        self.assertEqual(07, Expression("07").evaluate({}))
+        self.assertEqual(0xF2, Expression("0xF2").evaluate({}))
+        self.assertEqual(0XF2, Expression("0XF2").evaluate({}))
+
+    def test_dict_literal(self):
+        self.assertEqual({}, Expression("{}").evaluate({}))
+        self.assertEqual({'key': True},
+                         Expression("{'key': value}").evaluate({'value': True}))
+
+    def test_list_literal(self):
+        self.assertEqual([], Expression("[]").evaluate({}))
+        self.assertEqual([1, 2, 3], Expression("[1, 2, 3]").evaluate({}))
+        self.assertEqual([True],
+                         Expression("[value]").evaluate({'value': True}))
+
+    def test_tuple_literal(self):
+        self.assertEqual((), Expression("()").evaluate({}))
+        self.assertEqual((1, 2, 3), Expression("(1, 2, 3)").evaluate({}))
+        self.assertEqual((True,),
+                         Expression("(value,)").evaluate({'value': True}))
+
+    def test_unaryop_pos(self):
+        self.assertEqual(1, Expression("+1").evaluate({}))
+        self.assertEqual(1, Expression("+x").evaluate({'x': 1}))
+
+    def test_unaryop_neg(self):
+        self.assertEqual(-1, Expression("-1").evaluate({}))
+        self.assertEqual(-1, Expression("-x").evaluate({'x': 1}))
+
+    def test_unaryop_not(self):
+        self.assertEqual(False, Expression("not True").evaluate({}))
+        self.assertEqual(False, Expression("not x").evaluate({'x': True}))
+
+    def test_unaryop_inv(self):
+        self.assertEqual(-2, Expression("~1").evaluate({}))
+        self.assertEqual(-2, Expression("~x").evaluate({'x': 1}))
+
+    def test_binop_add(self):
+        self.assertEqual(3, Expression("2 + 1").evaluate({}))
+        self.assertEqual(3, Expression("x + y").evaluate({'x': 2, 'y': 1}))
+
+    def test_binop_sub(self):
+        self.assertEqual(1, Expression("2 - 1").evaluate({}))
+        self.assertEqual(1, Expression("x - y").evaluate({'x': 1, 'y': 1}))
+
+    def test_binop_sub(self):
+        self.assertEqual(1, Expression("2 - 1").evaluate({}))
+        self.assertEqual(1, Expression("x - y").evaluate({'x': 2, 'y': 1}))
+
+    def test_binop_mul(self):
+        self.assertEqual(4, Expression("2 * 2").evaluate({}))
+        self.assertEqual(4, Expression("x * y").evaluate({'x': 2, 'y': 2}))
+
+    def test_binop_pow(self):
+        self.assertEqual(4, Expression("2 ** 2").evaluate({}))
+        self.assertEqual(4, Expression("x ** y").evaluate({'x': 2, 'y': 2}))
+
+    def test_binop_div(self):
+        self.assertEqual(2, Expression("4 / 2").evaluate({}))
+        self.assertEqual(2, Expression("x / y").evaluate({'x': 4, 'y': 2}))
+
+    def test_binop_floordiv(self):
+        self.assertEqual(1, Expression("3 // 2").evaluate({}))
+        self.assertEqual(1, Expression("x // y").evaluate({'x': 3, 'y': 2}))
+
+    def test_binop_mod(self):
+        self.assertEqual(1, Expression("3 % 2").evaluate({}))
+        self.assertEqual(1, Expression("x % y").evaluate({'x': 3, 'y': 2}))
+
+    def test_binop_and(self):
+        self.assertEqual(0, Expression("1 & 0").evaluate({}))
+        self.assertEqual(0, Expression("x & y").evaluate({'x': 1, 'y': 0}))
+
+    def test_binop_or(self):
+        self.assertEqual(1, Expression("1 | 0").evaluate({}))
+        self.assertEqual(1, Expression("x | y").evaluate({'x': 1, 'y': 0}))
+
+    def test_binop_contains(self):
+        self.assertEqual(True, Expression("1 in (1, 2, 3)").evaluate({}))
+        self.assertEqual(True, Expression("x in y").evaluate({'x': 1,
+                                                              'y': (1, 2, 3)}))
+
+    def test_binop_not_contains(self):
+        self.assertEqual(True, Expression("4 not in (1, 2, 3)").evaluate({}))
+        self.assertEqual(True, Expression("x not in y").evaluate({'x': 4,
+                                                                  'y': (1, 2, 3)}))
+
+    def test_binop_is(self):
+        self.assertEqual(True, Expression("1 is 1").evaluate({}))
+        self.assertEqual(True, Expression("x is y").evaluate({'x': 1, 'y': 1}))
+        self.assertEqual(False, Expression("1 is 2").evaluate({}))
+        self.assertEqual(False, Expression("x is y").evaluate({'x': 1, 'y': 2}))
+
+    def test_binop_is_not(self):
+        self.assertEqual(True, Expression("1 is not 2").evaluate({}))
+        self.assertEqual(True, Expression("x is not y").evaluate({'x': 1,
+                                                                  'y': 2}))
+        self.assertEqual(False, Expression("1 is not 1").evaluate({}))
+        self.assertEqual(False, Expression("x is not y").evaluate({'x': 1,
+                                                                   'y': 1}))
+
+    def test_boolop_and(self):
+        self.assertEqual(False, Expression("True and False").evaluate({}))
+        self.assertEqual(False, Expression("x and y").evaluate({'x': True,
+                                                                'y': False}))
+
+    def test_boolop_or(self):
+        self.assertEqual(True, Expression("True or False").evaluate({}))
+        self.assertEqual(True, Expression("x or y").evaluate({'x': True,
+                                                              'y': False}))
+
+    def test_compare_eq(self):
+        self.assertEqual(True, Expression("1 == 1").evaluate({}))
+        self.assertEqual(True, Expression("x == y").evaluate({'x': 1, 'y': 1}))
+
+    def test_compare_ne(self):
+        self.assertEqual(False, Expression("1 != 1").evaluate({}))
+        self.assertEqual(False, Expression("x != y").evaluate({'x': 1, 'y': 1}))
+        self.assertEqual(False, Expression("1 <> 1").evaluate({}))
+        self.assertEqual(False, Expression("x <> y").evaluate({'x': 1, 'y': 1}))
+
+    def test_compare_lt(self):
+        self.assertEqual(True, Expression("1 < 2").evaluate({}))
+        self.assertEqual(True, Expression("x < y").evaluate({'x': 1, 'y': 2}))
+
+    def test_compare_le(self):
+        self.assertEqual(True, Expression("1 <= 1").evaluate({}))
+        self.assertEqual(True, Expression("x <= y").evaluate({'x': 1, 'y': 1}))
+
+    def test_compare_gt(self):
+        self.assertEqual(True, Expression("2 > 1").evaluate({}))
+        self.assertEqual(True, Expression("x > y").evaluate({'x': 2, 'y': 1}))
+
+    def test_compare_ge(self):
+        self.assertEqual(True, Expression("1 >= 1").evaluate({}))
+        self.assertEqual(True, Expression("x >= y").evaluate({'x': 1, 'y': 1}))
+
+    def test_compare_multi(self):
+        self.assertEqual(True, Expression("1 != 3 == 3").evaluate({}))
+        self.assertEqual(True, Expression("x != y == y").evaluate({'x': 1,
+                                                                   'y': 3}))
+
+    def test_call_function(self):
+        self.assertEqual(42, Expression("foo()").evaluate({'foo': lambda: 42}))
+        data = {'foo': 'bar'}
+        self.assertEqual('BAR', Expression("foo.upper()").evaluate(data))
+        data = {'foo': {'bar': range(42)}}
+        self.assertEqual(42, Expression("len(foo.bar)").evaluate(data))
+
+    def test_call_keywords(self):
+        self.assertEqual(42, Expression("foo(x=bar)").evaluate({'foo': lambda x: x,
+                                                                'bar': 42}))
+
+    def test_call_star_args(self):
+        self.assertEqual(42, Expression("foo(*bar)").evaluate({'foo': lambda x: x,
+                                                               'bar': [42]}))
+
+    def test_call_dstar_args(self):
+        def foo(x):
+            return x
+        self.assertEqual(42, Expression("foo(**bar)").evaluate({'foo': foo,
+                                                                'bar': {"x": 42}}))
+
+    def test_call_function_without_params(self):
+        self.assertEqual(42, Expression("foo").evaluate({'foo': lambda: 42}))
+        data = {'foo': 'bar'}
+        self.assertEqual('BAR', Expression("foo.upper").evaluate(data))
+        data = {'foo': {'bar': range(42)}}
+
+    def test_lambda(self):
+        # Define a custom `sorted` function cause the builtin isn't available
+        # on Python 2.3
+        def sorted(items, compfunc):
+            items.sort(compfunc)
+            return items
+        data = {'items': [{'name': 'b', 'value': 0}, {'name': 'a', 'value': 1}],
+                'sorted': sorted}
+        expr = Expression("sorted(items, lambda a, b: cmp(a.name, b.name))")
+        self.assertEqual([{'name': 'a', 'value': 1}, {'name': 'b', 'value': 0}],
+                         expr.evaluate(data))
+
+    def test_list_comprehension(self):
+        expr = Expression("[n for n in numbers if n < 2]")
+        self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)}))
+
+        expr = Expression("[(i, n + 1) for i, n in enumerate(numbers)]")
+        self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)],
+                         expr.evaluate({'numbers': range(5)}))
+
+        expr = Expression("[offset + n for n in numbers]")
+        self.assertEqual([2, 3, 4, 5, 6],
+                         expr.evaluate({'numbers': range(5), 'offset': 2}))
+
+    def test_list_comprehension_with_getattr(self):
+        items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}]
+        expr = Expression("[i.name for i in items if i.value > 1]")
+        self.assertEqual(['b'], expr.evaluate({'items': items}))
+
+    def test_list_comprehension_with_getitem(self):
+        items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}]
+        expr = Expression("[i['name'] for i in items if i['value'] > 1]")
+        self.assertEqual(['b'], expr.evaluate({'items': items}))
+
+    if sys.version_info >= (2, 4):
+        # Generator expressions only supported in Python 2.4 and up
+
+        def test_generator_expression(self):
+            expr = Expression("list(n for n in numbers if n < 2)")
+            self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)}))
+
+            expr = Expression("list((i, n + 1) for i, n in enumerate(numbers))")
+            self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)],
+                             expr.evaluate({'numbers': range(5)}))
+
+            expr = Expression("list(offset + n for n in numbers)")
+            self.assertEqual([2, 3, 4, 5, 6],
+                             expr.evaluate({'numbers': range(5), 'offset': 2}))
+
+        def test_generator_expression_with_getattr(self):
+            items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}]
+            expr = Expression("list(i.name for i in items if i.value > 1)")
+            self.assertEqual(['b'], expr.evaluate({'items': items}))
+
+        def test_generator_expression_with_getitem(self):
+            items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}]
+            expr = Expression("list(i['name'] for i in items if i['value'] > 1)")
+            self.assertEqual(['b'], expr.evaluate({'items': items}))
+
+    def test_slice(self):
+        expr = Expression("numbers[0:2]")
+        self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)}))
+
+    def test_slice_with_vars(self):
+        expr = Expression("numbers[start:end]")
+        self.assertEqual([0, 1], expr.evaluate({'numbers': range(5), 'start': 0,
+                                                'end': 2}))
+
+    def test_slice_copy(self):
+        expr = Expression("numbers[:]")
+        self.assertEqual([0, 1, 2, 3, 4], expr.evaluate({'numbers': range(5)}))
+
+    def test_slice_stride(self):
+        expr = Expression("numbers[::stride]")
+        self.assertEqual([0, 2, 4], expr.evaluate({'numbers': range(5),
+                                                   'stride': 2}))
+
+    def test_slice_negative_start(self):
+        expr = Expression("numbers[-1:]")
+        self.assertEqual([4], expr.evaluate({'numbers': range(5)}))
+
+    def test_slice_negative_end(self):
+        expr = Expression("numbers[:-1]")
+        self.assertEqual([0, 1, 2, 3], expr.evaluate({'numbers': range(5)}))
+
+    def test_error_access_undefined(self):
+        expr = Expression("nothing", filename='index.html', lineno=50)
+        self.assertEqual(Undefined, type(expr.evaluate({})))
+
+    def test_error_call_undefined(self):
+        expr = Expression("nothing()", filename='index.html', lineno=50)
+        try:
+            expr.evaluate({})
+            self.fail('Expected NameError')
+        except NameError, e:
+            exc_type, exc_value, exc_traceback = sys.exc_info()
+            frame = exc_traceback.tb_next
+            frames = []
+            while frame.tb_next:
+                frame = frame.tb_next
+                frames.append(frame)
+            self.assertEqual('Variable "nothing" is not defined', str(e))
+            self.assertEqual('<Expression "nothing()">',
+                             frames[-3].tb_frame.f_code.co_name)
+            self.assertEqual('index.html',
+                             frames[-3].tb_frame.f_code.co_filename)
+            self.assertEqual(50, frames[-3].tb_lineno)
+
+    def test_error_getattr_undefined(self):
+        expr = Expression("nothing.nil", filename='index.html', lineno=50)
+        try:
+            expr.evaluate({})
+            self.fail('Expected NameError')
+        except NameError, e:
+            exc_type, exc_value, exc_traceback = sys.exc_info()
+            frame = exc_traceback.tb_next
+            frames = []
+            while frame.tb_next:
+                frame = frame.tb_next
+                frames.append(frame)
+            self.assertEqual('Variable "nothing" is not defined', str(e))
+            self.assertEqual('<Expression "nothing.nil">',
+                             frames[-3].tb_frame.f_code.co_name)
+            self.assertEqual('index.html',
+                             frames[-3].tb_frame.f_code.co_filename)
+            self.assertEqual(50, frames[-3].tb_lineno)
+
+    def test_error_getitem_undefined(self):
+        expr = Expression("nothing[0]", filename='index.html', lineno=50)
+        try:
+            expr.evaluate({})
+            self.fail('Expected NameError')
+        except NameError, e:
+            exc_type, exc_value, exc_traceback = sys.exc_info()
+            frame = exc_traceback.tb_next
+            frames = []
+            while frame.tb_next:
+                frame = frame.tb_next
+                frames.append(frame)
+            self.assertEqual('Variable "nothing" is not defined', str(e))
+            self.assertEqual('<Expression "nothing[0]">',
+                             frames[-3].tb_frame.f_code.co_name)
+            self.assertEqual('index.html',
+                             frames[-3].tb_frame.f_code.co_filename)
+            self.assertEqual(50, frames[-3].tb_lineno)
+
+    def test_error_getattr_nested_undefined(self):
+        expr = Expression("nothing.nil", filename='index.html', lineno=50)
+        val = expr.evaluate({'nothing': object()})
+        assert isinstance(val, Undefined)
+        self.assertEqual("nil", val._name)
+
+    def test_error_getitem_nested_undefined_string(self):
+        expr = Expression("nothing['bla']", filename='index.html', lineno=50)
+        val = expr.evaluate({'nothing': object()})
+        assert isinstance(val, Undefined)
+        self.assertEqual("bla", val._name)
+
+    def test_error_getitem_nested_undefined_int(self):
+        expr = Expression("nothing[0]", filename='index.html', lineno=50)
+        self.assertRaises(TypeError, expr.evaluate, {'nothing': object()})
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(Expression.__module__))
+    suite.addTest(unittest.makeSuite(ExpressionTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/loader.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+import doctest
+import os
+import shutil
+import tempfile
+import unittest
+
+from genshi.template.loader import TemplateLoader
+from genshi.template.markup import MarkupTemplate
+
+
+class TemplateLoaderTestCase(unittest.TestCase):
+    """Tests for the template loader."""
+
+    def setUp(self):
+        self.dirname = tempfile.mkdtemp(suffix='markup_test')
+
+    def tearDown(self):
+        shutil.rmtree(self.dirname)
+
+    def test_search_path_empty(self):
+        loader = TemplateLoader()
+        self.assertEqual([], loader.search_path)
+
+    def test_search_path_as_string(self):
+        loader = TemplateLoader(self.dirname)
+        self.assertEqual([self.dirname], loader.search_path)
+
+    def test_relative_include_samedir(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included</div>""")
+        finally:
+            file1.close()
+
+        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
+        try:
+            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl1.html" />
+            </html>""")
+        finally:
+            file2.close()
+
+        loader = TemplateLoader([self.dirname])
+        tmpl = loader.load('tmpl2.html')
+        self.assertEqual("""<html>
+              <div>Included</div>
+            </html>""", tmpl.generate().render())
+
+    def test_relative_include_subdir(self):
+        os.mkdir(os.path.join(self.dirname, 'sub'))
+        file1 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included</div>""")
+        finally:
+            file1.close()
+
+        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
+        try:
+            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="sub/tmpl1.html" />
+            </html>""")
+        finally:
+            file2.close()
+
+        loader = TemplateLoader([self.dirname])
+        tmpl = loader.load('tmpl2.html')
+        self.assertEqual("""<html>
+              <div>Included</div>
+            </html>""", tmpl.generate().render())
+
+    def test_relative_include_parentdir(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included</div>""")
+        finally:
+            file1.close()
+
+        os.mkdir(os.path.join(self.dirname, 'sub'))
+        file2 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w')
+        try:
+            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="../tmpl1.html" />
+            </html>""")
+        finally:
+            file2.close()
+
+        loader = TemplateLoader([self.dirname])
+        tmpl = loader.load('sub/tmpl2.html')
+        self.assertEqual("""<html>
+              <div>Included</div>
+            </html>""", tmpl.generate().render())
+
+    def test_relative_include_without_search_path(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included</div>""")
+        finally:
+            file1.close()
+
+        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
+        try:
+            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl1.html" />
+            </html>""")
+        finally:
+            file2.close()
+
+        loader = TemplateLoader()
+        tmpl = loader.load(os.path.join(self.dirname, 'tmpl2.html'))
+        self.assertEqual("""<html>
+              <div>Included</div>
+            </html>""", tmpl.generate().render())
+
+    def test_relative_include_without_search_path_nested(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included</div>""")
+        finally:
+            file1.close()
+
+        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
+        try:
+            file2.write("""<div xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl1.html" />
+            </div>""")
+        finally:
+            file2.close()
+
+        file3 = open(os.path.join(self.dirname, 'tmpl3.html'), 'w')
+        try:
+            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl2.html" />
+            </html>""")
+        finally:
+            file3.close()
+
+        loader = TemplateLoader()
+        tmpl = loader.load(os.path.join(self.dirname, 'tmpl3.html'))
+        self.assertEqual("""<html>
+              <div>
+              <div>Included</div>
+            </div>
+            </html>""", tmpl.generate().render())
+
+    def test_relative_include_from_inmemory_template(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included</div>""")
+        finally:
+            file1.close()
+
+        loader = TemplateLoader([self.dirname])
+        tmpl2 = MarkupTemplate("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+          <xi:include href="../tmpl1.html" />
+        </html>""", filename='subdir/tmpl2.html', loader=loader)
+
+        self.assertEqual("""<html>
+          <div>Included</div>
+        </html>""", tmpl2.generate().render())
+
+    def test_load_with_default_encoding(self):
+        f = open(os.path.join(self.dirname, 'tmpl.html'), 'w')
+        try:
+            f.write(u'<div>\xf6</div>'.encode('iso-8859-1'))
+        finally:
+            f.close()
+        loader = TemplateLoader([self.dirname], default_encoding='iso-8859-1')
+        loader.load('tmpl.html')
+
+    def test_load_with_explicit_encoding(self):
+        f = open(os.path.join(self.dirname, 'tmpl.html'), 'w')
+        try:
+            f.write(u'<div>\xf6</div>'.encode('iso-8859-1'))
+        finally:
+            f.close()
+        loader = TemplateLoader([self.dirname], default_encoding='utf-8')
+        loader.load('tmpl.html', encoding='iso-8859-1')
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(TemplateLoader.__module__))
+    suite.addTest(unittest.makeSuite(TemplateLoaderTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/markup.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+import doctest
+import sys
+import unittest
+
+from genshi.core import Markup
+from genshi.template.core import BadDirectiveError, TemplateSyntaxError
+from genshi.template.markup import MarkupTemplate
+
+
+class MarkupTemplateTestCase(unittest.TestCase):
+    """Tests for markup template processing."""
+
+    def test_interpolate_mixed3(self):
+        tmpl = MarkupTemplate('<root> ${var} $var</root>')
+        self.assertEqual('<root> 42 42</root>', str(tmpl.generate(var=42)))
+
+    def test_interpolate_leading_trailing_space(self):
+        tmpl = MarkupTemplate('<root>${    foo    }</root>')
+        self.assertEqual('<root>bar</root>', str(tmpl.generate(foo='bar')))
+
+    def test_interpolate_multiline(self):
+        tmpl = MarkupTemplate("""<root>${dict(
+          bar = 'baz'
+        )[foo]}</root>""")
+        self.assertEqual('<root>baz</root>', str(tmpl.generate(foo='bar')))
+
+    def test_interpolate_non_string_attrs(self):
+        tmpl = MarkupTemplate('<root attr="${1}"/>')
+        self.assertEqual('<root attr="1"/>', str(tmpl.generate()))
+
+    def test_interpolate_list_result(self):
+        tmpl = MarkupTemplate('<root>$foo</root>')
+        self.assertEqual('<root>buzz</root>', str(tmpl.generate(foo=('buzz',))))
+
+    def test_empty_attr(self):
+        tmpl = MarkupTemplate('<root attr=""/>')
+        self.assertEqual('<root attr=""/>', str(tmpl.generate()))
+
+    def test_bad_directive_error(self):
+        xml = '<p xmlns:py="http://genshi.edgewall.org/" py:do="nothing" />'
+        try:
+            tmpl = MarkupTemplate(xml, filename='test.html')
+        except BadDirectiveError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(1, e.lineno)
+
+    def test_directive_value_syntax_error(self):
+        xml = """<p xmlns:py="http://genshi.edgewall.org/" py:if="bar'" />"""
+        try:
+            tmpl = MarkupTemplate(xml, filename='test.html')
+            self.fail('Expected SyntaxError')
+        except TemplateSyntaxError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(1, e.lineno)
+
+    def test_expression_syntax_error(self):
+        xml = """<p>
+          Foo <em>${bar"}</em>
+        </p>"""
+        try:
+            tmpl = MarkupTemplate(xml, filename='test.html')
+            self.fail('Expected SyntaxError')
+        except TemplateSyntaxError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(2, e.lineno)
+
+    def test_expression_syntax_error_multi_line(self):
+        xml = """<p><em></em>
+
+ ${bar"}
+
+        </p>"""
+        try:
+            tmpl = MarkupTemplate(xml, filename='test.html')
+            self.fail('Expected SyntaxError')
+        except TemplateSyntaxError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(3, e.lineno)
+
+    def test_markup_noescape(self):
+        """
+        Verify that outputting context data that is a `Markup` instance is not
+        escaped.
+        """
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          $myvar
+        </div>""")
+        self.assertEqual("""<div>
+          <b>foo</b>
+        </div>""", str(tmpl.generate(myvar=Markup('<b>foo</b>'))))
+
+    def test_text_noescape_quotes(self):
+        """
+        Verify that outputting context data in text nodes doesn't escape quotes.
+        """
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          $myvar
+        </div>""")
+        self.assertEqual("""<div>
+          "foo"
+        </div>""", str(tmpl.generate(myvar='"foo"')))
+
+    def test_attr_escape_quotes(self):
+        """
+        Verify that outputting context data in attribtes escapes quotes.
+        """
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <elem class="$myvar"/>
+        </div>""")
+        self.assertEqual("""<div>
+          <elem class="&#34;foo&#34;"/>
+        </div>""", str(tmpl.generate(myvar='"foo"')))
+
+    def test_directive_element(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <py:if test="myvar">bar</py:if>
+        </div>""")
+        self.assertEqual("""<div>
+          bar
+        </div>""", str(tmpl.generate(myvar='"foo"')))
+
+    def test_normal_comment(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <!-- foo bar -->
+        </div>""")
+        self.assertEqual("""<div>
+          <!-- foo bar -->
+        </div>""", str(tmpl.generate()))
+
+    def test_template_comment(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <!-- !foo -->
+          <!--!bar-->
+        </div>""")
+        self.assertEqual("""<div>
+        </div>""", str(tmpl.generate()))
+
+    def test_parse_with_same_namespace_nested(self):
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
+          <span xmlns:py="http://genshi.edgewall.org/">
+          </span>
+        </div>""")
+        self.assertEqual("""<div>
+          <span>
+          </span>
+        </div>""", str(tmpl.generate()))
+
+    def test_latin1_encoded_with_xmldecl(self):
+        tmpl = MarkupTemplate(u"""<?xml version="1.0" encoding="iso-8859-1" ?>
+        <div xmlns:py="http://genshi.edgewall.org/">
+          \xf6
+        </div>""".encode('iso-8859-1'), encoding='iso-8859-1')
+        self.assertEqual(u"""<div>
+          \xf6
+        </div>""", unicode(tmpl.generate()))
+
+    def test_latin1_encoded_explicit_encoding(self):
+        tmpl = MarkupTemplate(u"""<div xmlns:py="http://genshi.edgewall.org/">
+          \xf6
+        </div>""".encode('iso-8859-1'), encoding='iso-8859-1')
+        self.assertEqual(u"""<div>
+          \xf6
+        </div>""", unicode(tmpl.generate()))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(MarkupTemplate.__module__))
+    suite.addTest(unittest.makeSuite(MarkupTemplateTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/text.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+import doctest
+import unittest
+
+from genshi.template.text import TextTemplate
+
+
+class TextTemplateTestCase(unittest.TestCase):
+    """Tests for text template processing."""
+
+    def test_escaping(self):
+        tmpl = TextTemplate('\\#escaped')
+        self.assertEqual('#escaped', str(tmpl.generate()))
+
+    def test_comment(self):
+        tmpl = TextTemplate('## a comment')
+        self.assertEqual('', str(tmpl.generate()))
+
+    def test_comment_escaping(self):
+        tmpl = TextTemplate('\\## escaped comment')
+        self.assertEqual('## escaped comment', str(tmpl.generate()))
+
+    def test_end_with_args(self):
+        tmpl = TextTemplate("""
+        #if foo
+          bar
+        #end 'if foo'""")
+        self.assertEqual('', str(tmpl.generate()))
+
+    def test_latin1_encoded(self):
+        text = u'$foo\xf6$bar'.encode('iso-8859-1')
+        tmpl = TextTemplate(text, encoding='iso-8859-1')
+        self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
+
+    # FIXME
+    #def test_empty_lines(self):
+    #    tmpl = TextTemplate("""Your items:
+    #
+    #    #for item in items
+    #      * ${item}
+    #
+    #    #end""")
+    #    self.assertEqual("""Your items:
+    #      * 0
+    #      * 1
+    #      * 2
+    #    """, tmpl.generate(items=range(3)).render('text'))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(TextTemplate.__module__))
+    suite.addTest(unittest.makeSuite(TextTemplateTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/genshi/template/text.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Plain text templating engine."""
+
+import re
+
+from genshi.template.core import BadDirectiveError, Template, SUB
+from genshi.template.directives import *
+
+
+class TextTemplate(Template):
+    """Implementation of a simple text-based template engine.
+    
+    >>> tmpl = TextTemplate('''Dear $name,
+    ... 
+    ... We have the following items for you:
+    ... #for item in items
+    ...  * $item
+    ... #end
+    ... 
+    ... All the best,
+    ... Foobar''')
+    >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
+    Dear Joe,
+    <BLANKLINE>
+    We have the following items for you:
+     * 1
+     * 2
+     * 3
+    <BLANKLINE>
+    All the best,
+    Foobar
+    """
+    directives = [('def', DefDirective),
+                  ('when', WhenDirective),
+                  ('otherwise', OtherwiseDirective),
+                  ('for', ForDirective),
+                  ('if', IfDirective),
+                  ('choose', ChooseDirective),
+                  ('with', WithDirective)]
+
+    _DIRECTIVE_RE = re.compile(r'^\s*(?<!\\)#((?:\w+|#).*)\n?', re.MULTILINE)
+
+    def _parse(self, encoding):
+        """Parse the template from text input."""
+        stream = [] # list of events of the "compiled" template
+        dirmap = {} # temporary mapping of directives to elements
+        depth = 0
+        if not encoding:
+            encoding = 'utf-8'
+
+        source = self.source.read().decode(encoding, 'replace')
+        offset = 0
+        lineno = 1
+
+        for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)):
+            start, end = mo.span()
+            if start > offset:
+                text = source[offset:start]
+                for kind, data, pos in self._interpolate(text, self.basedir,
+                                                         self.filename, lineno):
+                    stream.append((kind, data, pos))
+                lineno += len(text.splitlines())
+
+            text = source[start:end].lstrip()[1:]
+            lineno += len(text.splitlines())
+            directive = text.split(None, 1)
+            if len(directive) > 1:
+                command, value = directive
+            else:
+                command, value = directive[0], None
+
+            if command == 'end':
+                depth -= 1
+                if depth in dirmap:
+                    directive, start_offset = dirmap.pop(depth)
+                    substream = stream[start_offset:]
+                    stream[start_offset:] = [(SUB, ([directive], substream),
+                                              (self.filepath, lineno, 0))]
+            elif command != '#':
+                cls = self._dir_by_name.get(command)
+                if cls is None:
+                    raise BadDirectiveError(command)
+                directive = cls(value, None, self.filepath, lineno, 0)
+                dirmap[depth] = (directive, len(stream))
+                depth += 1
+
+            offset = end
+
+        if offset < len(source):
+            text = source[offset:].replace('\\#', '#')
+            for kind, data, pos in self._interpolate(text, self.basedir,
+                                                     self.filename, lineno):
+                stream.append((kind, data, pos))
+
+        return stream
--- a/genshi/tests/__init__.py
+++ b/genshi/tests/__init__.py
@@ -11,18 +11,17 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://genshi.edgewall.org/log/.
 
-import doctest
 import unittest
 
 def suite():
     import genshi
-    from genshi.tests import builder, core, eval, filters, input, output, \
-                             path, template, util
+    from genshi.tests import builder, core, filters, input, output, path, \
+                             util
+    from genshi.template import tests as template
+
     suite = unittest.TestSuite()
-    suite.addTest(doctest.DocTestSuite(genshi))
     suite.addTest(builder.suite())
     suite.addTest(core.suite())
-    suite.addTest(eval.suite())
     suite.addTest(filters.suite())
     suite.addTest(input.suite())
     suite.addTest(output.suite())
deleted file mode 100644
--- a/genshi/tests/eval.py
+++ /dev/null
@@ -1,389 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://genshi.edgewall.org/wiki/License.
-#
-# This software consists of voluntary contributions made by many
-# individuals. For the exact contribution history, see the revision
-# history and logs, available at http://genshi.edgewall.org/log/.
-
-import doctest
-import sys
-import unittest
-
-from genshi.eval import Expression, Undefined
-
-
-class ExpressionTestCase(unittest.TestCase):
-
-    def test_name_lookup(self):
-        self.assertEqual('bar', Expression('foo').evaluate({'foo': 'bar'}))
-        self.assertEqual(id, Expression('id').evaluate({}, nocall=True))
-        self.assertEqual('bar', Expression('id').evaluate({'id': 'bar'}))
-        self.assertEqual(None, Expression('id').evaluate({'id': None},
-                                                         nocall=True))
-
-    def test_str_literal(self):
-        self.assertEqual('foo', Expression('"foo"').evaluate({}))
-        self.assertEqual('foo', Expression('"""foo"""').evaluate({}))
-        self.assertEqual('foo', Expression("'foo'").evaluate({}))
-        self.assertEqual('foo', Expression("'''foo'''").evaluate({}))
-        self.assertEqual('foo', Expression("u'foo'").evaluate({}))
-        self.assertEqual('foo', Expression("r'foo'").evaluate({}))
-
-    def test_str_literal_non_ascii(self):
-        expr = Expression(u"u'\xfe'")
-        self.assertEqual(u'þ', expr.evaluate({}))
-        expr = Expression("u'\xfe'")
-        self.assertEqual(u'þ', expr.evaluate({}))
-        expr = Expression("'\xc3\xbe'")
-        self.assertEqual(u'þ', expr.evaluate({}))
-
-    def test_num_literal(self):
-        self.assertEqual(42, Expression("42").evaluate({}))
-        self.assertEqual(42L, Expression("42L").evaluate({}))
-        self.assertEqual(.42, Expression(".42").evaluate({}))
-        self.assertEqual(07, Expression("07").evaluate({}))
-        self.assertEqual(0xF2, Expression("0xF2").evaluate({}))
-        self.assertEqual(0XF2, Expression("0XF2").evaluate({}))
-
-    def test_dict_literal(self):
-        self.assertEqual({}, Expression("{}").evaluate({}))
-        self.assertEqual({'key': True},
-                         Expression("{'key': value}").evaluate({'value': True}))
-
-    def test_list_literal(self):
-        self.assertEqual([], Expression("[]").evaluate({}))
-        self.assertEqual([1, 2, 3], Expression("[1, 2, 3]").evaluate({}))
-        self.assertEqual([True],
-                         Expression("[value]").evaluate({'value': True}))
-
-    def test_tuple_literal(self):
-        self.assertEqual((), Expression("()").evaluate({}))
-        self.assertEqual((1, 2, 3), Expression("(1, 2, 3)").evaluate({}))
-        self.assertEqual((True,),
-                         Expression("(value,)").evaluate({'value': True}))
-
-    def test_unaryop_pos(self):
-        self.assertEqual(1, Expression("+1").evaluate({}))
-        self.assertEqual(1, Expression("+x").evaluate({'x': 1}))
-
-    def test_unaryop_neg(self):
-        self.assertEqual(-1, Expression("-1").evaluate({}))
-        self.assertEqual(-1, Expression("-x").evaluate({'x': 1}))
-
-    def test_unaryop_not(self):
-        self.assertEqual(False, Expression("not True").evaluate({}))
-        self.assertEqual(False, Expression("not x").evaluate({'x': True}))
-
-    def test_unaryop_inv(self):
-        self.assertEqual(-2, Expression("~1").evaluate({}))
-        self.assertEqual(-2, Expression("~x").evaluate({'x': 1}))
-
-    def test_binop_add(self):
-        self.assertEqual(3, Expression("2 + 1").evaluate({}))
-        self.assertEqual(3, Expression("x + y").evaluate({'x': 2, 'y': 1}))
-
-    def test_binop_sub(self):
-        self.assertEqual(1, Expression("2 - 1").evaluate({}))
-        self.assertEqual(1, Expression("x - y").evaluate({'x': 1, 'y': 1}))
-
-    def test_binop_sub(self):
-        self.assertEqual(1, Expression("2 - 1").evaluate({}))
-        self.assertEqual(1, Expression("x - y").evaluate({'x': 2, 'y': 1}))
-
-    def test_binop_mul(self):
-        self.assertEqual(4, Expression("2 * 2").evaluate({}))
-        self.assertEqual(4, Expression("x * y").evaluate({'x': 2, 'y': 2}))
-
-    def test_binop_pow(self):
-        self.assertEqual(4, Expression("2 ** 2").evaluate({}))
-        self.assertEqual(4, Expression("x ** y").evaluate({'x': 2, 'y': 2}))
-
-    def test_binop_div(self):
-        self.assertEqual(2, Expression("4 / 2").evaluate({}))
-        self.assertEqual(2, Expression("x / y").evaluate({'x': 4, 'y': 2}))
-
-    def test_binop_floordiv(self):
-        self.assertEqual(1, Expression("3 // 2").evaluate({}))
-        self.assertEqual(1, Expression("x // y").evaluate({'x': 3, 'y': 2}))
-
-    def test_binop_mod(self):
-        self.assertEqual(1, Expression("3 % 2").evaluate({}))
-        self.assertEqual(1, Expression("x % y").evaluate({'x': 3, 'y': 2}))
-
-    def test_binop_and(self):
-        self.assertEqual(0, Expression("1 & 0").evaluate({}))
-        self.assertEqual(0, Expression("x & y").evaluate({'x': 1, 'y': 0}))
-
-    def test_binop_or(self):
-        self.assertEqual(1, Expression("1 | 0").evaluate({}))
-        self.assertEqual(1, Expression("x | y").evaluate({'x': 1, 'y': 0}))
-
-    def test_binop_contains(self):
-        self.assertEqual(True, Expression("1 in (1, 2, 3)").evaluate({}))
-        self.assertEqual(True, Expression("x in y").evaluate({'x': 1,
-                                                              'y': (1, 2, 3)}))
-
-    def test_binop_not_contains(self):
-        self.assertEqual(True, Expression("4 not in (1, 2, 3)").evaluate({}))
-        self.assertEqual(True, Expression("x not in y").evaluate({'x': 4,
-                                                                  'y': (1, 2, 3)}))
-
-    def test_binop_is(self):
-        self.assertEqual(True, Expression("1 is 1").evaluate({}))
-        self.assertEqual(True, Expression("x is y").evaluate({'x': 1, 'y': 1}))
-        self.assertEqual(False, Expression("1 is 2").evaluate({}))
-        self.assertEqual(False, Expression("x is y").evaluate({'x': 1, 'y': 2}))
-
-    def test_binop_is_not(self):
-        self.assertEqual(True, Expression("1 is not 2").evaluate({}))
-        self.assertEqual(True, Expression("x is not y").evaluate({'x': 1,
-                                                                  'y': 2}))
-        self.assertEqual(False, Expression("1 is not 1").evaluate({}))
-        self.assertEqual(False, Expression("x is not y").evaluate({'x': 1,
-                                                                   'y': 1}))
-
-    def test_boolop_and(self):
-        self.assertEqual(False, Expression("True and False").evaluate({}))
-        self.assertEqual(False, Expression("x and y").evaluate({'x': True,
-                                                                'y': False}))
-
-    def test_boolop_or(self):
-        self.assertEqual(True, Expression("True or False").evaluate({}))
-        self.assertEqual(True, Expression("x or y").evaluate({'x': True,
-                                                              'y': False}))
-
-    def test_compare_eq(self):
-        self.assertEqual(True, Expression("1 == 1").evaluate({}))
-        self.assertEqual(True, Expression("x == y").evaluate({'x': 1, 'y': 1}))
-
-    def test_compare_ne(self):
-        self.assertEqual(False, Expression("1 != 1").evaluate({}))
-        self.assertEqual(False, Expression("x != y").evaluate({'x': 1, 'y': 1}))
-        self.assertEqual(False, Expression("1 <> 1").evaluate({}))
-        self.assertEqual(False, Expression("x <> y").evaluate({'x': 1, 'y': 1}))
-
-    def test_compare_lt(self):
-        self.assertEqual(True, Expression("1 < 2").evaluate({}))
-        self.assertEqual(True, Expression("x < y").evaluate({'x': 1, 'y': 2}))
-
-    def test_compare_le(self):
-        self.assertEqual(True, Expression("1 <= 1").evaluate({}))
-        self.assertEqual(True, Expression("x <= y").evaluate({'x': 1, 'y': 1}))
-
-    def test_compare_gt(self):
-        self.assertEqual(True, Expression("2 > 1").evaluate({}))
-        self.assertEqual(True, Expression("x > y").evaluate({'x': 2, 'y': 1}))
-
-    def test_compare_ge(self):
-        self.assertEqual(True, Expression("1 >= 1").evaluate({}))
-        self.assertEqual(True, Expression("x >= y").evaluate({'x': 1, 'y': 1}))
-
-    def test_compare_multi(self):
-        self.assertEqual(True, Expression("1 != 3 == 3").evaluate({}))
-        self.assertEqual(True, Expression("x != y == y").evaluate({'x': 1,
-                                                                   'y': 3}))
-
-    def test_call_function(self):
-        self.assertEqual(42, Expression("foo()").evaluate({'foo': lambda: 42}))
-        data = {'foo': 'bar'}
-        self.assertEqual('BAR', Expression("foo.upper()").evaluate(data))
-        data = {'foo': {'bar': range(42)}}
-        self.assertEqual(42, Expression("len(foo.bar)").evaluate(data))
-
-    def test_call_keywords(self):
-        self.assertEqual(42, Expression("foo(x=bar)").evaluate({'foo': lambda x: x,
-                                                                'bar': 42}))
-
-    def test_call_star_args(self):
-        self.assertEqual(42, Expression("foo(*bar)").evaluate({'foo': lambda x: x,
-                                                               'bar': [42]}))
-
-    def test_call_dstar_args(self):
-        def foo(x):
-            return x
-        self.assertEqual(42, Expression("foo(**bar)").evaluate({'foo': foo,
-                                                                'bar': {"x": 42}}))
-
-    def test_call_function_without_params(self):
-        self.assertEqual(42, Expression("foo").evaluate({'foo': lambda: 42}))
-        data = {'foo': 'bar'}
-        self.assertEqual('BAR', Expression("foo.upper").evaluate(data))
-        data = {'foo': {'bar': range(42)}}
-
-    def test_lambda(self):
-        # Define a custom `sorted` function cause the builtin isn't available
-        # on Python 2.3
-        def sorted(items, compfunc):
-            items.sort(compfunc)
-            return items
-        data = {'items': [{'name': 'b', 'value': 0}, {'name': 'a', 'value': 1}],
-                'sorted': sorted}
-        expr = Expression("sorted(items, lambda a, b: cmp(a.name, b.name))")
-        self.assertEqual([{'name': 'a', 'value': 1}, {'name': 'b', 'value': 0}],
-                         expr.evaluate(data))
-
-    def test_list_comprehension(self):
-        expr = Expression("[n for n in numbers if n < 2]")
-        self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)}))
-
-        expr = Expression("[(i, n + 1) for i, n in enumerate(numbers)]")
-        self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)],
-                         expr.evaluate({'numbers': range(5)}))
-
-        expr = Expression("[offset + n for n in numbers]")
-        self.assertEqual([2, 3, 4, 5, 6],
-                         expr.evaluate({'numbers': range(5), 'offset': 2}))
-
-    def test_list_comprehension_with_getattr(self):
-        items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}]
-        expr = Expression("[i.name for i in items if i.value > 1]")
-        self.assertEqual(['b'], expr.evaluate({'items': items}))
-
-    def test_list_comprehension_with_getitem(self):
-        items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}]
-        expr = Expression("[i['name'] for i in items if i['value'] > 1]")
-        self.assertEqual(['b'], expr.evaluate({'items': items}))
-
-    if sys.version_info >= (2, 4):
-        # Generator expressions only supported in Python 2.4 and up
-
-        def test_generator_expression(self):
-            expr = Expression("list(n for n in numbers if n < 2)")
-            self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)}))
-
-            expr = Expression("list((i, n + 1) for i, n in enumerate(numbers))")
-            self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)],
-                             expr.evaluate({'numbers': range(5)}))
-
-            expr = Expression("list(offset + n for n in numbers)")
-            self.assertEqual([2, 3, 4, 5, 6],
-                             expr.evaluate({'numbers': range(5), 'offset': 2}))
-
-        def test_generator_expression_with_getattr(self):
-            items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}]
-            expr = Expression("list(i.name for i in items if i.value > 1)")
-            self.assertEqual(['b'], expr.evaluate({'items': items}))
-
-        def test_generator_expression_with_getitem(self):
-            items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}]
-            expr = Expression("list(i['name'] for i in items if i['value'] > 1)")
-            self.assertEqual(['b'], expr.evaluate({'items': items}))
-
-    def test_slice(self):
-        expr = Expression("numbers[0:2]")
-        self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)}))
-
-    def test_slice_with_vars(self):
-        expr = Expression("numbers[start:end]")
-        self.assertEqual([0, 1], expr.evaluate({'numbers': range(5), 'start': 0,
-                                                'end': 2}))
-
-    def test_slice_copy(self):
-        expr = Expression("numbers[:]")
-        self.assertEqual([0, 1, 2, 3, 4], expr.evaluate({'numbers': range(5)}))
-
-    def test_slice_stride(self):
-        expr = Expression("numbers[::stride]")
-        self.assertEqual([0, 2, 4], expr.evaluate({'numbers': range(5),
-                                                   'stride': 2}))
-
-    def test_slice_negative_start(self):
-        expr = Expression("numbers[-1:]")
-        self.assertEqual([4], expr.evaluate({'numbers': range(5)}))
-
-    def test_slice_negative_end(self):
-        expr = Expression("numbers[:-1]")
-        self.assertEqual([0, 1, 2, 3], expr.evaluate({'numbers': range(5)}))
-
-    def test_error_access_undefined(self):
-        expr = Expression("nothing", filename='index.html', lineno=50)
-        self.assertEqual(Undefined, type(expr.evaluate({})))
-
-    def test_error_call_undefined(self):
-        expr = Expression("nothing()", filename='index.html', lineno=50)
-        try:
-            expr.evaluate({})
-            self.fail('Expected NameError')
-        except NameError, e:
-            exc_type, exc_value, exc_traceback = sys.exc_info()
-            frame = exc_traceback.tb_next
-            frames = []
-            while frame.tb_next:
-                frame = frame.tb_next
-                frames.append(frame)
-            self.assertEqual('Variable "nothing" is not defined', str(e))
-            self.assertEqual('<Expression "nothing()">',
-                             frames[-3].tb_frame.f_code.co_name)
-            self.assertEqual('index.html',
-                             frames[-3].tb_frame.f_code.co_filename)
-            self.assertEqual(50, frames[-3].tb_lineno)
-
-    def test_error_getattr_undefined(self):
-        expr = Expression("nothing.nil", filename='index.html', lineno=50)
-        try:
-            expr.evaluate({})
-            self.fail('Expected NameError')
-        except NameError, e:
-            exc_type, exc_value, exc_traceback = sys.exc_info()
-            frame = exc_traceback.tb_next
-            frames = []
-            while frame.tb_next:
-                frame = frame.tb_next
-                frames.append(frame)
-            self.assertEqual('Variable "nothing" is not defined', str(e))
-            self.assertEqual('<Expression "nothing.nil">',
-                             frames[-3].tb_frame.f_code.co_name)
-            self.assertEqual('index.html',
-                             frames[-3].tb_frame.f_code.co_filename)
-            self.assertEqual(50, frames[-3].tb_lineno)
-
-    def test_error_getitem_undefined(self):
-        expr = Expression("nothing[0]", filename='index.html', lineno=50)
-        try:
-            expr.evaluate({})
-            self.fail('Expected NameError')
-        except NameError, e:
-            exc_type, exc_value, exc_traceback = sys.exc_info()
-            frame = exc_traceback.tb_next
-            frames = []
-            while frame.tb_next:
-                frame = frame.tb_next
-                frames.append(frame)
-            self.assertEqual('Variable "nothing" is not defined', str(e))
-            self.assertEqual('<Expression "nothing[0]">',
-                             frames[-3].tb_frame.f_code.co_name)
-            self.assertEqual('index.html',
-                             frames[-3].tb_frame.f_code.co_filename)
-            self.assertEqual(50, frames[-3].tb_lineno)
-
-    def test_error_getattr_nested_undefined(self):
-        expr = Expression("nothing.nil", filename='index.html', lineno=50)
-        val = expr.evaluate({'nothing': object()})
-        assert isinstance(val, Undefined)
-        self.assertEqual("nil", val._name)
-
-    def test_error_getitem_nested_undefined_string(self):
-        expr = Expression("nothing['bla']", filename='index.html', lineno=50)
-        val = expr.evaluate({'nothing': object()})
-        assert isinstance(val, Undefined)
-        self.assertEqual("bla", val._name)
-
-    def test_error_getitem_nested_undefined_int(self):
-        expr = Expression("nothing[0]", filename='index.html', lineno=50)
-        self.assertRaises(TypeError, expr.evaluate, {'nothing': object()})
-
-
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(ExpressionTestCase, 'test'))
-    suite.addTest(doctest.DocTestSuite(Expression.__module__))
-    return suite
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
deleted file mode 100644
--- a/genshi/tests/template.py
+++ /dev/null
@@ -1,1377 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://genshi.edgewall.org/wiki/License.
-#
-# This software consists of voluntary contributions made by many
-# individuals. For the exact contribution history, see the revision
-# history and logs, available at http://genshi.edgewall.org/log/.
-
-import doctest
-import os
-import unittest
-import shutil
-import sys
-import tempfile
-
-from genshi import template
-from genshi.core import Markup, Stream
-from genshi.template import BadDirectiveError, MarkupTemplate, Template, \
-                            TemplateLoader, TemplateRuntimeError, \
-                            TemplateSyntaxError, TextTemplate
-
-
-class AttrsDirectiveTestCase(unittest.TestCase):
-    """Tests for the `py:attrs` template directive."""
-
-    def test_combined_with_loop(self):
-        """
-        Verify that the directive has access to the loop variables.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <elem py:for="item in items" py:attrs="item"/>
-        </doc>""")
-        items = [{'id': 1, 'class': 'foo'}, {'id': 2, 'class': 'bar'}]
-        self.assertEqual("""<doc>
-          <elem id="1" class="foo"/><elem id="2" class="bar"/>
-        </doc>""", str(tmpl.generate(items=items)))
-
-    def test_update_existing_attr(self):
-        """
-        Verify that an attribute value that evaluates to `None` removes an
-        existing attribute of that name.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <elem class="foo" py:attrs="{'class': 'bar'}"/>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <elem class="bar"/>
-        </doc>""", str(tmpl.generate()))
-
-    def test_remove_existing_attr(self):
-        """
-        Verify that an attribute value that evaluates to `None` removes an
-        existing attribute of that name.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <elem class="foo" py:attrs="{'class': None}"/>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <elem/>
-        </doc>""", str(tmpl.generate()))
-
-
-class ChooseDirectiveTestCase(unittest.TestCase):
-    """Tests for the `py:choose` template directive and the complementary
-    directives `py:when` and `py:otherwise`."""
-
-    def test_multiple_true_whens(self):
-        """
-        Verify that, if multiple `py:when` bodies match, only the first is
-        output.
-        """
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/" py:choose="">
-          <span py:when="1 == 1">1</span>
-          <span py:when="2 == 2">2</span>
-          <span py:when="3 == 3">3</span>
-        </div>""")
-        self.assertEqual("""<div>
-          <span>1</span>
-        </div>""", str(tmpl.generate()))
-
-    def test_otherwise(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/" py:choose="">
-          <span py:when="False">hidden</span>
-          <span py:otherwise="">hello</span>
-        </div>""")
-        self.assertEqual("""<div>
-          <span>hello</span>
-        </div>""", str(tmpl.generate()))
-
-    def test_nesting(self):
-        """
-        Verify that `py:choose` blocks can be nested:
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:choose="1">
-            <div py:when="1" py:choose="3">
-              <span py:when="2">2</span>
-              <span py:when="3">3</span>
-            </div>
-          </div>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <div>
-            <div>
-              <span>3</span>
-            </div>
-          </div>
-        </doc>""", str(tmpl.generate()))
-
-    def test_complex_nesting(self):
-        """
-        Verify more complex nesting.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:choose="1">
-            <div py:when="1" py:choose="">
-              <span py:when="2">OK</span>
-              <span py:when="1">FAIL</span>
-            </div>
-          </div>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <div>
-            <div>
-              <span>OK</span>
-            </div>
-          </div>
-        </doc>""", str(tmpl.generate()))
-
-    def test_complex_nesting_otherwise(self):
-        """
-        Verify more complex nesting using otherwise.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:choose="1">
-            <div py:when="1" py:choose="2">
-              <span py:when="1">FAIL</span>
-              <span py:otherwise="">OK</span>
-            </div>
-          </div>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <div>
-            <div>
-              <span>OK</span>
-            </div>
-          </div>
-        </doc>""", str(tmpl.generate()))
-
-    def test_when_with_strip(self):
-        """
-        Verify that a when directive with a strip directive actually strips of
-        the outer element.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:choose="" py:strip="">
-            <span py:otherwise="">foo</span>
-          </div>
-        </doc>""")
-        self.assertEqual("""<doc>
-            <span>foo</span>
-        </doc>""", str(tmpl.generate()))
-
-    def test_when_outside_choose(self):
-        """
-        Verify that a `when` directive outside of a `choose` directive is
-        reported as an error.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:when="xy" />
-        </doc>""")
-        self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
-
-    def test_otherwise_outside_choose(self):
-        """
-        Verify that an `otherwise` directive outside of a `choose` directive is
-        reported as an error.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:otherwise="" />
-        </doc>""")
-        self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
-
-    def test_when_without_test(self):
-        """
-        Verify that an `when` directive that doesn't have a `test` attribute
-        is reported as an error.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:choose="" py:strip="">
-            <py:when>foo</py:when>
-          </div>
-        </doc>""")
-        self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
-
-    def test_when_without_test_but_with_choose_value(self):
-        """
-        Verify that an `when` directive that doesn't have a `test` attribute
-        works as expected as long as the parent `choose` directive has a test
-        expression.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:choose="foo" py:strip="">
-            <py:when>foo</py:when>
-          </div>
-        </doc>""")
-        self.assertEqual("""<doc>
-            foo
-        </doc>""", str(tmpl.generate(foo='Yeah')))
-
-    def test_otherwise_without_test(self):
-        """
-        Verify that an `otherwise` directive can be used without a `test`
-        attribute.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:choose="" py:strip="">
-            <py:otherwise>foo</py:otherwise>
-          </div>
-        </doc>""")
-        self.assertEqual("""<doc>
-            foo
-        </doc>""", str(tmpl.generate()))
-
-    def test_as_element(self):
-        """
-        Verify that the directive can also be used as an element.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:choose>
-            <py:when test="1 == 1">1</py:when>
-            <py:when test="2 == 2">2</py:when>
-            <py:when test="3 == 3">3</py:when>
-          </py:choose>
-        </doc>""")
-        self.assertEqual("""<doc>
-            1
-        </doc>""", str(tmpl.generate()))
-
-    def test_in_text_template(self):
-        """
-        Verify that the directive works as expected in a text template.
-        """
-        tmpl = TextTemplate("""#choose
-          #when 1 == 1
-            1
-          #end
-          #when 2 == 2
-            2
-          #end
-          #when 3 == 3
-            3
-          #end
-        #end""")
-        self.assertEqual("""            1\n""", str(tmpl.generate()))
-
-
-class DefDirectiveTestCase(unittest.TestCase):
-    """Tests for the `py:def` template directive."""
-
-    def test_function_with_strip(self):
-        """
-        Verify that a named template function with a strip directive actually
-        strips of the outer element.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:def="echo(what)" py:strip="">
-            <b>${what}</b>
-          </div>
-          ${echo('foo')}
-        </doc>""")
-        self.assertEqual("""<doc>
-            <b>foo</b>
-        </doc>""", str(tmpl.generate()))
-
-    def test_exec_in_replace(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <p py:def="echo(greeting, name='world')" class="message">
-            ${greeting}, ${name}!
-          </p>
-          <div py:replace="echo('hello')"></div>
-        </div>""")
-        self.assertEqual("""<div>
-          <p class="message">
-            hello, world!
-          </p>
-        </div>""", str(tmpl.generate()))
-
-    def test_as_element(self):
-        """
-        Verify that the directive can also be used as an element.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:def function="echo(what)">
-            <b>${what}</b>
-          </py:def>
-          ${echo('foo')}
-        </doc>""")
-        self.assertEqual("""<doc>
-            <b>foo</b>
-        </doc>""", str(tmpl.generate()))
-
-    def test_nested_defs(self):
-        """
-        Verify that a template function defined inside a conditional block can
-        be called from outside that block.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:if test="semantic">
-            <strong py:def="echo(what)">${what}</strong>
-          </py:if>
-          <py:if test="not semantic">
-            <b py:def="echo(what)">${what}</b>
-          </py:if>
-          ${echo('foo')}
-        </doc>""")
-        self.assertEqual("""<doc>
-          <strong>foo</strong>
-        </doc>""", str(tmpl.generate(semantic=True)))
-
-    def test_function_with_default_arg(self):
-        """
-        Verify that keyword arguments work with `py:def` directives.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <b py:def="echo(what, bold=False)" py:strip="not bold">${what}</b>
-          ${echo('foo')}
-        </doc>""")
-        self.assertEqual("""<doc>
-          foo
-        </doc>""", str(tmpl.generate()))
-
-    def test_invocation_in_attribute(self):
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:def function="echo(what)">${what or 'something'}</py:def>
-          <p class="${echo('foo')}">bar</p>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <p class="foo">bar</p>
-        </doc>""", str(tmpl.generate()))
-
-    def test_invocation_in_attribute_none(self):
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:def function="echo()">${None}</py:def>
-          <p class="${echo()}">bar</p>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <p>bar</p>
-        </doc>""", str(tmpl.generate()))
-
-    def test_function_raising_typeerror(self):
-        def badfunc():
-            raise TypeError
-        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
-          <div py:def="dobadfunc()">
-            ${badfunc()}
-          </div>
-          <div py:content="dobadfunc()"/>
-        </html>""")
-        self.assertRaises(TypeError, list, tmpl.generate(badfunc=badfunc))
-
-    def test_def_in_matched(self):
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <head py:match="head">${select('*')}</head>
-          <head>
-            <py:def function="maketitle(test)"><b py:replace="test" /></py:def>
-            <title>${maketitle(True)}</title>
-          </head>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <head><title>True</title></head>
-        </doc>""", str(tmpl.generate()))
-
-    def test_in_text_template(self):
-        """
-        Verify that the directive works as expected in a text template.
-        """
-        tmpl = TextTemplate("""
-          #def echo(greeting, name='world')
-            ${greeting}, ${name}!
-          #end
-          ${echo('Hi', name='you')}
-        """)
-        self.assertEqual("""                      Hi, you!
-        """, str(tmpl.generate()))
-
-
-class ForDirectiveTestCase(unittest.TestCase):
-    """Tests for the `py:for` template directive."""
-
-    def test_loop_with_strip(self):
-        """
-        Verify that the combining the `py:for` directive with `py:strip` works
-        correctly.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:for="item in items" py:strip="">
-            <b>${item}</b>
-          </div>
-        </doc>""")
-        self.assertEqual("""<doc>
-            <b>1</b>
-            <b>2</b>
-            <b>3</b>
-            <b>4</b>
-            <b>5</b>
-        </doc>""", str(tmpl.generate(items=range(1, 6))))
-
-    def test_as_element(self):
-        """
-        Verify that the directive can also be used as an element.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:for each="item in items">
-            <b>${item}</b>
-          </py:for>
-        </doc>""")
-        self.assertEqual("""<doc>
-            <b>1</b>
-            <b>2</b>
-            <b>3</b>
-            <b>4</b>
-            <b>5</b>
-        </doc>""", str(tmpl.generate(items=range(1, 6))))
-
-    def test_multi_assignment(self):
-        """
-        Verify that assignment to tuples works correctly.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:for each="k, v in items">
-            <p>key=$k, value=$v</p>
-          </py:for>
-        </doc>""")
-        self.assertEqual("""<doc>
-            <p>key=a, value=1</p>
-            <p>key=b, value=2</p>
-        </doc>""", str(tmpl.generate(items=dict(a=1, b=2).items())))
-
-    def test_nested_assignment(self):
-        """
-        Verify that assignment to nested tuples works correctly.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:for each="idx, (k, v) in items">
-            <p>$idx: key=$k, value=$v</p>
-          </py:for>
-        </doc>""")
-        self.assertEqual("""<doc>
-            <p>0: key=a, value=1</p>
-            <p>1: key=b, value=2</p>
-        </doc>""", str(tmpl.generate(items=enumerate(dict(a=1, b=2).items()))))
-
-    def test_not_iterable(self):
-        """
-        Verify that assignment to nested tuples works correctly.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:for each="item in foo">
-            $item
-          </py:for>
-        </doc>""", filename='test.html')
-        try:
-            list(tmpl.generate(foo=12))
-        except TemplateRuntimeError, e:
-            self.assertEqual('test.html', e.filename)
-            if sys.version_info[:2] >= (2, 4):
-                self.assertEqual(2, e.lineno)
-
-
-class IfDirectiveTestCase(unittest.TestCase):
-    """Tests for the `py:if` template directive."""
-
-    def test_loop_with_strip(self):
-        """
-        Verify that the combining the `py:if` directive with `py:strip` works
-        correctly.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <b py:if="foo" py:strip="">${bar}</b>
-        </doc>""")
-        self.assertEqual("""<doc>
-          Hello
-        </doc>""", str(tmpl.generate(foo=True, bar='Hello')))
-
-    def test_as_element(self):
-        """
-        Verify that the directive can also be used as an element.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:if test="foo">${bar}</py:if>
-        </doc>""")
-        self.assertEqual("""<doc>
-          Hello
-        </doc>""", str(tmpl.generate(foo=True, bar='Hello')))
-
-
-class MatchDirectiveTestCase(unittest.TestCase):
-    """Tests for the `py:match` template directive."""
-
-    def test_with_strip(self):
-        """
-        Verify that a match template can produce the same kind of element that
-        it matched without entering an infinite recursion.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <elem py:match="elem" py:strip="">
-            <div class="elem">${select('text()')}</div>
-          </elem>
-          <elem>Hey Joe</elem>
-        </doc>""")
-        self.assertEqual("""<doc>
-            <div class="elem">Hey Joe</div>
-        </doc>""", str(tmpl.generate()))
-
-    def test_without_strip(self):
-        """
-        Verify that a match template can produce the same kind of element that
-        it matched without entering an infinite recursion.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <elem py:match="elem">
-            <div class="elem">${select('text()')}</div>
-          </elem>
-          <elem>Hey Joe</elem>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <elem>
-            <div class="elem">Hey Joe</div>
-          </elem>
-        </doc>""", str(tmpl.generate()))
-
-    def test_as_element(self):
-        """
-        Verify that the directive can also be used as an element.
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:match path="elem">
-            <div class="elem">${select('text()')}</div>
-          </py:match>
-          <elem>Hey Joe</elem>
-        </doc>""")
-        self.assertEqual("""<doc>
-            <div class="elem">Hey Joe</div>
-        </doc>""", str(tmpl.generate()))
-
-    def test_recursive_match_1(self):
-        """
-        Match directives are applied recursively, meaning that they are also
-        applied to any content they may have produced themselves:
-        """
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <elem py:match="elem">
-            <div class="elem">
-              ${select('*')}
-            </div>
-          </elem>
-          <elem>
-            <subelem>
-              <elem/>
-            </subelem>
-          </elem>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <elem>
-            <div class="elem">
-              <subelem>
-              <elem>
-            <div class="elem">
-            </div>
-          </elem>
-            </subelem>
-            </div>
-          </elem>
-        </doc>""", str(tmpl.generate()))
-
-    def test_recursive_match_2(self):
-        """
-        When two or more match templates match the same element and also
-        themselves output the element they match, avoiding recursion is even
-        more complex, but should work.
-        """
-        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
-          <body py:match="body">
-            <div id="header"/>
-            ${select('*')}
-          </body>
-          <body py:match="body">
-            ${select('*')}
-            <div id="footer"/>
-          </body>
-          <body>
-            <h1>Foo</h1>
-          </body>
-        </html>""")
-        self.assertEqual("""<html>
-          <body>
-            <div id="header"/><h1>Foo</h1>
-            <div id="footer"/>
-          </body>
-        </html>""", str(tmpl.generate()))
-
-    def test_select_all_attrs(self):
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:match="elem" py:attrs="select('@*')">
-            ${select('text()')}
-          </div>
-          <elem id="joe">Hey Joe</elem>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <div id="joe">
-            Hey Joe
-          </div>
-        </doc>""", str(tmpl.generate()))
-
-    def test_select_all_attrs_empty(self):
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:match="elem" py:attrs="select('@*')">
-            ${select('text()')}
-          </div>
-          <elem>Hey Joe</elem>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <div>
-            Hey Joe
-          </div>
-        </doc>""", str(tmpl.generate()))
-
-    def test_select_all_attrs_in_body(self):
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <div py:match="elem">
-            Hey ${select('text()')} ${select('@*')}
-          </div>
-          <elem title="Cool">Joe</elem>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <div>
-            Hey Joe Cool
-          </div>
-        </doc>""", str(tmpl.generate()))
-
-    def test_def_in_match(self):
-        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
-          <py:def function="maketitle(test)"><b py:replace="test" /></py:def>
-          <head py:match="head">${select('*')}</head>
-          <head><title>${maketitle(True)}</title></head>
-        </doc>""")
-        self.assertEqual("""<doc>
-          <head><title>True</title></head>
-        </doc>""", str(tmpl.generate()))
-
-    def test_match_with_xpath_variable(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <span py:match="*[name()=$tagname]">
-            Hello ${select('@name')}
-          </span>
-          <greeting name="Dude"/>
-        </div>""")
-        self.assertEqual("""<div>
-          <span>
-            Hello Dude
-          </span>
-        </div>""", str(tmpl.generate(tagname='greeting')))
-        self.assertEqual("""<div>
-          <greeting name="Dude"/>
-        </div>""", str(tmpl.generate(tagname='sayhello')))
-
-    def test_content_directive_in_match(self):
-        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
-          <div py:match="foo">I said <q py:content="select('text()')">something</q>.</div>
-          <foo>bar</foo>
-        </html>""")
-        self.assertEqual("""<html>
-          <div>I said <q>bar</q>.</div>
-        </html>""", str(tmpl.generate()))
-
-    def test_cascaded_matches(self):
-        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
-          <body py:match="body">${select('*')}</body>
-          <head py:match="head">${select('title')}</head>
-          <body py:match="body">${select('*')}<hr /></body>
-          <head><title>Welcome to Markup</title></head>
-          <body><h2>Are you ready to mark up?</h2></body>
-        </html>""")
-        self.assertEqual("""<html>
-          <head><title>Welcome to Markup</title></head>
-          <body><h2>Are you ready to mark up?</h2><hr/></body>
-        </html>""", str(tmpl.generate()))
-
-    def test_multiple_matches(self):
-        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
-          <input py:match="form//input" py:attrs="select('@*')"
-                 value="${values[str(select('@name'))]}" />
-          <form><p py:for="field in fields">
-            <label>${field.capitalize()}</label>
-            <input type="text" name="${field}" />
-          </p></form>
-        </html>""")
-        fields = ['hello_%s' % i for i in range(5)]
-        values = dict([('hello_%s' % i, i) for i in range(5)])
-        self.assertEqual("""<html>
-          <form><p>
-            <label>Hello_0</label>
-            <input value="0" type="text" name="hello_0"/>
-          </p><p>
-            <label>Hello_1</label>
-            <input value="1" type="text" name="hello_1"/>
-          </p><p>
-            <label>Hello_2</label>
-            <input value="2" type="text" name="hello_2"/>
-          </p><p>
-            <label>Hello_3</label>
-            <input value="3" type="text" name="hello_3"/>
-          </p><p>
-            <label>Hello_4</label>
-            <input value="4" type="text" name="hello_4"/>
-          </p></form>
-        </html>""", str(tmpl.generate(fields=fields, values=values)))
-
-    def test_namespace_context(self):
-        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
-                                       xmlns:x="http://www.example.org/">
-          <div py:match="x:foo">Foo</div>
-          <foo xmlns="http://www.example.org/"/>
-        </html>""")
-        # FIXME: there should be a way to strip out unwanted/unused namespaces,
-        #        such as the "x" in this example
-        self.assertEqual("""<html xmlns:x="http://www.example.org/">
-          <div>Foo</div>
-        </html>""", str(tmpl.generate()))
-
-    def test_match_with_position_predicate(self):
-        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
-          <p py:match="body/p[1]" class="first">${select('*|text()')}</p>
-          <body>
-            <p>Foo</p>
-            <p>Bar</p>
-          </body>
-        </html>""")
-        self.assertEqual("""<html>
-          <body>
-            <p class="first">Foo</p>
-            <p>Bar</p>
-          </body>
-        </html>""", str(tmpl.generate()))
-
-    def test_match_with_closure(self):
-        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
-          <p py:match="body//p" class="para">${select('*|text()')}</p>
-          <body>
-            <p>Foo</p>
-            <div><p>Bar</p></div>
-          </body>
-        </html>""")
-        self.assertEqual("""<html>
-          <body>
-            <p class="para">Foo</p>
-            <div><p class="para">Bar</p></div>
-          </body>
-        </html>""", str(tmpl.generate()))
-
-    def test_match_without_closure(self):
-        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
-          <p py:match="body/p" class="para">${select('*|text()')}</p>
-          <body>
-            <p>Foo</p>
-            <div><p>Bar</p></div>
-          </body>
-        </html>""")
-        self.assertEqual("""<html>
-          <body>
-            <p class="para">Foo</p>
-            <div><p>Bar</p></div>
-          </body>
-        </html>""", str(tmpl.generate()))
-
-    # FIXME
-    #def test_match_after_step(self):
-    #    tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-    #      <span py:match="div/greeting">
-    #        Hello ${select('@name')}
-    #      </span>
-    #      <greeting name="Dude" />
-    #    </div>""")
-    #    self.assertEqual("""<div>
-    #      <span>
-    #        Hello Dude
-    #      </span>
-    #    </div>""", str(tmpl.generate()))
-
-
-class StripDirectiveTestCase(unittest.TestCase):
-    """Tests for the `py:strip` template directive."""
-
-    def test_strip_false(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <div py:strip="False"><b>foo</b></div>
-        </div>""")
-        self.assertEqual("""<div>
-          <div><b>foo</b></div>
-        </div>""", str(tmpl.generate()))
-
-    def test_strip_empty(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <div py:strip=""><b>foo</b></div>
-        </div>""")
-        self.assertEqual("""<div>
-          <b>foo</b>
-        </div>""", str(tmpl.generate()))
-
-
-class WithDirectiveTestCase(unittest.TestCase):
-    """Tests for the `py:with` template directive."""
-
-    def test_shadowing(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          ${x}
-          <span py:with="x = x * 2" py:replace="x"/>
-          ${x}
-        </div>""")
-        self.assertEqual("""<div>
-          42
-          84
-          42
-        </div>""", str(tmpl.generate(x=42)))
-
-    def test_as_element(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <py:with vars="x = x * 2">${x}</py:with>
-        </div>""")
-        self.assertEqual("""<div>
-          84
-        </div>""", str(tmpl.generate(x=42)))
-
-    def test_multiple_vars_same_name(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <py:with vars="
-            foo = 'bar';
-            foo = foo.replace('r', 'z')
-          ">
-            $foo
-          </py:with>
-        </div>""")
-        self.assertEqual("""<div>
-            baz
-        </div>""", str(tmpl.generate(x=42)))
-
-    def test_multiple_vars_single_assignment(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <py:with vars="x = y = z = 1">${x} ${y} ${z}</py:with>
-        </div>""")
-        self.assertEqual("""<div>
-          1 1 1
-        </div>""", str(tmpl.generate(x=42)))
-
-    def test_nested_vars_single_assignment(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <py:with vars="x, (y, z) = (1, (2, 3))">${x} ${y} ${z}</py:with>
-        </div>""")
-        self.assertEqual("""<div>
-          1 2 3
-        </div>""", str(tmpl.generate(x=42)))
-
-    def test_multiple_vars_trailing_semicolon(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <py:with vars="x = x * 2; y = x / 2;">${x} ${y}</py:with>
-        </div>""")
-        self.assertEqual("""<div>
-          84 42
-        </div>""", str(tmpl.generate(x=42)))
-
-    def test_semicolon_escape(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <py:with vars="x = 'here is a semicolon: ;'; y = 'here are two semicolons: ;;' ;">
-            ${x}
-            ${y}
-          </py:with>
-        </div>""")
-        self.assertEqual("""<div>
-            here is a semicolon: ;
-            here are two semicolons: ;;
-        </div>""", str(tmpl.generate()))
-
-    def test_unicode_expr(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <span py:with="weeks=(u'一', u'二', u'三', u'四', u'五', u'六', u'日')">
-            $weeks
-          </span>
-        </div>""")
-        self.assertEqual("""<div>
-          <span>
-            一二三四五六日
-          </span>
-        </div>""", str(tmpl.generate()))
-
-
-class TemplateTestCase(unittest.TestCase):
-    """Tests for basic template processing, expression evaluation and error
-    reporting.
-    """
-
-    def test_interpolate_string(self):
-        parts = list(Template._interpolate('bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('bla', parts[0][1])
-
-    def test_interpolate_simple(self):
-        parts = list(Template._interpolate('${bla}'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('bla', parts[0][1].source)
-
-    def test_interpolate_escaped(self):
-        parts = list(Template._interpolate('$${bla}'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('${bla}', parts[0][1])
-
-    def test_interpolate_short(self):
-        parts = list(Template._interpolate('$bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('bla', parts[0][1].source)
-
-    def test_interpolate_short_starting_with_underscore(self):
-        parts = list(Template._interpolate('$_bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('_bla', parts[0][1].source)
-
-    def test_interpolate_short_containing_underscore(self):
-        parts = list(Template._interpolate('$foo_bar'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('foo_bar', parts[0][1].source)
-
-    def test_interpolate_short_starting_with_dot(self):
-        parts = list(Template._interpolate('$.bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('$.bla', parts[0][1])
-
-    def test_interpolate_short_containing_dot(self):
-        parts = list(Template._interpolate('$foo.bar'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('foo.bar', parts[0][1].source)
-
-    def test_interpolate_short_starting_with_digit(self):
-        parts = list(Template._interpolate('$0bla'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('$0bla', parts[0][1])
-
-    def test_interpolate_short_containing_digit(self):
-        parts = list(Template._interpolate('$foo0'))
-        self.assertEqual(1, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('foo0', parts[0][1].source)
-
-    def test_interpolate_mixed1(self):
-        parts = list(Template._interpolate('$foo bar $baz'))
-        self.assertEqual(3, len(parts))
-        self.assertEqual(Template.EXPR, parts[0][0])
-        self.assertEqual('foo', parts[0][1].source)
-        self.assertEqual(Stream.TEXT, parts[1][0])
-        self.assertEqual(' bar ', parts[1][1])
-        self.assertEqual(Template.EXPR, parts[2][0])
-        self.assertEqual('baz', parts[2][1].source)
-
-    def test_interpolate_mixed2(self):
-        parts = list(Template._interpolate('foo $bar baz'))
-        self.assertEqual(3, len(parts))
-        self.assertEqual(Stream.TEXT, parts[0][0])
-        self.assertEqual('foo ', parts[0][1])
-        self.assertEqual(Template.EXPR, parts[1][0])
-        self.assertEqual('bar', parts[1][1].source)
-        self.assertEqual(Stream.TEXT, parts[2][0])
-        self.assertEqual(' baz', parts[2][1])
-
-
-class MarkupTemplateTestCase(unittest.TestCase):
-    """Tests for markup template processing."""
-
-    def test_interpolate_mixed3(self):
-        tmpl = MarkupTemplate('<root> ${var} $var</root>')
-        self.assertEqual('<root> 42 42</root>', str(tmpl.generate(var=42)))
-
-    def test_interpolate_leading_trailing_space(self):
-        tmpl = MarkupTemplate('<root>${    foo    }</root>')
-        self.assertEqual('<root>bar</root>', str(tmpl.generate(foo='bar')))
-
-    def test_interpolate_multiline(self):
-        tmpl = MarkupTemplate("""<root>${dict(
-          bar = 'baz'
-        )[foo]}</root>""")
-        self.assertEqual('<root>baz</root>', str(tmpl.generate(foo='bar')))
-
-    def test_interpolate_non_string_attrs(self):
-        tmpl = MarkupTemplate('<root attr="${1}"/>')
-        self.assertEqual('<root attr="1"/>', str(tmpl.generate()))
-
-    def test_interpolate_list_result(self):
-        tmpl = MarkupTemplate('<root>$foo</root>')
-        self.assertEqual('<root>buzz</root>', str(tmpl.generate(foo=('buzz',))))
-
-    def test_empty_attr(self):
-        tmpl = MarkupTemplate('<root attr=""/>')
-        self.assertEqual('<root attr=""/>', str(tmpl.generate()))
-
-    def test_bad_directive_error(self):
-        xml = '<p xmlns:py="http://genshi.edgewall.org/" py:do="nothing" />'
-        try:
-            tmpl = MarkupTemplate(xml, filename='test.html')
-        except BadDirectiveError, e:
-            self.assertEqual('test.html', e.filename)
-            if sys.version_info[:2] >= (2, 4):
-                self.assertEqual(1, e.lineno)
-
-    def test_directive_value_syntax_error(self):
-        xml = """<p xmlns:py="http://genshi.edgewall.org/" py:if="bar'" />"""
-        try:
-            tmpl = MarkupTemplate(xml, filename='test.html')
-            self.fail('Expected SyntaxError')
-        except TemplateSyntaxError, e:
-            self.assertEqual('test.html', e.filename)
-            if sys.version_info[:2] >= (2, 4):
-                self.assertEqual(1, e.lineno)
-
-    def test_expression_syntax_error(self):
-        xml = """<p>
-          Foo <em>${bar"}</em>
-        </p>"""
-        try:
-            tmpl = MarkupTemplate(xml, filename='test.html')
-            self.fail('Expected SyntaxError')
-        except TemplateSyntaxError, e:
-            self.assertEqual('test.html', e.filename)
-            if sys.version_info[:2] >= (2, 4):
-                self.assertEqual(2, e.lineno)
-
-    def test_expression_syntax_error_multi_line(self):
-        xml = """<p><em></em>
-
- ${bar"}
-
-        </p>"""
-        try:
-            tmpl = MarkupTemplate(xml, filename='test.html')
-            self.fail('Expected SyntaxError')
-        except TemplateSyntaxError, e:
-            self.assertEqual('test.html', e.filename)
-            if sys.version_info[:2] >= (2, 4):
-                self.assertEqual(3, e.lineno)
-
-    def test_markup_noescape(self):
-        """
-        Verify that outputting context data that is a `Markup` instance is not
-        escaped.
-        """
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          $myvar
-        </div>""")
-        self.assertEqual("""<div>
-          <b>foo</b>
-        </div>""", str(tmpl.generate(myvar=Markup('<b>foo</b>'))))
-
-    def test_text_noescape_quotes(self):
-        """
-        Verify that outputting context data in text nodes doesn't escape quotes.
-        """
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          $myvar
-        </div>""")
-        self.assertEqual("""<div>
-          "foo"
-        </div>""", str(tmpl.generate(myvar='"foo"')))
-
-    def test_attr_escape_quotes(self):
-        """
-        Verify that outputting context data in attribtes escapes quotes.
-        """
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <elem class="$myvar"/>
-        </div>""")
-        self.assertEqual("""<div>
-          <elem class="&#34;foo&#34;"/>
-        </div>""", str(tmpl.generate(myvar='"foo"')))
-
-    def test_directive_element(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <py:if test="myvar">bar</py:if>
-        </div>""")
-        self.assertEqual("""<div>
-          bar
-        </div>""", str(tmpl.generate(myvar='"foo"')))
-
-    def test_normal_comment(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <!-- foo bar -->
-        </div>""")
-        self.assertEqual("""<div>
-          <!-- foo bar -->
-        </div>""", str(tmpl.generate()))
-
-    def test_template_comment(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <!-- !foo -->
-          <!--!bar-->
-        </div>""")
-        self.assertEqual("""<div>
-        </div>""", str(tmpl.generate()))
-
-    def test_parse_with_same_namespace_nested(self):
-        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
-          <span xmlns:py="http://genshi.edgewall.org/">
-          </span>
-        </div>""")
-        self.assertEqual("""<div>
-          <span>
-          </span>
-        </div>""", str(tmpl.generate()))
-
-    def test_latin1_encoded_with_xmldecl(self):
-        tmpl = MarkupTemplate(u"""<?xml version="1.0" encoding="iso-8859-1" ?>
-        <div xmlns:py="http://genshi.edgewall.org/">
-          \xf6
-        </div>""".encode('iso-8859-1'), encoding='iso-8859-1')
-        self.assertEqual(u"""<div>
-          \xf6
-        </div>""", unicode(tmpl.generate()))
-
-    def test_latin1_encoded_explicit_encoding(self):
-        tmpl = MarkupTemplate(u"""<div xmlns:py="http://genshi.edgewall.org/">
-          \xf6
-        </div>""".encode('iso-8859-1'), encoding='iso-8859-1')
-        self.assertEqual(u"""<div>
-          \xf6
-        </div>""", unicode(tmpl.generate()))
-
-
-class TextTemplateTestCase(unittest.TestCase):
-    """Tests for text template processing."""
-
-    def test_escaping(self):
-        tmpl = TextTemplate('\\#escaped')
-        self.assertEqual('#escaped', str(tmpl.generate()))
-
-    def test_comment(self):
-        tmpl = TextTemplate('## a comment')
-        self.assertEqual('', str(tmpl.generate()))
-
-    def test_comment_escaping(self):
-        tmpl = TextTemplate('\\## escaped comment')
-        self.assertEqual('## escaped comment', str(tmpl.generate()))
-
-    def test_end_with_args(self):
-        tmpl = TextTemplate("""
-        #if foo
-          bar
-        #end 'if foo'""")
-        self.assertEqual('', str(tmpl.generate()))
-
-    def test_latin1_encoded(self):
-        text = u'$foo\xf6$bar'.encode('iso-8859-1')
-        tmpl = TextTemplate(text, encoding='iso-8859-1')
-        self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y')))
-
-    # FIXME
-    #def test_empty_lines(self):
-    #    tmpl = TextTemplate("""Your items:
-    #
-    #    #for item in items
-    #      * ${item}
-    #
-    #    #end""")
-    #    self.assertEqual("""Your items:
-    #      * 0
-    #      * 1
-    #      * 2
-    #    """, tmpl.generate(items=range(3)).render('text'))
-
-
-class TemplateLoaderTestCase(unittest.TestCase):
-    """Tests for the template loader."""
-
-    def setUp(self):
-        self.dirname = tempfile.mkdtemp(suffix='markup_test')
-
-    def tearDown(self):
-        shutil.rmtree(self.dirname)
-
-    def test_search_path_empty(self):
-        loader = TemplateLoader()
-        self.assertEqual([], loader.search_path)
-
-    def test_search_path_as_string(self):
-        loader = TemplateLoader(self.dirname)
-        self.assertEqual([self.dirname], loader.search_path)
-
-    def test_relative_include_samedir(self):
-        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
-        try:
-            file1.write("""<div>Included</div>""")
-        finally:
-            file1.close()
-
-        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
-        try:
-            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
-              <xi:include href="tmpl1.html" />
-            </html>""")
-        finally:
-            file2.close()
-
-        loader = TemplateLoader([self.dirname])
-        tmpl = loader.load('tmpl2.html')
-        self.assertEqual("""<html>
-              <div>Included</div>
-            </html>""", tmpl.generate().render())
-
-    def test_relative_include_subdir(self):
-        os.mkdir(os.path.join(self.dirname, 'sub'))
-        file1 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w')
-        try:
-            file1.write("""<div>Included</div>""")
-        finally:
-            file1.close()
-
-        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
-        try:
-            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
-              <xi:include href="sub/tmpl1.html" />
-            </html>""")
-        finally:
-            file2.close()
-
-        loader = TemplateLoader([self.dirname])
-        tmpl = loader.load('tmpl2.html')
-        self.assertEqual("""<html>
-              <div>Included</div>
-            </html>""", tmpl.generate().render())
-
-    def test_relative_include_parentdir(self):
-        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
-        try:
-            file1.write("""<div>Included</div>""")
-        finally:
-            file1.close()
-
-        os.mkdir(os.path.join(self.dirname, 'sub'))
-        file2 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w')
-        try:
-            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
-              <xi:include href="../tmpl1.html" />
-            </html>""")
-        finally:
-            file2.close()
-
-        loader = TemplateLoader([self.dirname])
-        tmpl = loader.load('sub/tmpl2.html')
-        self.assertEqual("""<html>
-              <div>Included</div>
-            </html>""", tmpl.generate().render())
-
-    def test_relative_include_without_search_path(self):
-        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
-        try:
-            file1.write("""<div>Included</div>""")
-        finally:
-            file1.close()
-
-        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
-        try:
-            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
-              <xi:include href="tmpl1.html" />
-            </html>""")
-        finally:
-            file2.close()
-
-        loader = TemplateLoader()
-        tmpl = loader.load(os.path.join(self.dirname, 'tmpl2.html'))
-        self.assertEqual("""<html>
-              <div>Included</div>
-            </html>""", tmpl.generate().render())
-
-    def test_relative_include_without_search_path_nested(self):
-        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
-        try:
-            file1.write("""<div>Included</div>""")
-        finally:
-            file1.close()
-
-        file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w')
-        try:
-            file2.write("""<div xmlns:xi="http://www.w3.org/2001/XInclude">
-              <xi:include href="tmpl1.html" />
-            </div>""")
-        finally:
-            file2.close()
-
-        file3 = open(os.path.join(self.dirname, 'tmpl3.html'), 'w')
-        try:
-            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
-              <xi:include href="tmpl2.html" />
-            </html>""")
-        finally:
-            file3.close()
-
-        loader = TemplateLoader()
-        tmpl = loader.load(os.path.join(self.dirname, 'tmpl3.html'))
-        self.assertEqual("""<html>
-              <div>
-              <div>Included</div>
-            </div>
-            </html>""", tmpl.generate().render())
-
-    def test_relative_include_from_inmemory_template(self):
-        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
-        try:
-            file1.write("""<div>Included</div>""")
-        finally:
-            file1.close()
-
-        loader = TemplateLoader([self.dirname])
-        tmpl2 = MarkupTemplate("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
-          <xi:include href="../tmpl1.html" />
-        </html>""", filename='subdir/tmpl2.html', loader=loader)
-
-        self.assertEqual("""<html>
-          <div>Included</div>
-        </html>""", tmpl2.generate().render())
-
-    def test_load_with_default_encoding(self):
-        f = open(os.path.join(self.dirname, 'tmpl.html'), 'w')
-        try:
-            f.write(u'<div>\xf6</div>'.encode('iso-8859-1'))
-        finally:
-            f.close()
-        loader = TemplateLoader([self.dirname], default_encoding='iso-8859-1')
-        loader.load('tmpl.html')
-
-    def test_load_with_explicit_encoding(self):
-        f = open(os.path.join(self.dirname, 'tmpl.html'), 'w')
-        try:
-            f.write(u'<div>\xf6</div>'.encode('iso-8859-1'))
-        finally:
-            f.close()
-        loader = TemplateLoader([self.dirname], default_encoding='utf-8')
-        loader.load('tmpl.html', encoding='iso-8859-1')
-
-
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(doctest.DocTestSuite(template))
-    suite.addTest(unittest.makeSuite(AttrsDirectiveTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ChooseDirectiveTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(DefDirectiveTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ForDirectiveTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(IfDirectiveTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(MatchDirectiveTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(StripDirectiveTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(WithDirectiveTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(TemplateTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(MarkupTemplateTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(TextTemplateTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(TemplateLoaderTestCase, 'test'))
-    return suite
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
--- a/setup.py
+++ b/setup.py
@@ -46,14 +46,14 @@
         'Topic :: Text Processing :: Markup :: XML'
     ],
     keywords = ['python.templating.engines'],
-    packages = ['genshi'],
+    packages = ['genshi', 'genshi.template'],
     test_suite = 'genshi.tests.suite',
 
     extras_require = {'plugin': ['setuptools>=0.6a2']},
     entry_points = """
     [python.templating.engines]
-    genshi = genshi.plugin:MarkupTemplateEnginePlugin[plugin]
-    genshi-markup = genshi.plugin:MarkupTemplateEnginePlugin[plugin]
-    genshi-text = genshi.plugin:TextTemplateEnginePlugin[plugin]
+    genshi = genshi.template.plugin:MarkupTemplateEnginePlugin[plugin]
+    genshi-markup = genshi.template.plugin:MarkupTemplateEnginePlugin[plugin]
+    genshi-text = genshi.template.plugin:TextTemplateEnginePlugin[plugin]
     """,
 )
Copyright (C) 2012-2017 Edgewall Software