changeset 233:88ec2b306296 trunk

* Added implementation of a simple text-based template engine. Closes #47. * Added upgrade instructions.
author cmlenz
date Tue, 12 Sep 2006 13:30:26 +0000
parents 43d3f2d2ec9d
children 39c424b80edd
files UPGRADE.txt examples/bench/bigtable.py examples/transform/run.py genshi/plugin.py genshi/template.py genshi/tests/template.py
diffstat 6 files changed, 472 insertions(+), 197 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/UPGRADE.txt
@@ -0,0 +1,29 @@
+Upgrading Genshi
+================
+
+Upgrading from Markup
+---------------------
+
+Prior to version 0.3, the name of the Genshi project was "Markup". The
+name change means that you will have to adjust your import statements
+and the namespace URI of XML templates, among other things:
+
+ * The package name was changed from "markup" to "genshi". Please
+   adjust any import statements referring to the old package name.
+ * The namespace URI for directives in Genshi XML templates has changed
+   from http://markup.edgewall.org/ to http://genshi.edgewall.org/.
+   Please update the xmlns:py declaration in your template files
+   accordingly.
+
+Furthermore, due to the inclusion of a text-based template language,
+the class:
+
+  `markup.template.Template`
+
+has been renamed to:
+
+  `markup.template.MarkupTemplate`
+
+If you've been using the Template class directly, you'll need to
+update your code (a simple find/replace should do--the API itself
+did not change).
--- a/examples/bench/bigtable.py
+++ b/examples/bench/bigtable.py
@@ -12,7 +12,7 @@
 import cElementTree as cet
 from elementtree import ElementTree as et
 from genshi.builder import tag
-from genshi.template import Template
+from genshi.template import MarkupTemplate
 import neo_cgi
 import neo_cs
 import neo_util
@@ -38,7 +38,7 @@
 table = [dict(a=1,b=2,c=3,d=4,e=5,f=6,g=7,h=8,i=9,j=10)
           for x in range(1000)]
 
-genshi_tmpl = Template("""
+genshi_tmpl = MarkupTemplate("""
 <table xmlns:py="http://genshi.edgewall.org/">
 <tr py:for="row in table">
 <td py:for="c in row.values()" py:content="c"/>
@@ -46,7 +46,7 @@
 </table>
 """)
 
-genshi_tmpl2 = Template("""
+genshi_tmpl2 = MarkupTemplate("""
 <table xmlns:py="http://genshi.edgewall.org/">$table</table>
 """)
 
--- a/examples/transform/run.py
+++ b/examples/transform/run.py
@@ -5,11 +5,11 @@
 import sys
 
 from genshi.input import HTMLParser
-from genshi.template import Context, Template
+from genshi.template import Context, MarkupTemplate
 
 def transform(html_filename, tmpl_filename):
     tmpl_fileobj = open(tmpl_filename)
-    tmpl = Template(tmpl_fileobj, tmpl_filename)
+    tmpl = MarkupTemplate(tmpl_fileobj, tmpl_filename)
     tmpl_fileobj.close()
 
     html_fileobj = open(html_filename)
--- a/genshi/plugin.py
+++ b/genshi/plugin.py
@@ -21,7 +21,7 @@
 from genshi.core import Attrs, Stream, QName
 from genshi.eval import Undefined
 from genshi.input import HTML, XML
-from genshi.template import Context, Template, TemplateLoader
+from genshi.template import Context, MarkupTemplate, Template, TemplateLoader
 
 def ET(element):
     """Converts the given ElementTree element to a markup stream."""
@@ -59,7 +59,7 @@
         a string.
         """
         if template_string is not None:
-            return Template(template_string)
+            return MarkupTemplate(template_string)
 
         divider = templatename.rfind('.')
         if divider >= 0:
--- a/genshi/template.py
+++ b/genshi/template.py
@@ -31,7 +31,8 @@
 from genshi.path import Path
 
 __all__ = ['BadDirectiveError', 'TemplateError', 'TemplateSyntaxError',
-           'TemplateNotFound', 'Template', 'TemplateLoader']
+           'TemplateNotFound', 'MarkupTemplate', 'TextTemplate',
+           'TemplateLoader']
 
 
 class TemplateError(Exception):
@@ -216,7 +217,7 @@
     of `(name, value)` tuples. The items in that dictionary or sequence are
     added as attributes to the element:
     
-    >>> tmpl = Template('''<ul xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
     ...   <li py:attrs="foo">Bar</li>
     ... </ul>''')
     >>> print tmpl.generate(foo={'class': 'collapse'})
@@ -269,7 +270,7 @@
     This directive replaces the content of the element with the result of
     evaluating the value of the `py:content` attribute:
     
-    >>> tmpl = Template('''<ul xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
     ...   <li py:content="bar">Hello</li>
     ... </ul>''')
     >>> print tmpl.generate(bar='Bye')
@@ -305,7 +306,7 @@
     A named template function can be used just like a normal Python function
     from template expressions:
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
     ...   <p py:def="echo(greeting, name='world')" class="message">
     ...     ${greeting}, ${name}!
     ...   </p>
@@ -321,7 +322,7 @@
     If a function does not require parameters, the parenthesis can be omitted
     both when defining and when calling it:
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
     ...   <p py:def="helloworld" class="message">
     ...     Hello, world!
     ...   </p>
@@ -394,7 +395,7 @@
     """Implementation of the `py:for` template directive for repeating an
     element based on an iterable in the context data.
     
-    >>> tmpl = Template('''<ul xmlns:py="http://genshi.edgewall.org/">
+    >>> 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])
@@ -439,7 +440,7 @@
     """Implementation of the `py:if` template directive for conditionally
     excluding elements from being output.
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
     ...   <b py:if="foo">${bar}</b>
     ... </div>''')
     >>> print tmpl.generate(foo=True, bar='Hello')
@@ -460,7 +461,7 @@
 class MatchDirective(Directive):
     """Implementation of the `py:match` template directive.
 
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
     ...   <span py:match="greeting">
     ...     Hello ${select('@name')}
     ...   </span>
@@ -496,7 +497,7 @@
     This directive replaces the element with the result of evaluating the
     value of the `py:replace` attribute:
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
     ...   <span py:replace="bar">Hello</span>
     ... </div>''')
     >>> print tmpl.generate(bar='Bye')
@@ -507,7 +508,7 @@
     This directive is equivalent to `py:content` combined with `py:strip`,
     providing a less verbose way to achieve the same effect:
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
     ...   <span py:content="bar" py:strip="">Hello</span>
     ... </div>''')
     >>> print tmpl.generate(bar='Bye')
@@ -528,7 +529,7 @@
     When the value of the `py:strip` attribute evaluates to `True`, the element
     is stripped from the output
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
     ...   <div py:strip="True"><b>foo</b></div>
     ... </div>''')
     >>> print tmpl.generate()
@@ -541,7 +542,7 @@
     This directive is particulary interesting for named template functions or
     match templates that do not generate a top-level element:
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/">
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
     ...   <div py:def="echo(what)" py:strip="">
     ...     <b>${what}</b>
     ...   </div>
@@ -582,7 +583,7 @@
     If no `py:when` directive is matched then the fallback directive
     `py:otherwise` will be used.
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/"
+    >>> 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>
@@ -596,7 +597,7 @@
     If the `py:choose` directive contains an expression, the nested `py:when`
     directives are tested for equality to the `py:choose` expression:
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/"
+    >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"
     ...   py:choose="2">
     ...   <span py:when="1">1</span>
     ...   <span py:when="2">2</span>
@@ -679,7 +680,7 @@
     """Implementation of the `py:with` template directive, which allows
     shorthand access to variables and expressions.
     
-    >>> tmpl = Template('''<div xmlns:py="http://genshi.edgewall.org/">
+    >>> 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)
@@ -729,11 +730,22 @@
                                          for name, expr in self.vars]))
 
 
+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):
     """Can parse a template and transform it into the corresponding output
     based on context data.
     """
-    NAMESPACE = Namespace('http://genshi.edgewall.org/')
+    __metaclass__ = TemplateMeta
 
     EXPR = StreamEventKind('EXPR') # an expression
     SUB = StreamEventKind('SUB') # a "subprogram"
@@ -750,10 +762,8 @@
                   ('content', ContentDirective),
                   ('attrs', AttrsDirective),
                   ('strip', StripDirective)]
-    _dir_by_name = dict(directives)
-    _dir_order = [directive[1] for directive in directives]
 
-    def __init__(self, source, basedir=None, filename=None):
+    def __init__(self, source, basedir=None, filename=None, loader=None):
         """Initialize a template from either a string or a file-like object."""
         if isinstance(source, basestring):
             self.source = StringIO(source)
@@ -766,107 +776,22 @@
         else:
             self.filepath = None
 
-        self.filters = []
-        self.stream = self.parse()
+        self.filters = [self._flatten, self._eval]
+
+        self.stream = self._parse()
 
     def __repr__(self):
         return '<%s "%s">' % (self.__class__.__name__, self.filename)
 
-    def parse(self):
+    def _parse(self):
         """Parse the template.
         
-        The parsing stage parses the XML template and constructs a list of
+        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 (markup that does not depend on the
-        context data) and actual directives (commands or variable
-        substitution).
+        split up into literal output (text that does not depend on the context
+        data) and directives or expressions.
         """
-        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):
-
-            if kind is START_NS:
-                # Strip out the namespace declaration for template directives
-                prefix, uri = data
-                if uri == self.NAMESPACE:
-                    ns_prefix[prefix] = uri
-                else:
-                    stream.append((kind, data, pos))
-
-            elif kind is END_NS:
-                if data in ns_prefix:
-                    del ns_prefix[data]
-                else:
-                    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, pos[0], pos[1])
-                    value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '')
-                    directives.append(cls(value, *pos))
-                    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, pos[0],
-                                                    pos[1])
-                        directives.append(cls(value, *pos))
-                    else:
-                        if value:
-                            value = list(self._interpolate(value, *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:
-                    directives.sort(lambda a, b: cmp(self._dir_order.index(a.__class__),
-                                                     self._dir_order.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, *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
+        raise NotImplementedError
 
     _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}', re.DOTALL)
     _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z][a-zA-Z0-9_\.]*)')
@@ -931,7 +856,7 @@
             ctxt = Context(**kwargs)
 
         stream = self.stream
-        for filter_ in [self._flatten, self._eval, self._match] + self.filters:
+        for filter_ in self.filters:
             stream = filter_(iter(stream), ctxt)
         return Stream(stream)
 
@@ -1080,6 +1005,290 @@
 SUB = Template.SUB
 
 
+class MarkupTemplate(Template):
+    """Can parse a template and transform it into the corresponding output
+    based on context data.
+    """
+    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):
+        """Initialize a template from either a string or a file-like object."""
+        Template.__init__(self, source, basedir=basedir, filename=filename,
+                          loader=loader)
+
+        self.filters.append(self._match)
+        if loader:
+            from genshi.filters import IncludeFilter
+            self.filters.append(IncludeFilter(loader))
+
+    def _parse(self):
+        """Parse the template.
+        
+        The parsing stage parses the XML template and constructs a list of
+        directives that will be executed in the render stage. The input is
+        split up into literal output (markup that does not depend on the
+        context data) and actual directives (commands or variable
+        substitution).
+        """
+        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):
+
+            if kind is START_NS:
+                # Strip out the namespace declaration for template directives
+                prefix, uri = data
+                if uri == self.NAMESPACE:
+                    ns_prefix[prefix] = uri
+                else:
+                    stream.append((kind, data, pos))
+
+            elif kind is END_NS:
+                if data in ns_prefix:
+                    del ns_prefix[data]
+                else:
+                    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, pos[0], pos[1])
+                    value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '')
+                    directives.append(cls(value, *pos))
+                    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, pos[0],
+                                                    pos[1])
+                        directives.append(cls(value, *pos))
+                    else:
+                        if value:
+                            value = list(self._interpolate(value, *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, *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=None, 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
+        nsprefix = {} # mapping of namespace prefixes to URIs
+
+        tail = []
+        def _strip(stream):
+            depth = 1
+            while 1:
+                kind, data, pos = stream.next()
+                if kind is START:
+                    depth += 1
+                elif kind is END:
+                    depth -= 1
+                if depth > 0:
+                    yield kind, data, pos
+                else:
+                    tail[:] = [(kind, data, pos)]
+                    break
+
+        for kind, data, pos 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 kind not in (START, END):
+                yield kind, data, pos
+                continue
+
+            for idx, (test, path, template, directives) in \
+                    enumerate(match_templates):
+
+                if test(kind, data, pos, nsprefix, 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(kind, data, pos, nsprefix, ctxt)
+
+                    # Consume and store all events until an end event
+                    # corresponding to this start event is encountered
+                    content = [(kind, data, pos)]
+                    content += list(self._match(_strip(stream), ctxt)) + tail
+
+                    kind, data, pos = tail[0]
+                    for test in [mt[0] for mt in match_templates]:
+                        test(kind, data, pos, nsprefix, ctxt)
+
+                    # Make the select() function available in the body of the
+                    # match template
+                    def select(path):
+                        return Stream(content).select(path)
+                    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 kind, data, pos
+
+
+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
+    ... #endfor
+    ... 
+    ... 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),
+                  ('comment', StripDirective),
+                  ('when', WhenDirective),
+                  ('otherwise', OtherwiseDirective),
+                  ('for', ForDirective),
+                  ('if', IfDirective),
+                  ('choose', ChooseDirective),
+                  ('with', WithDirective)]
+
+    _directive_re = re.compile('^\s*#(\w+.*)\n?', re.MULTILINE)
+
+    def _parse(self):
+        stream = [] # list of events of the "compiled" template
+        dirmap = {} # temporary mapping of directives to elements
+        depth = 0
+
+        source = self.source.read()
+        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.filename,
+                                                         lineno, 0):
+                    stream.append((kind, data, pos))
+                lineno += len(text.splitlines())
+
+            text = source[start:end].lstrip().lstrip('#')
+            lineno += len(text.splitlines())
+            directive = text.split(None, 1)
+            if len(directive) > 1:
+                command, value = directive
+            else:
+                command, value = directive[0], None
+
+            if not command.startswith('end'):
+                cls = self._dir_by_name.get(command)
+                if cls is None:
+                    raise BadDirectiveError(command)
+                directive = cls(value, self.filename, lineno, 0)
+                dirmap[depth] = (directive, len(stream))
+                depth += 1
+            else:
+                depth -= 1
+                command = command[3:]
+                if depth in dirmap:
+                    directive, start_offset = dirmap.pop(depth)
+                    substream = stream[start_offset:]
+                    stream[start_offset:] = [(SUB, ([directive], substream),
+                                              (self.filename, lineno, 0))]
+
+            offset = end
+
+        if offset < len(source):
+            text = source[offset:]
+            for kind, data, pos in self._interpolate(text, self.filename,
+                                                     lineno, 0):
+                stream.append((kind, data, pos))
+
+        return stream
+
+
 class TemplateLoader(object):
     """Responsible for loading templates from files on the specified search
     path.
@@ -1100,7 +1309,7 @@
     template file, and returns the corresponding `Template` object:
     
     >>> template = loader.load(os.path.basename(path))
-    >>> isinstance(template, Template)
+    >>> isinstance(template, MarkupTemplate)
     True
     
     Template instances are cached: requesting a template with the same name
@@ -1126,7 +1335,7 @@
         self._cache = {}
         self._mtime = {}
 
-    def load(self, filename, relative_to=None):
+    def load(self, filename, relative_to=None, cls=MarkupTemplate):
         """Load the template with the given name.
         
         If the `filename` parameter is relative, this method searches the search
@@ -1150,9 +1359,8 @@
         @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
         """
-        from genshi.filters import IncludeFilter
-
         if relative_to:
             filename = os.path.join(os.path.dirname(relative_to), filename)
         filename = os.path.normpath(filename)
@@ -1179,8 +1387,8 @@
             try:
                 fileobj = open(filepath, 'U')
                 try:
-                    tmpl = Template(fileobj, basedir=dirname, filename=filename)
-                    tmpl.filters.append(IncludeFilter(self))
+                    tmpl = cls(fileobj, basedir=dirname, filename=filename,
+                               loader=self)
                 finally:
                     fileobj.close()
                 self._cache[filename] = tmpl
--- a/genshi/tests/template.py
+++ b/genshi/tests/template.py
@@ -18,9 +18,10 @@
 import sys
 import tempfile
 
+from genshi import template
 from genshi.core import Markup, Stream
-from genshi.template import BadDirectiveError, Template, TemplateLoader, \
-                            TemplateSyntaxError
+from genshi.template import BadDirectiveError, MarkupTemplate, Template, \
+                            TemplateLoader, TemplateSyntaxError, TextTemplate
 
 
 class AttrsDirectiveTestCase(unittest.TestCase):
@@ -30,7 +31,7 @@
         """
         Verify that the directive has access to the loop variables.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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'}]
@@ -43,7 +44,7 @@
         Verify that an attribute value that evaluates to `None` removes an
         existing attribute of that name.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <elem class="foo" py:attrs="{'class': 'bar'}"/>
         </doc>""")
         self.assertEqual("""<doc>
@@ -55,7 +56,7 @@
         Verify that an attribute value that evaluates to `None` removes an
         existing attribute of that name.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <elem class="foo" py:attrs="{'class': None}"/>
         </doc>""")
         self.assertEqual("""<doc>
@@ -72,7 +73,7 @@
         Verify that, if multiple `py:when` bodies match, only the first is
         output.
         """
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/" py:choose="">
+        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>
@@ -82,7 +83,7 @@
         </div>""", str(tmpl.generate()))
 
     def test_otherwise(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/" py:choose="">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/" py:choose="">
           <span py:when="False">hidden</span>
           <span py:otherwise="">hello</span>
         </div>""")
@@ -94,7 +95,7 @@
         """
         Verify that `py:choose` blocks can be nested:
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -114,7 +115,7 @@
         """
         Verify more complex nesting.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -134,7 +135,7 @@
         """
         Verify more complex nesting using otherwise.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -155,7 +156,7 @@
         Verify that a when directive with a strip directive actually strips of
         the outer element.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:choose="" py:strip="">
             <span py:otherwise="">foo</span>
           </div>
@@ -169,7 +170,7 @@
         Verify that a `when` directive outside of a `choose` directive is
         reported as an error.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:when="xy" />
         </doc>""")
         self.assertRaises(TemplateSyntaxError, str, tmpl.generate())
@@ -179,7 +180,7 @@
         Verify that an `otherwise` directive outside of a `choose` directive is
         reported as an error.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:otherwise="" />
         </doc>""")
         self.assertRaises(TemplateSyntaxError, str, tmpl.generate())
@@ -189,7 +190,7 @@
         Verify that an `when` directive that doesn't have a `test` attribute
         is reported as an error.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:choose="" py:strip="">
             <py:when>foo</py:when>
           </div>
@@ -201,7 +202,7 @@
         Verify that an `otherwise` directive can be used without a `test`
         attribute.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:choose="" py:strip="">
             <py:otherwise>foo</py:otherwise>
           </div>
@@ -214,7 +215,7 @@
         """
         Verify that the directive can also be used as an element.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -225,6 +226,23 @@
             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
+          #endwhen
+          #when 2 == 2
+            2
+          #endwhen
+          #when 3 == 3
+            3
+          #endwhen
+        #endchoose""")
+        self.assertEqual("""            1\n""", str(tmpl.generate()))
+
 
 class DefDirectiveTestCase(unittest.TestCase):
     """Tests for the `py:def` template directive."""
@@ -234,7 +252,7 @@
         Verify that a named template function with a strip directive actually
         strips of the outer element.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:def="echo(what)" py:strip="">
             <b>${what}</b>
           </div>
@@ -245,7 +263,7 @@
         </doc>""", str(tmpl.generate()))
 
     def test_exec_in_replace(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <p py:def="echo(greeting, name='world')" class="message">
             ${greeting}, ${name}!
           </p>
@@ -261,7 +279,7 @@
         """
         Verify that the directive can also be used as an element.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <py:def function="echo(what)">
             <b>${what}</b>
           </py:def>
@@ -276,7 +294,7 @@
         Verify that a template function defined inside a conditional block can
         be called from outside that block.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <py:if test="semantic">
             <strong py:def="echo(what)">${what}</strong>
           </py:if>
@@ -293,7 +311,7 @@
         """
         Verify that keyword arguments work with `py:def` directives.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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>""")
@@ -302,7 +320,7 @@
         </doc>""", str(tmpl.generate()))
 
     def test_invocation_in_attribute(self):
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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>""")
@@ -311,7 +329,7 @@
         </doc>""", str(tmpl.generate()))
 
     def test_invocation_in_attribute_none(self):
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <py:def function="echo()">${None}</py:def>
           <p class="${echo()}">bar</p>
         </doc>""")
@@ -322,7 +340,7 @@
     def test_function_raising_typeerror(self):
         def badfunc():
             raise TypeError
-        tmpl = Template("""<html xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
           <div py:def="dobadfunc()">
             ${badfunc()}
           </div>
@@ -331,7 +349,7 @@
         self.assertRaises(TypeError, list, tmpl.generate(badfunc=badfunc))
 
     def test_def_in_matched(self):
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -342,6 +360,19 @@
           <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}!
+          #enddef
+          ${echo('Hi', name='you')}
+        """)
+        self.assertEqual("""                      Hi, you!
+        """, str(tmpl.generate()))
+
 
 class ForDirectiveTestCase(unittest.TestCase):
     """Tests for the `py:for` template directive."""
@@ -351,7 +382,7 @@
         Verify that the combining the `py:for` directive with `py:strip` works
         correctly.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:for="item in items" py:strip="">
             <b>${item}</b>
           </div>
@@ -368,7 +399,7 @@
         """
         Verify that the directive can also be used as an element.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <py:for each="item in items">
             <b>${item}</b>
           </py:for>
@@ -385,7 +416,7 @@
         """
         Verify that assignment to tuples works correctly.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <py:for each="k, v in items">
             <p>key=$k, value=$v</p>
           </py:for>
@@ -399,7 +430,7 @@
         """
         Verify that assignment to nested tuples works correctly.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -418,7 +449,7 @@
         Verify that the combining the `py:if` directive with `py:strip` works
         correctly.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <b py:if="foo" py:strip="">${bar}</b>
         </doc>""")
         self.assertEqual("""<doc>
@@ -429,7 +460,7 @@
         """
         Verify that the directive can also be used as an element.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <py:if test="foo">${bar}</py:if>
         </doc>""")
         self.assertEqual("""<doc>
@@ -445,7 +476,7 @@
         Verify that a match template can produce the same kind of element that
         it matched without entering an infinite recursion.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <elem py:match="elem" py:strip="">
             <div class="elem">${select('text()')}</div>
           </elem>
@@ -460,7 +491,7 @@
         Verify that a match template can produce the same kind of element that
         it matched without entering an infinite recursion.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <elem py:match="elem">
             <div class="elem">${select('text()')}</div>
           </elem>
@@ -476,7 +507,7 @@
         """
         Verify that the directive can also be used as an element.
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <py:match path="elem">
             <div class="elem">${select('text()')}</div>
           </py:match>
@@ -491,7 +522,7 @@
         Match directives are applied recursively, meaning that they are also
         applied to any content they may have produced themselves:
         """
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <elem py:match="elem">
             <div class="elem">
               ${select('*')}
@@ -522,7 +553,7 @@
         themselves output the element they match, avoiding recursion is even
         more complex, but should work.
         """
-        tmpl = Template("""<html xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/">
           <body py:match="body">
             <div id="header"/>
             ${select('*')}
@@ -543,7 +574,7 @@
         </html>""", str(tmpl.generate()))
 
     def test_select_all_attrs(self):
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:match="elem" py:attrs="select('@*')">
             ${select('text()')}
           </div>
@@ -556,7 +587,7 @@
         </doc>""", str(tmpl.generate()))
 
     def test_select_all_attrs_empty(self):
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:match="elem" py:attrs="select('@*')">
             ${select('text()')}
           </div>
@@ -569,7 +600,7 @@
         </doc>""", str(tmpl.generate()))
 
     def test_select_all_attrs_in_body(self):
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/">
           <div py:match="elem">
             Hey ${select('text()')} ${select('@*')}
           </div>
@@ -582,7 +613,7 @@
         </doc>""", str(tmpl.generate()))
 
     def test_def_in_match(self):
-        tmpl = Template("""<doc xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -592,7 +623,7 @@
         </doc>""", str(tmpl.generate()))
 
     def test_match_with_xpath_variable(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <span py:match="*[name()=$tagname]">
             Hello ${select('@name')}
           </span>
@@ -608,7 +639,7 @@
         </div>""", str(tmpl.generate(tagname='sayhello')))
 
     def test_content_directive_in_match(self):
-        tmpl = Template("""<html xmlns:py="http://genshi.edgewall.org/">
+        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>""")
@@ -617,7 +648,7 @@
         </html>""", str(tmpl.generate()))
 
     def test_cascaded_matches(self):
-        tmpl = Template("""<html xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -630,7 +661,7 @@
         </html>""", str(tmpl.generate()))
 
     def test_multiple_matches(self):
-        tmpl = Template("""<html xmlns:py="http://genshi.edgewall.org/">
+        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">
@@ -661,7 +692,7 @@
 
     # FIXME
     #def test_match_after_step(self):
-    #    tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+    #    tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
     #      <span py:match="div/greeting">
     #        Hello ${select('@name')}
     #      </span>
@@ -678,7 +709,7 @@
     """Tests for the `py:strip` template directive."""
 
     def test_strip_false(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <div py:strip="False"><b>foo</b></div>
         </div>""")
         self.assertEqual("""<div>
@@ -686,7 +717,7 @@
         </div>""", str(tmpl.generate()))
 
     def test_strip_empty(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <div py:strip=""><b>foo</b></div>
         </div>""")
         self.assertEqual("""<div>
@@ -698,7 +729,7 @@
     """Tests for the `py:with` template directive."""
 
     def test_shadowing(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           ${x}
           <span py:with="x = x * 2" py:replace="x"/>
           ${x}
@@ -710,7 +741,7 @@
         </div>""", str(tmpl.generate(x=42)))
 
     def test_as_element(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <py:with vars="x = x * 2">${x}</py:with>
         </div>""")
         self.assertEqual("""<div>
@@ -718,7 +749,7 @@
         </div>""", str(tmpl.generate(x=42)))
 
     def test_multiple_vars_same_name(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <py:with vars="
             foo = 'bar';
             foo = foo.replace('r', 'z')
@@ -731,7 +762,7 @@
         </div>""", str(tmpl.generate(x=42)))
 
     def test_multiple_vars_single_assignment(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -739,7 +770,7 @@
         </div>""", str(tmpl.generate(x=42)))
 
     def test_nested_vars_single_assignment(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -747,7 +778,7 @@
         </div>""", str(tmpl.generate(x=42)))
 
     def test_multiple_vars_trailing_semicolon(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        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>
@@ -755,7 +786,7 @@
         </div>""", str(tmpl.generate(x=42)))
 
     def test_semicolon_escape(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <py:with vars="x = 'here is a semicolon: ;'; y = 'here are two semicolons: ;;' ;">
             ${x}
             ${y}
@@ -816,36 +847,42 @@
         self.assertEqual(Stream.TEXT, parts[2][0])
         self.assertEqual(' baz', parts[2][1])
 
+
+class MarkupTemplateTestCase(unittest.TestCase):
+    """Tests for basic template processing, expression evaluation and error
+    reporting.
+    """
+
     def test_interpolate_mixed3(self):
-        tmpl = Template('<root> ${var} $var</root>')
+        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 = Template('<root>${    foo    }</root>')
+        tmpl = MarkupTemplate('<root>${    foo    }</root>')
         self.assertEqual('<root>bar</root>', str(tmpl.generate(foo='bar')))
 
     def test_interpolate_multiline(self):
-        tmpl = Template("""<root>${dict(
+        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 = Template('<root attr="${1}"/>')
+        tmpl = MarkupTemplate('<root attr="${1}"/>')
         self.assertEqual('<root attr="1"/>', str(tmpl.generate()))
 
     def test_interpolate_list_result(self):
-        tmpl = Template('<root>$foo</root>')
+        tmpl = MarkupTemplate('<root>$foo</root>')
         self.assertEqual('<root>buzz</root>', str(tmpl.generate(foo=('buzz',))))
 
     def test_empty_attr(self):
-        tmpl = Template('<root attr=""/>')
+        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 = Template(xml, filename='test.html')
+            tmpl = MarkupTemplate(xml, filename='test.html')
         except BadDirectiveError, e:
             self.assertEqual('test.html', e.filename)
             if sys.version_info[:2] >= (2, 4):
@@ -854,7 +891,7 @@
     def test_directive_value_syntax_error(self):
         xml = """<p xmlns:py="http://genshi.edgewall.org/" py:if="bar'" />"""
         try:
-            tmpl = Template(xml, filename='test.html')
+            tmpl = MarkupTemplate(xml, filename='test.html')
             self.fail('Expected SyntaxError')
         except TemplateSyntaxError, e:
             self.assertEqual('test.html', e.filename)
@@ -866,7 +903,7 @@
           Foo <em>${bar"}</em>
         </p>"""
         try:
-            tmpl = Template(xml, filename='test.html')
+            tmpl = MarkupTemplate(xml, filename='test.html')
             self.fail('Expected SyntaxError')
         except TemplateSyntaxError, e:
             self.assertEqual('test.html', e.filename)
@@ -880,7 +917,7 @@
 
         </p>"""
         try:
-            tmpl = Template(xml, filename='test.html')
+            tmpl = MarkupTemplate(xml, filename='test.html')
             self.fail('Expected SyntaxError')
         except TemplateSyntaxError, e:
             self.assertEqual('test.html', e.filename)
@@ -892,7 +929,7 @@
         Verify that outputting context data that is a `Markup` instance is not
         escaped.
         """
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           $myvar
         </div>""")
         self.assertEqual("""<div>
@@ -903,7 +940,7 @@
         """
         Verify that outputting context data in text nodes doesn't escape quotes.
         """
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           $myvar
         </div>""")
         self.assertEqual("""<div>
@@ -914,7 +951,7 @@
         """
         Verify that outputting context data in attribtes escapes quotes.
         """
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <elem class="$myvar"/>
         </div>""")
         self.assertEqual("""<div>
@@ -922,7 +959,7 @@
         </div>""", str(tmpl.generate(myvar='"foo"')))
 
     def test_directive_element(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <py:if test="myvar">bar</py:if>
         </div>""")
         self.assertEqual("""<div>
@@ -930,7 +967,7 @@
         </div>""", str(tmpl.generate(myvar='"foo"')))
 
     def test_normal_comment(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <!-- foo bar -->
         </div>""")
         self.assertEqual("""<div>
@@ -938,7 +975,7 @@
         </div>""", str(tmpl.generate()))
 
     def test_template_comment(self):
-        tmpl = Template("""<div xmlns:py="http://genshi.edgewall.org/">
+        tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/">
           <!-- !foo -->
           <!--!bar-->
         </div>""")
@@ -1044,7 +1081,7 @@
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(doctest.DocTestSuite(Template.__module__))
+    suite.addTest(doctest.DocTestSuite(template))
     suite.addTest(unittest.makeSuite(AttrsDirectiveTestCase, 'test'))
     suite.addTest(unittest.makeSuite(ChooseDirectiveTestCase, 'test'))
     suite.addTest(unittest.makeSuite(DefDirectiveTestCase, 'test'))
@@ -1054,6 +1091,7 @@
     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(TemplateLoaderTestCase, 'test'))
     return suite
 
Copyright (C) 2012-2017 Edgewall Software