changeset 69:c40a5dcd2b55 trunk

A couple of minor performance improvements.
author cmlenz
date Mon, 10 Jul 2006 17:37:01 +0000
parents e7f91e75b0e1
children dd73921530e8
files examples/bench/clearsilver/header.cs examples/bench/clearsilver/template.cs examples/bench/kid/base.kid examples/bench/kid/template.kid examples/bench/markup/base.html examples/bench/markup/footer.html examples/bench/markup/header.html examples/bench/markup/template.html examples/bench/run.py markup/core.py markup/eval.py markup/filters.py markup/input.py markup/output.py markup/path.py markup/template.py
diffstat 16 files changed, 186 insertions(+), 140 deletions(-) [+]
line wrap: on
line diff
--- a/examples/bench/clearsilver/header.cs
+++ b/examples/bench/clearsilver/header.cs
@@ -1,3 +1,7 @@
 <div id="header">
   <h1><?cs var:title ?></h1>
 </div>
+
+<?cs def:greeting(name) ?>
+  <p>Hello, <?cs var:name ?>!</p>
+<?cs /def ?>
--- a/examples/bench/clearsilver/template.cs
+++ b/examples/bench/clearsilver/template.cs
@@ -7,12 +7,15 @@
   </head>
   <body>
     <?cs include:"header.cs" ?>
+    <?cs call:greeting('you') ?>
+    <?cs call:greeting('me') ?>
+    <?cs call:greeting('all the others') ?>
     
     <h2>Loop</h2>
     <?cs if:len(items) ?>
       <ul>
         <?cs each:item = items ?>
-          <li><?cs var:item ?></li>
+          <li<?cs if:name(item) == len(items) ?> class="last"<?cs /if ?>><?cs var:item ?></li>
         <?cs /each ?>
       </ul>
     <?cs /if ?>
--- a/examples/bench/kid/base.kid
+++ b/examples/bench/kid/base.kid
@@ -1,8 +1,15 @@
 <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:py="http://purl.org/kid/ns#">
-  <div py:def="header()" id="header">
-    <h1><?cs var:title ?></h1>
-  </div>
-  <div py:def="footer()" id="footer">
-  </div>
+
+  <p py:def="greeting(name)">
+    Hello, ${name}!
+  </p>
+
+  <body py:match="item.tag == '{http://www.w3.org/1999/xhtml}body'" py:strip="">
+    <div id="header">
+      <h1>${title}</h1>
+    </div>
+    ${item}
+    <div id="footer" />
+  </body>
 </html>
--- a/examples/bench/kid/template.kid
+++ b/examples/bench/kid/template.kid
@@ -9,13 +9,14 @@
     <title>${title}</title>
   </head>
   <body>
-    ${header()}
+    <div>${greeting('you')}</div>
+    <div>${greeting('me')}</div>
+    <div>${greeting('all the others')}</div>
     
     <h2>Loop</h2>
     <ul py:if="items">
-      <li py:for="item in items">${item}</li>
+      <li py:for="idx, item in enumerate(items)" py:content="item"
+          class="${idx == len(items) and 'last' or None}" />
     </ul>
-    
-    ${footer()}
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/examples/bench/markup/base.html
@@ -0,0 +1,17 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://markup.edgewall.org/"
+      py:strip="">
+
+  <p py:def="greeting(name)">
+    Hello, ${name}!
+  </p>
+
+  <py:match path="body">
+    <div id="header">
+      <h1>${title}</h1>
+    </div>
+    ${select('*')}
+    <div id="footer" />
+  </py:match>
+
+</html>
deleted file mode 100644
--- a/examples/bench/markup/footer.html
+++ /dev/null
@@ -1,2 +0,0 @@
-<div id="footer">
-</div>
deleted file mode 100644
--- a/examples/bench/markup/header.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<div id="header">
-  <h1>${title}</h1>
-</div>
--- a/examples/bench/markup/template.html
+++ b/examples/bench/markup/template.html
@@ -5,17 +5,20 @@
       xmlns:py="http://markup.edgewall.org/"
       xmlns:xi="http://www.w3.org/2001/XInclude"
       lang="en">
+  <xi:include href="base.html" />
   <head>
     <title>${title}</title>
   </head>
   <body>
-    <xi:include href="header.html" />
+    <div>${greeting('you')}</div>
+    <div>${greeting('me')}</div>
+    <div>${greeting('all the others')}</div>
 
     <h2>Loop</h2>
     <ul py:if="items">
-      <li py:for="item in items" py:content="item"/>
+      <li py:for="idx, item in enumerate(items)" py:content="item"
+          class="${idx == len(items) and 'last' or None}" />
     </ul>
 
-    <xi:include href="footer.html" />
   </body>
 </html>
--- a/examples/bench/run.py
+++ b/examples/bench/run.py
@@ -1,11 +1,12 @@
 from cgi import escape
-from datetime import datetime, timedelta
 import os
 import sys
+import time
+import timeit
 
 def markup(dirname):
     from markup.template import Context, TemplateLoader
-    loader = TemplateLoader([dirname])
+    loader = TemplateLoader([dirname], auto_reload=False)
     template = loader.load('template.html')
     def render():
         ctxt = Context(title='Just a test',
@@ -75,30 +76,26 @@
     except ImportError:
         return None
 
-def main():
+def main(engines):
     basepath = os.path.abspath(os.path.dirname(__file__))
-    for engine in ('markup', 'clearsilver', 'kid'):
+    for engine in engines:
         dirname = os.path.join(basepath, engine)
         print '%s:' % engine.capitalize()
-        func = globals()[engine](dirname)
-        if not func:
-            print 'Skipping %s, not installed?' % engine.capitalize()
-            continue
-        times = []
-        for i in range(100):
-            start = datetime.now()
-            sys.stdout.write('.')
-            sys.stdout.flush()
-            func()
-            times.append(datetime.now() - start)
-
-        print
-        total_ms = sum([t.seconds * 1000 + t.microseconds for t in times])
-        print ' --> timing: %s (avg), %s (min), %s (max)' % (
-              timedelta(microseconds=total_ms / len(times)),
-              timedelta(microseconds=min([t.seconds * 1000 + t.microseconds for t in times])),
-              timedelta(microseconds=max([t.seconds * 1000 + t.microseconds for t in times])))
-        print
+        t = timeit.Timer(setup='from __main__ import %s; render = %s("%s")'
+                               % (engine, engine, dirname),
+                         stmt='render()')
+        print '%.2f ms' % (1000 * t.timeit(number=1000) / 1000)
 
 if __name__ == '__main__':
-    main()
+    engines = [arg for arg in sys.argv[1:] if arg[0] != '-']
+
+    if '-p' in sys.argv:
+        import hotshot, hotshot.stats
+        prof = hotshot.Profile("template.prof")
+        benchtime = prof.runcall(main, engines)
+        stats = hotshot.stats.load("template.prof")
+        stats.strip_dirs()
+        stats.sort_stats('time', 'calls')
+        stats.print_stats()
+    else:
+        main(engines)
--- a/markup/core.py
+++ b/markup/core.py
@@ -129,6 +129,17 @@
         return self.render(encoding=None)
 
 
+START = Stream.START
+END = Stream.END
+TEXT = Stream.TEXT
+PROLOG = Stream.PROLOG
+DOCTYPE = Stream.DOCTYPE
+START_NS = Stream.START_NS
+END_NS = Stream.END_NS
+PI = Stream.PI
+COMMENT = Stream.COMMENT
+
+
 class Attributes(list):
     """Sequence type that stores the attributes of an element.
     
--- a/markup/eval.py
+++ b/markup/eval.py
@@ -69,7 +69,7 @@
     3
     """
     __slots__ = ['source', 'ast']
-    __visitors = {}
+    _visitors = {}
 
     def __init__(self, source):
         """Create the expression.
@@ -97,10 +97,10 @@
         # AST traversal
 
         def _visit(self, node, data):
-            v = self.__visitors.get(node.__class__)
+            v = self._visitors.get(node.__class__)
             if not v:
                 v = getattr(self, '_visit_%s' % node.__class__.__name__.lower())
-                self.__visitors[node.__class__] = v
+                self._visitors[node.__class__] = v
             return v(node, data)
 
         def _visit_expression(self, node, data):
@@ -248,10 +248,10 @@
         # AST traversal
 
         def _visit(self, node, data):
-            v = self.__visitors.get(node.__class__)
+            v = self._visitors.get(node.__class__)
             if not v:
                 v = getattr(self, '_visit_%s' % node.__class__.__name__.lower())
-                self.__visitors[node.__class__] = v
+                self._visitors[node.__class__] = v
             return v(node, data)
 
         def _visit_expression(self, node, data):
--- a/markup/filters.py
+++ b/markup/filters.py
@@ -19,7 +19,8 @@
     from sets import ImmutableSet as frozenset
 import re
 
-from markup.core import Attributes, Markup, Namespace, Stream
+from markup.core import Attributes, Markup, Namespace
+from markup.core import END, END_NS, START, START_NS, TEXT
 from markup.path import Path
 
 __all__ = ['IncludeFilter', 'WhitespaceFilter', 'HTMLSanitizer']
@@ -53,11 +54,11 @@
             ns_prefixes = []
         in_fallback = False
         include_href, fallback_stream = None, None
+        namespace = self.NAMESPACE
 
         for kind, data, pos in stream:
 
-            if kind is Stream.START and data[0] in self.NAMESPACE \
-                    and not in_fallback:
+            if kind is START and not in_fallback and data[0] in namespace:
                 tag, attrib = data
                 if tag.localname == 'include':
                     include_href = attrib.get('href')
@@ -65,7 +66,7 @@
                     in_fallback = True
                     fallback_stream = []
 
-            elif kind is Stream.END and data in self.NAMESPACE:
+            elif kind is END and data in namespace:
                 if data.localname == 'include':
                     try:
                         if not include_href:
@@ -91,10 +92,10 @@
             elif in_fallback:
                 fallback_stream.append((kind, data, pos))
 
-            elif kind is Stream.START_NS and data[1] == self.NAMESPACE:
+            elif kind is START_NS and data[1] == namespace:
                 ns_prefixes.append(data[0])
 
-            elif kind is Stream.END_NS and data in ns_prefixes:
+            elif kind is END_NS and data in ns_prefixes:
                 ns_prefixes.pop()
 
             else:
@@ -104,34 +105,35 @@
 class WhitespaceFilter(object):
     """A filter that removes extraneous white space from the stream.
 
-    Todo:
+    TODO:
      * Support for xml:space
     """
-
     _TRAILING_SPACE = re.compile('[ \t]+(?=\n)')
     _LINE_COLLAPSE = re.compile('\n{2,}')
 
     def __call__(self, stream, ctxt=None):
+        trim_trailing_space = self._TRAILING_SPACE.sub
+        collapse_lines = self._LINE_COLLAPSE.sub
+        mjoin = Markup('').join
+
         textbuf = []
-        prev_kind = None
         for kind, data, pos in stream:
-            if kind is Stream.TEXT:
+            if kind is TEXT:
                 textbuf.append(data)
-            elif prev_kind is Stream.TEXT:
-                text = Markup('').join(textbuf, escape_quotes=False)
-                text = self._TRAILING_SPACE.sub('', text)
-                text = self._LINE_COLLAPSE.sub('\n', text)
-                yield Stream.TEXT, Markup(text), pos
-                del textbuf[:]
-            prev_kind = kind
-            if kind is not Stream.TEXT:
+            else:
+                if textbuf:
+                    text = mjoin(textbuf, escape_quotes=False)
+                    text = trim_trailing_space('', text)
+                    text = collapse_lines('\n', text)
+                    yield TEXT, Markup(text), pos
+                    del textbuf[:]
                 yield kind, data, pos
-
-        if textbuf:
-            text = Markup('').join(textbuf, escape_quotes=False)
-            text = self._TRAILING_SPACE.sub('', text)
-            text = self._LINE_COLLAPSE.sub('\n', text)
-            yield Stream.TEXT, Markup(text), pos
+        else:
+            if textbuf:
+                text = mjoin(textbuf, escape_quotes=False)
+                text = trim_trailing_space('', text)
+                text = collapse_lines('\n', text)
+                yield TEXT, Markup(text), pos
 
 
 class HTMLSanitizer(object):
@@ -168,7 +170,7 @@
         waiting_for = None
 
         for kind, data, pos in stream:
-            if kind is Stream.START:
+            if kind is START:
                 if waiting_for:
                     continue
                 tag, attrib = data
@@ -204,7 +206,7 @@
 
                 yield kind, (tag, new_attrib), pos
 
-            elif kind is Stream.END:
+            elif kind is END:
                 tag = data
                 if waiting_for:
                     if waiting_for == tag:
--- a/markup/input.py
+++ b/markup/input.py
@@ -85,7 +85,7 @@
         try:
             bufsize = 4 * 1024 # 4K
             done = False
-            while True:
+            while 1:
                 while not done and len(self._queue) == 0:
                     data = self.source.read(bufsize)
                     if data == '': # end of data
@@ -194,7 +194,7 @@
         try:
             bufsize = 4 * 1024 # 4K
             done = False
-            while True:
+            while 1:
                 while not done and len(self._queue) == 0:
                     data = self.source.read(bufsize)
                     if data == '': # end of data
--- a/markup/output.py
+++ b/markup/output.py
@@ -20,7 +20,8 @@
 except NameError:
     from sets import ImmutableSet as frozenset
 
-from markup.core import Markup, Namespace, QName, Stream
+from markup.core import Markup, Namespace, QName
+from markup.core import DOCTYPE, START, END, START_NS, END_NS, TEXT
 
 __all__ = ['Serializer', 'XMLSerializer', 'HTMLSerializer']
 
@@ -54,11 +55,11 @@
         stream = _PushbackIterator(stream)
         for kind, data, pos in stream:
 
-            if kind is Stream.DOCTYPE:
+            if kind is DOCTYPE:
                 # FIXME: what if there's no system or public ID in the input?
                 yield Markup('<!DOCTYPE %s "%s" "%s">\n' % data)
 
-            elif kind is Stream.START_NS:
+            elif kind is START_NS:
                 prefix, uri = data
                 if uri not in ns_mapping:
                     ns_mapping[uri] = prefix
@@ -67,7 +68,7 @@
                     else:
                         ns_attrib.append((QName('xmlns:%s' % prefix), uri))
 
-            elif kind is Stream.START:
+            elif kind is START:
                 tag, attrib = data
 
                 tagname = tag.localname
@@ -75,10 +76,10 @@
                     try:
                         prefix = ns_mapping[tag.namespace]
                         if prefix:
-                            tagname = prefix + ':' + tag.localname
+                            tagname = '%s:%s' % (prefix, tag.localname)
                     except KeyError:
                         ns_attrib.append((QName('xmlns'), tag.namespace))
-                buf = ['<', tagname]
+                buf = ['<%s' % tagname]
 
                 if ns_attrib:
                     attrib.extend(ns_attrib)
@@ -88,11 +89,11 @@
                     if attr.namespace:
                         prefix = ns_mapping.get(attr.namespace)
                         if prefix:
-                            attrname = prefix + ':' + attrname
+                            attrname = '%s:%s' % (prefix, attrname)
                     buf.append(' %s="%s"' % (attrname, Markup.escape(value)))
 
                 kind, data, pos = stream.next()
-                if kind is Stream.END:
+                if kind is END:
                     buf.append('/>')
                 else:
                     buf.append('>')
@@ -100,16 +101,16 @@
 
                 yield Markup(''.join(buf))
 
-            elif kind is Stream.END:
+            elif kind is END:
                 tag = data
                 tagname = tag.localname
                 if tag.namespace:
                     prefix = ns_mapping.get(tag.namespace)
                     if prefix:
-                        tagname = prefix + ':' + tag.localname
+                        tagname = '%s:%s' % (prefix, tag.localname)
                 yield Markup('</%s>' % tagname)
 
-            elif kind is Stream.TEXT:
+            elif kind is TEXT:
                 yield Markup.escape(data, quotes=False)
 
 
@@ -137,15 +138,15 @@
         stream = _PushbackIterator(stream)
         for kind, data, pos in stream:
 
-            if kind is Stream.DOCTYPE:
+            if kind is DOCTYPE:
                 yield Markup('<!DOCTYPE %s "%s" "%s">\n' % data)
 
-            elif kind is Stream.START_NS:
+            elif kind is START_NS:
                 prefix, uri = data
                 if uri not in ns_mapping:
                     ns_mapping[uri] = prefix
 
-            elif kind is Stream.START:
+            elif kind is START:
                 tag, attrib = data
                 if tag.namespace and tag not in self.NAMESPACE:
                     continue # not in the HTML namespace, so don't emit
@@ -162,18 +163,18 @@
 
                 if tag.localname in self._EMPTY_ELEMS:
                     kind, data, pos = stream.next()
-                    if kind is not Stream.END:
+                    if kind is not END:
                         stream.pushback((kind, data, pos))
 
                 yield Markup(''.join(buf + ['>']))
 
-            elif kind is Stream.END:
+            elif kind is END:
                 tag = data
                 if tag.namespace and tag not in self.NAMESPACE:
                     continue # not in the HTML namespace, so don't emit
                 yield Markup('</%s>' % tag.localname)
 
-            elif kind is Stream.TEXT:
+            elif kind is TEXT:
                 yield Markup.escape(data, quotes=False)
 
 
--- a/markup/path.py
+++ b/markup/path.py
@@ -114,6 +114,8 @@
         @param stream: the stream to select from
         @return: the substream matching the path, or an empty stream
         """
+        from markup.core import END, START
+
         stream = iter(stream)
         def _generate():
             test = self.test()
@@ -124,7 +126,7 @@
                     depth = 1
                     while depth > 0:
                         ev = stream.next()
-                        depth += {Stream.START: 1, Stream.END: -1}.get(ev[0], 0)
+                        depth += {START: 1, END: -1}.get(ev[0], 0)
                         yield ev
                         test(*ev)
                 elif result:
@@ -149,17 +151,18 @@
         START (u'child', [(u'id', u'1')])
         START (u'child', [(u'id', u'2')])
         """
+        from markup.core import END, START
         stack = [0] # stack of cursors into the location path
 
         def _test(kind, data, pos):
             if not stack:
                 return False
 
-            if kind is Stream.END:
+            elif kind is END:
                 stack.pop()
                 return None
 
-            if kind is Stream.START:
+            elif kind is START:
                 stack.append(stack[-1])
 
             matched = False
@@ -180,7 +183,7 @@
                 else:
                     stack[-1] += 1
 
-            elif kind is Stream.START and not closure:
+            elif kind is START and not closure:
                 # If this step is not a closure, it cannot be matched until the
                 # current element is closed... so we need to move the cursor
                 # back to the last closure and retest that against the current
--- a/markup/template.py
+++ b/markup/template.py
@@ -45,8 +45,9 @@
 from StringIO import StringIO
 
 from markup.core import Attributes, Namespace, Stream, StreamEventKind
+from markup.core import START, END, START_NS, END_NS, TEXT
 from markup.eval import Expression
-from markup.input import HTML, XMLParser, XML
+from markup.input import XMLParser
 from markup.path import Path
 
 __all__ = ['Context', 'BadDirectiveError', 'TemplateError',
@@ -257,7 +258,7 @@
             kind, data, pos = stream.next()
             if kind is Stream.START:
                 yield kind, data, pos # emit start tag
-            yield Template.EXPR, self.expr, pos
+            yield EXPR, self.expr, pos
             previous = stream.next()
             for event in stream:
                 previous = event
@@ -342,8 +343,7 @@
             else:
                 scope[name] = kwargs.pop(name, self.defaults.get(name))
         ctxt.push(**scope)
-        stream = self._apply_directives(self.stream, ctxt, self.directives)
-        for event in stream:
+        for event in self._apply_directives(self.stream, ctxt, self.directives):
             yield event
         ctxt.pop()
 
@@ -479,7 +479,7 @@
 
     def __call__(self, stream, ctxt, directives):
         kind, data, pos = stream.next()
-        yield Template.EXPR, self.expr, pos
+        yield EXPR, self.expr, pos
 
 
 class StripDirective(Directive):
@@ -680,7 +680,7 @@
 
         for kind, data, pos in XMLParser(self.source, filename=self.filename):
 
-            if kind is Stream.START_NS:
+            if kind is START_NS:
                 # Strip out the namespace declaration for template directives
                 prefix, uri = data
                 if uri == self.NAMESPACE:
@@ -688,13 +688,13 @@
                 else:
                     stream.append((kind, data, pos))
 
-            elif kind is Stream.END_NS:
+            elif kind is END_NS:
                 if data in ns_prefix:
                     del ns_prefix[data]
                 else:
                     stream.append((kind, data, pos))
 
-            elif kind is Stream.START:
+            elif kind is START:
                 # Record any directive attributes in start tags
                 tag, attrib = data
                 directives = []
@@ -727,7 +727,7 @@
                 stream.append((kind, (tag, Attributes(new_attrib)), pos))
                 depth += 1
 
-            elif kind is Stream.END:
+            elif kind is END:
                 depth -= 1
                 stream.append((kind, data, pos))
 
@@ -738,10 +738,10 @@
                     substream = stream[start_offset:]
                     if strip:
                         substream = substream[1:-1]
-                    stream[start_offset:] = [(Template.SUB,
-                                              (directives, substream), pos)]
+                    stream[start_offset:] = [(SUB, (directives, substream),
+                                              pos)]
 
-            elif kind is Stream.TEXT:
+            elif kind is TEXT:
                 for kind, data, pos in self._interpolate(data, *pos):
                     stream.append((kind, data, pos))
 
@@ -768,14 +768,14 @@
         def _interpolate(text):
             for idx, group in enumerate(patterns.pop(0).split(text)):
                 if idx % 2:
-                    yield Template.EXPR, Expression(group), (lineno, offset)
+                    yield EXPR, Expression(group), (lineno, offset)
                 elif group:
                     if patterns:
                         for result in _interpolate(group):
                             yield result
                     else:
-                        yield Stream.TEXT, group.replace('$$', '$'), \
-                              (filename, lineno, offset)
+                        yield TEXT, group.replace('$$', '$'), (filename, lineno,
+                                                               offset)
         return _interpolate(text)
     _interpolate = classmethod(_interpolate)
 
@@ -791,9 +791,8 @@
         if not hasattr(ctxt, '_match_templates'):
             ctxt._match_templates = []
 
-        stream = self._flatten(self._match(self._eval(self.stream, ctxt), ctxt),
-                               ctxt)
-        for filter_ in self.filters:
+        stream = self.stream
+        for filter_ in [self._eval, self._match, self._flatten] + self.filters:
             stream = filter_(iter(stream), ctxt)
         return Stream(stream)
 
@@ -803,7 +802,7 @@
         """
         for kind, data, pos in stream:
 
-            if kind is Stream.START:
+            if kind is START:
                 # Attributes may still contain expressions in start tags at
                 # this point, so do some evaluation
                 tag, attrib = data
@@ -814,7 +813,7 @@
                     else:
                         values = []
                         for subkind, subdata, subpos in substream:
-                            if subkind is Template.EXPR:
+                            if subkind is EXPR:
                                 values.append(subdata.evaluate(ctxt))
                             else:
                                 values.append(subdata)
@@ -824,7 +823,7 @@
                     new_attrib.append((name, u''.join(value)))
                 yield kind, (tag, Attributes(new_attrib)), pos
 
-            elif kind is Template.EXPR:
+            elif kind is EXPR:
                 result = data.evaluate(ctxt)
                 if result is None:
                     continue
@@ -833,7 +832,7 @@
                 # succeeds, and the string will be chopped up into individual
                 # characters
                 if isinstance(result, basestring):
-                    yield Stream.TEXT, result, pos
+                    yield TEXT, result, pos
                 else:
                     # Test if the expression evaluated to an iterable, in which
                     # case we yield the individual items
@@ -844,7 +843,7 @@
                     except TypeError:
                         # Neither a string nor an iterable, so just pass it
                         # through
-                        yield Stream.TEXT, unicode(result), pos
+                        yield TEXT, unicode(result), pos
 
             else:
                 yield kind, data, pos
@@ -853,19 +852,22 @@
         """Internal stream filter that expands `SUB` events in the stream."""
         try:
             for kind, data, pos in stream:
-                if kind is Template.SUB:
+                if kind is SUB:
                     # This event is a list of directives and a list of nested
                     # events to which those directives should be applied
                     directives, substream = data
-                    substream = directives[0](iter(substream), ctxt, directives[1:])
-                    substream = self._match(self._eval(substream, ctxt), ctxt)
-                    for event in self._flatten(substream, ctxt):
+                    if directives:
+                        substream = directives[0](iter(substream), ctxt,
+                                                  directives[1:])
+                    for filter_ in (self._eval, self._match, self._flatten):
+                        substream = filter_(substream, ctxt)
+                    for event in substream:
                         yield event
                         continue
                 else:
                     yield kind, data, pos
         except SyntaxError, err:
-            raise TemplateSyntaxError(err, self.filename, pos[1],
+            raise TemplateSyntaxError(err, pos[0], pos[1],
                                       pos[2] + (err.offset or 0))
 
     def _match(self, stream, ctxt=None, match_templates=None):
@@ -879,7 +881,7 @@
 
             # We (currently) only care about start and end events for matching
             # We might care about namespace events in the future, though
-            if kind not in (Stream.START, Stream.END):
+            if kind not in (START, END):
                 yield kind, data, pos
                 continue
 
@@ -893,15 +895,10 @@
                     content = [(kind, data, pos)]
                     depth = 1
                     while depth > 0:
-                        event = stream.next()
-                        if event[0] is Stream.START:
-                            depth += 1
-                        elif event[0] is Stream.END:
-                            depth -= 1
-                        content.append(event)
-
-                        # enable the path to keep track of the stream state
-                        test(*event)
+                        ev = stream.next()
+                        depth += {START: 1, END: -1}.get(ev[0], 0)
+                        content.append(ev)
+                        test(*ev)
 
                     content = list(self._flatten(content, ctxt))
                     ctxt.push(select=lambda path: Stream(content).select(path))
@@ -909,18 +906,22 @@
                     if directives:
                         template = directives[0](iter(template), ctxt,
                                                  directives[1:])
-                    template = self._match(self._eval(iter(template), ctxt),
-                                           ctxt, match_templates[:idx] +
-                                           match_templates[idx + 1:])
-                    for event in template:
+                    for event in self._match(self._eval(template, ctxt),
+                                             ctxt, match_templates[:idx] +
+                                             match_templates[idx + 1:]):
                         yield event
                     ctxt.pop()
 
                     break
-            else:
+
+            else: # no matches
                 yield kind, data, pos
 
 
+EXPR = Template.EXPR
+SUB = Template.SUB
+
+
 class TemplateLoader(object):
     """Responsible for loading templates from files on the specified search
     path.
@@ -990,6 +991,8 @@
             template is being loaded, or `None` if the template is being loaded
             directly
         """
+        from markup.filters import IncludeFilter
+
         if relative_to:
             filename = posixpath.join(posixpath.dirname(relative_to), filename)
         filename = os.path.normpath(filename)
@@ -1013,7 +1016,6 @@
             try:
                 fileobj = file(filepath, 'rt')
                 try:
-                    from markup.filters import IncludeFilter
                     tmpl = Template(fileobj, basedir=dirname, filename=filename)
                     tmpl.filters.append(IncludeFilter(self))
                 finally:
Copyright (C) 2012-2017 Edgewall Software