changeset 703:af57b12e3dd2 experimental-match-fastpaths

merge in trunk up through r818 - fundamentally changed the way MatchSet works, but actually is more consistent now
author aflett
date Mon, 31 Mar 2008 22:47:50 +0000
parents 52a597419c0d
children 422d0607ba85
files ChangeLog doc/templates.txt doc/text-templates.txt doc/upgrade.txt doc/xml-templates.txt genshi/core.py genshi/output.py genshi/template/base.py genshi/template/directives.py genshi/template/eval.py genshi/template/loader.py genshi/template/markup.py genshi/template/match.py genshi/template/tests/__init__.py genshi/template/tests/directives.py genshi/template/tests/eval.py genshi/template/tests/loader.py genshi/template/tests/markup.py genshi/template/tests/match.py genshi/template/tests/text.py genshi/template/text.py genshi/tests/core.py genshi/tests/output.py
diffstat 23 files changed, 865 insertions(+), 165 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -46,6 +46,31 @@
    of the serializer (ticket #146).
  * Assigning to a variable named `data` in a Python code block no longer
    breaks context lookup.
+ * The `Stream.render` now accepts an optional `out` parameter that can be
+   used to pass in a writable file-like object to use for assembling the
+   output, instead of building a big string and returning it.
+ * The XHTML serializer now strips `xml:space` attributes as they are only
+   allowed on very few tags.
+ * Match templates are now applied in a more controlled fashion: in the order
+   they are declared in the template source, all match templates up to (and
+   including) the matching template itself are applied to the matched content,
+   whereas the match templates declared after the matching template are only
+   applied to the generated content (ticket #186).
+ * The `TemplateLoader` class now provides an `instantiate()` method that can
+   be overridden by subclasses to implement advanced template instantiation
+   logic (ticket #204).
+ * The search path of the `TemplateLoader` class can now contain ''load
+   functions'' in addition to path strings. A load function is passed the
+   name of the requested template file, and should return a file-like object
+   and some metadata. New load functions are supplied for loading from egg
+   package data, and loading from different loaders depending on the path
+   prefix of the requested filename (ticket #182).
+ * Match templates can now be processed without keeping the complete matched
+   content in memory, which could cause excessive memory use on long pages.
+   The buffering can be disabled using the new `buffer` optimization hint on
+   the `<py:match>` directive.
+ * Improve error reporting when accessing an attribute in a Python expression
+   raises an `AttributeError` (ticket #191).
 
 
 Version 0.4.4
--- a/doc/templates.txt
+++ b/doc/templates.txt
@@ -270,6 +270,12 @@
 into a sandboxable template engine; there are sufficient ways to do harm even
 using plain expressions.
 
+.. warning:: Unfortunately, code blocks are severely limited when running
+             under Python 2.3: For example, it is not possible to access
+             variables defined in outer scopes. If you plan to use code blocks
+             extensively, it is strongly recommended that you run Python 2.4
+             or later.
+
 
 .. _`error handling`:
 
--- a/doc/text-templates.txt
+++ b/doc/text-templates.txt
@@ -215,7 +215,7 @@
 
 .. code-block:: genshitext
 
-  {% include '%s.txt' % filename %}
+  {% include ${'%s.txt' % filename} %}
 
 Note that a ``TemplateNotFound`` exception is raised if an included file can't
 be found.
@@ -349,10 +349,12 @@
 comments are lines that start with two ``#`` characters, and a line-break at the
 end of a directive is removed automatically.
 
-.. note:: If you're using this old syntax, it is strongly recommended to migrate
-          to the new syntax. Simply replace any references to ``TextTemplate``
-          by ``NewTextTemplate``. On the other hand, if you want to stick with
-          the old syntax for a while longer, replace references to
+.. note:: If you're using this old syntax, it is strongly recommended to
+          migrate to the new syntax. Simply replace any references to
+          ``TextTemplate`` by ``NewTextTemplate`` (and also change the
+          text templates, of course). On the other hand, if you want to stick
+          with the old syntax for a while longer, replace references to
           ``TextTemplate`` by ``OldTextTemplate``; while ``TextTemplate`` is
           still an alias for the old language at this point, that will change
-          in a future release.
+          in a future release. But also note that the old syntax may be
+          dropped entirely in a future release.
--- a/doc/upgrade.txt
+++ b/doc/upgrade.txt
@@ -30,6 +30,18 @@
 warned that lenient error handling may be removed completely in a
 future release.
 
+There has also been a subtle change to how ``py:match`` templates are
+processed: in previous versions, all match templates would be applied
+to the content generated by the matching template, and only the
+matching template itself was applied recursively to the original
+content. This behavior resulted in problems with many kinds of
+recursive matching, and hence was changed for 0.5: now, all match
+templates declared before the matching template are applied to the
+original content, and match templates declared after the matching
+template are applied to the generated content. This change should not
+have any effect on most applications, but you may want to check your
+use of match templates to make sure.
+
 
 Upgrading from Genshi 0.3.x to 0.4.x
 ------------------------------------
--- a/doc/xml-templates.txt
+++ b/doc/xml-templates.txt
@@ -322,6 +322,12 @@
 
 .. _`Using XPath`: streams.html#using-xpath
 
+Match templates are applied both to the original markup as well to the
+generated markup. The order in which they are applied depends on the order
+they are declared in the template source: a match template defined after
+another match template is applied to the output generated by the first match
+template. The match templates basically form a pipeline.
+
 This directive can also be used as an element:
 
 .. code-block:: genshi
@@ -352,6 +358,17 @@
 +---------------+-----------+-----------------------------------------------+
 | Attribute     | Default   | Description                                   |
 +===============+===========+===============================================+
+| ``buffer``    | ``true``  | Whether the matched content should be         |
+|               |           | buffered in memory. Buffering can improve     |
+|               |           | performance a bit at the cost of needing more |
+|               |           | memory during rendering. Buffering is         |
+|               |           | ''required'' for match templates that contain |
+|               |           | more than one invocation of the ``select()``  |
+|               |           | function. If there is only one call, and the  |
+|               |           | matched content can potentially be very long, |
+|               |           | consider disabling buffering to avoid         |
+|               |           | excessive memory use.                         |
++---------------+-----------+-----------------------------------------------+
 | ``once``      | ``false`` | Whether the engine should stop looking for    |
 |               |           | more matching elements after the first match. |
 |               |           | Use this on match templates that match        |
--- a/genshi/core.py
+++ b/genshi/core.py
@@ -149,7 +149,7 @@
         """
         return reduce(operator.or_, (self,) + filters)
 
-    def render(self, method=None, encoding='utf-8', **kwargs):
+    def render(self, method=None, encoding='utf-8', out=None, **kwargs):
         """Return a string representation of the stream.
         
         Any additional keyword arguments are passed to the serializer, and thus
@@ -161,15 +161,22 @@
                        the stream is used
         :param encoding: how the output string should be encoded; if set to
                          `None`, this method returns a `unicode` object
-        :return: a `str` or `unicode` object
+        :param out: a file-like object that the output should be written to
+                    instead of being returned as one big string; note that if
+                    this is a file or socket (or similar), the `encoding` must
+                    not be `None` (that is, the output must be encoded)
+        :return: a `str` or `unicode` object (depending on the `encoding`
+                 parameter), or `None` if the `out` parameter is provided
         :rtype: `basestring`
+        
         :see: XMLSerializer, XHTMLSerializer, HTMLSerializer, TextSerializer
+        :note: Changed in 0.5: added the `out` parameter
         """
         from genshi.output import encode
         if method is None:
             method = self.serializer or 'xml'
         generator = self.serialize(method=method, **kwargs)
-        return encode(generator, method=method, encoding=encoding)
+        return encode(generator, method=method, encoding=encoding, out=out)
 
     def select(self, path, namespaces=None, variables=None):
         """Return a new stream that contains the events matching the given
--- a/genshi/output.py
+++ b/genshi/output.py
@@ -30,7 +30,7 @@
            'XHTMLSerializer', 'HTMLSerializer', 'TextSerializer']
 __docformat__ = 'restructuredtext en'
 
-def encode(iterator, method='xml', encoding='utf-8'):
+def encode(iterator, method='xml', encoding='utf-8', out=None):
     """Encode serializer output into a string.
     
     :param iterator: the iterator returned from serializing a stream (basically
@@ -39,16 +39,27 @@
                    representable in the specified encoding are treated
     :param encoding: how the output string should be encoded; if set to `None`,
                      this method returns a `unicode` object
-    :return: a string or unicode object (depending on the `encoding` parameter)
+    :param out: a file-like object that the output should be written to
+                instead of being returned as one big string; note that if
+                this is a file or socket (or similar), the `encoding` must
+                not be `None` (that is, the output must be encoded)
+    :return: a `str` or `unicode` object (depending on the `encoding`
+             parameter), or `None` if the `out` parameter is provided
+    
     :since: version 0.4.1
+    :note: Changed in 0.5: added the `out` parameter
     """
-    output = u''.join(list(iterator))
     if encoding is not None:
         errors = 'replace'
         if method != 'text' and not isinstance(method, TextSerializer):
             errors = 'xmlcharrefreplace'
-        return output.encode(encoding, errors)
-    return output
+        _encode = lambda string: string.encode(encoding, errors)
+    else:
+        _encode = lambda string: string
+    if out is None:
+        return _encode(u''.join(list(iterator)))
+    for chunk in iterator:
+        out.write(_encode(chunk))
 
 def get_serializer(method='xml', **kwargs):
     """Return a serializer object for the given method.
@@ -298,6 +309,8 @@
                         value = attr
                     elif attr == u'xml:lang' and u'lang' not in attrib:
                         buf += [' lang="', escape(value), '"']
+                    elif attr == u'xml:space':
+                        continue
                     buf += [' ', attr, '="', escape(value), '"']
                 if kind is EMPTY:
                     if tag in empty_elems:
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -255,18 +255,53 @@
         """Pop the top-most scope from the stack."""
 
 
-def _apply_directives(stream, ctxt, directives):
+def _apply_directives(stream, directives, ctxt, **vars):
     """Apply the given directives to the stream.
     
     :param stream: the stream the directives should be applied to
+    :param directives: the list of directives to apply
     :param ctxt: the `Context`
-    :param directives: the list of directives to apply
+    :param vars: additional variables that should be available when Python
+                 code is executed
     :return: the stream with the given directives applied
     """
     if directives:
-        stream = directives[0](iter(stream), ctxt, directives[1:])
+        stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
     return stream
 
+def _eval_expr(expr, ctxt, **vars):
+    """Evaluate the given `Expression` object.
+    
+    :param expr: the expression to evaluate
+    :param ctxt: the `Context`
+    :param vars: additional variables that should be available to the
+                 expression
+    :return: the result of the evaluation
+    """
+    if vars:
+        ctxt.push(vars)
+    retval = expr.evaluate(ctxt)
+    if vars:
+        ctxt.pop()
+    return retval
+
+def _exec_suite(suite, ctxt, **vars):
+    """Execute the given `Suite` object.
+    
+    :param suite: the code suite to execute
+    :param ctxt: the `Context`
+    :param vars: additional variables that should be available to the
+                 code
+    """
+    if vars:
+        ctxt.push(vars)
+        ctxt.push({})
+    suite.execute(_ctxt2dict(ctxt))
+    if vars:
+        top = ctxt.pop()
+        ctxt.pop()
+        ctxt.frames[0].update(top)
+
 
 class TemplateMeta(type):
     """Meta class for templates."""
@@ -427,21 +462,24 @@
         :return: a markup event stream representing the result of applying
                  the template to the context data.
         """
+        vars = {}
         if args:
             assert len(args) == 1
             ctxt = args[0]
             if ctxt is None:
                 ctxt = Context(**kwargs)
+            else:
+                vars = kwargs
             assert isinstance(ctxt, Context)
         else:
             ctxt = Context(**kwargs)
 
         stream = self.stream
         for filter_ in self.filters:
-            stream = filter_(iter(stream), ctxt)
+            stream = filter_(iter(stream), ctxt, **vars)
         return Stream(stream, self.serializer)
 
-    def _eval(self, stream, ctxt):
+    def _eval(self, stream, ctxt, **vars):
         """Internal stream filter that evaluates any expressions in `START` and
         `TEXT` events.
         """
@@ -461,7 +499,8 @@
                     else:
                         values = []
                         for subkind, subdata, subpos in self._eval(substream,
-                                                                   ctxt):
+                                                                   ctxt,
+                                                                   **vars):
                             if subkind is TEXT:
                                 values.append(subdata)
                         value = [x for x in values if x is not None]
@@ -471,7 +510,7 @@
                 yield kind, (tag, Attrs(new_attrs)), pos
 
             elif kind is EXPR:
-                result = data.evaluate(ctxt)
+                result = _eval_expr(data, ctxt, **vars)
                 if result is not None:
                     # First check for a string, otherwise the iterable test
                     # below succeeds, and the string will be chopped up into
@@ -483,7 +522,7 @@
                     elif hasattr(result, '__iter__'):
                         substream = _ensure(result)
                         for filter_ in filters:
-                            substream = filter_(substream, ctxt)
+                            substream = filter_(substream, ctxt, **vars)
                         for event in substream:
                             yield event
                     else:
@@ -492,28 +531,29 @@
             else:
                 yield kind, data, pos
 
-    def _exec(self, stream, ctxt):
+    def _exec(self, stream, ctxt, **vars):
         """Internal stream filter that executes Python code blocks."""
         for event in stream:
             if event[0] is EXEC:
-                event[1].execute(_ctxt2dict(ctxt))
+                _exec_suite(event[1], ctxt, **vars)
             else:
                 yield event
 
-    def _flatten(self, stream, ctxt):
+    def _flatten(self, stream, ctxt, **vars):
         """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):
+                substream = _apply_directives(substream, directives, ctxt,
+                                              **vars)
+                for event in self._flatten(substream, ctxt, **vars):
                     yield event
             else:
                 yield event
 
-    def _include(self, stream, ctxt):
+    def _include(self, stream, ctxt, **vars):
         """Internal stream filter that performs inclusion of external
         template files.
         """
@@ -524,20 +564,21 @@
                 href, cls, fallback = event[1]
                 if not isinstance(href, basestring):
                     parts = []
-                    for subkind, subdata, subpos in self._eval(href, ctxt):
+                    for subkind, subdata, subpos in self._eval(href, ctxt,
+                                                               **vars):
                         if subkind is TEXT:
                             parts.append(subdata)
                     href = u''.join([x for x in parts if x is not None])
                 try:
                     tmpl = self.loader.load(href, relative_to=event[2][0],
                                             cls=cls or self.__class__)
-                    for event in tmpl.generate(ctxt):
+                    for event in tmpl.generate(ctxt, **vars):
                         yield event
                 except TemplateNotFound:
                     if fallback is None:
                         raise
                     for filter_ in self.filters:
-                        fallback = filter_(iter(fallback), ctxt)
+                        fallback = filter_(iter(fallback), ctxt, **vars)
                     for event in fallback:
                         yield event
             else:
--- a/genshi/template/directives.py
+++ b/genshi/template/directives.py
@@ -22,7 +22,8 @@
 from genshi.core import QName, Stream
 from genshi.path import Path
 from genshi.template.base import TemplateRuntimeError, TemplateSyntaxError, \
-                                 EXPR, _apply_directives, _ctxt2dict
+                                 EXPR, _apply_directives, _eval_expr, \
+                                 _exec_suite
 from genshi.template.eval import Expression, Suite, ExpressionASTTransformer, \
                                  _parse
 
@@ -88,13 +89,15 @@
         return cls(value, template, namespaces, *pos[1:]), stream
     attach = classmethod(attach)
 
-    def __call__(self, stream, ctxt, directives):
+    def __call__(self, stream, directives, ctxt, **vars):
         """Apply the directive to the given stream.
         
         :param stream: the event stream
-        :param ctxt: the context data
         :param directives: a list of the remaining directives that should
                            process the stream
+        :param ctxt: the context data
+        :param vars: additional variables that should be made available when
+                     Python code is executed
         """
         raise NotImplementedError
 
@@ -167,10 +170,10 @@
     """
     __slots__ = []
 
-    def __call__(self, stream, ctxt, directives):
+    def __call__(self, stream, directives, ctxt, **vars):
         def _generate():
             kind, (tag, attrib), pos  = stream.next()
-            attrs = self.expr.evaluate(ctxt)
+            attrs = _eval_expr(self.expr, ctxt, **vars)
             if attrs:
                 if isinstance(attrs, Stream):
                     try:
@@ -186,7 +189,7 @@
             for event in stream:
                 yield event
 
-        return _apply_directives(_generate(), ctxt, directives)
+        return _apply_directives(_generate(), directives, ctxt, **vars)
 
 
 class ContentDirective(Directive):
@@ -291,7 +294,7 @@
                                                namespaces, pos)
     attach = classmethod(attach)
 
-    def __call__(self, stream, ctxt, directives):
+    def __call__(self, stream, directives, ctxt, **vars):
         stream = list(stream)
 
         def function(*args, **kwargs):
@@ -304,14 +307,14 @@
                     if name in kwargs:
                         val = kwargs.pop(name)
                     else:
-                        val = self.defaults.get(name).evaluate(ctxt)
+                        val = _eval_expr(self.defaults.get(name), ctxt, **vars)
                     scope[name] = val
             if not self.star_args is None:
                 scope[self.star_args] = args
             if not self.dstar_args is None:
                 scope[self.dstar_args] = kwargs
             ctxt.push(scope)
-            for event in _apply_directives(stream, ctxt, directives):
+            for event in _apply_directives(stream, directives, ctxt, **vars):
                 yield event
             ctxt.pop()
         try:
@@ -364,8 +367,8 @@
                                                namespaces, pos)
     attach = classmethod(attach)
 
-    def __call__(self, stream, ctxt, directives):
-        iterable = self.expr.evaluate(ctxt)
+    def __call__(self, stream, directives, ctxt, **vars):
+        iterable = _eval_expr(self.expr, ctxt, **vars)
         if iterable is None:
             return
 
@@ -375,7 +378,7 @@
         for item in iterable:
             assign(scope, item)
             ctxt.push(scope)
-            for event in _apply_directives(stream, ctxt, directives):
+            for event in _apply_directives(stream, directives, ctxt, **vars):
                 yield event
             ctxt.pop()
 
@@ -405,9 +408,10 @@
                                               namespaces, pos)
     attach = classmethod(attach)
 
-    def __call__(self, stream, ctxt, directives):
-        if self.expr.evaluate(ctxt):
-            return _apply_directives(stream, ctxt, directives)
+    def __call__(self, stream, directives, ctxt, **vars):
+        value = _eval_expr(self.expr, ctxt, **vars)
+        if value:
+            return _apply_directives(stream, directives, ctxt, **vars)
         return []
 
 
@@ -440,6 +444,8 @@
     def attach(cls, template, stream, value, namespaces, pos):
         hints = []
         if type(value) is dict:
+            if value.get('buffer', '').lower() == 'false':
+                hints.append('not_buffered')
             if value.get('once', '').lower() == 'true':
                 hints.append('match_once')
             if value.get('recursive', '').lower() == 'false':
@@ -449,7 +455,7 @@
                stream
     attach = classmethod(attach)
 
-    def __call__(self, stream, ctxt, directives):
+    def __call__(self, stream, directives, ctxt, **vars):
         ctxt._match_set.add((self.path.test(ignore_context=True),
                              self.path, list(stream), self.hints,
                              self.namespaces, directives))
@@ -531,9 +537,9 @@
     """
     __slots__ = []
 
-    def __call__(self, stream, ctxt, directives):
+    def __call__(self, stream, directives, ctxt, **vars):
         def _generate():
-            if self.expr.evaluate(ctxt):
+            if _eval_expr(self.expr, ctxt, **vars):
                 stream.next() # skip start tag
                 previous = stream.next()
                 for event in stream:
@@ -542,7 +548,7 @@
             else:
                 for event in stream:
                     yield event
-        return _apply_directives(_generate(), ctxt, directives)
+        return _apply_directives(_generate(), directives, ctxt, **vars)
 
     def attach(cls, template, stream, value, namespaces, pos):
         if not value:
@@ -600,12 +606,12 @@
                                                   namespaces, pos)
     attach = classmethod(attach)
 
-    def __call__(self, stream, ctxt, directives):
+    def __call__(self, stream, directives, ctxt, **vars):
         info = [False, bool(self.expr), None]
         if self.expr:
-            info[2] = self.expr.evaluate(ctxt)
+            info[2] = _eval_expr(self.expr, ctxt, **vars)
         ctxt._choice_stack.append(info)
-        for event in _apply_directives(stream, ctxt, directives):
+        for event in _apply_directives(stream, directives, ctxt, **vars):
             yield event
         ctxt._choice_stack.pop()
 
@@ -629,7 +635,7 @@
                                                 namespaces, pos)
     attach = classmethod(attach)
 
-    def __call__(self, stream, ctxt, directives):
+    def __call__(self, stream, directives, ctxt, **vars):
         info = ctxt._choice_stack and ctxt._choice_stack[-1]
         if not info:
             raise TemplateRuntimeError('"when" directives can only be used '
@@ -644,16 +650,16 @@
         if info[1]:
             value = info[2]
             if self.expr:
-                matched = value == self.expr.evaluate(ctxt)
+                matched = value == _eval_expr(self.expr, ctxt, **vars)
             else:
                 matched = bool(value)
         else:
-            matched = bool(self.expr.evaluate(ctxt))
+            matched = bool(_eval_expr(self.expr, ctxt, **vars))
         info[0] = matched
         if not matched:
             return []
 
-        return _apply_directives(stream, ctxt, directives)
+        return _apply_directives(stream, directives, ctxt, **vars)
 
 
 class OtherwiseDirective(Directive):
@@ -668,7 +674,7 @@
         Directive.__init__(self, None, template, namespaces, lineno, offset)
         self.filename = template.filepath
 
-    def __call__(self, stream, ctxt, directives):
+    def __call__(self, stream, directives, ctxt, **vars):
         info = ctxt._choice_stack and ctxt._choice_stack[-1]
         if not info:
             raise TemplateRuntimeError('an "otherwise" directive can only be '
@@ -678,7 +684,7 @@
             return []
         info[0] = True
 
-        return _apply_directives(stream, ctxt, directives)
+        return _apply_directives(stream, directives, ctxt, **vars)
 
 
 class WithDirective(Directive):
@@ -722,11 +728,10 @@
                                                 namespaces, pos)
     attach = classmethod(attach)
 
-    def __call__(self, stream, ctxt, directives):
-        frame = {}
-        ctxt.push(frame)
-        self.suite.execute(_ctxt2dict(ctxt))
-        for event in _apply_directives(stream, ctxt, directives):
+    def __call__(self, stream, directives, ctxt, **vars):
+        ctxt.push({})
+        _exec_suite(self.suite, ctxt, **vars)
+        for event in _apply_directives(stream, directives, ctxt, **vars):
             yield event
         ctxt.pop()
 
--- a/genshi/template/eval.py
+++ b/genshi/template/eval.py
@@ -139,8 +139,7 @@
         :return: the result of the evaluation
         """
         __traceback_hide__ = 'before_and_this'
-        _globals = self._globals()
-        _globals['__data__'] = data
+        _globals = self._globals(data)
         return eval(self.code, _globals, {'__data__': data})
 
 
@@ -161,8 +160,7 @@
         :param data: a mapping containing the data to execute in
         """
         __traceback_hide__ = 'before_and_this'
-        _globals = self._globals()
-        _globals['__data__'] = data
+        _globals = self._globals(data)
         exec self.code in _globals, data
 
 
@@ -248,15 +246,16 @@
 class LookupBase(object):
     """Abstract base class for variable lookup implementations."""
 
-    def globals(cls):
+    def globals(cls, data):
         """Construct the globals dictionary to use as the execution context for
         the expression or suite.
         """
         return {
+            '__data__': data,
             '_lookup_name': cls.lookup_name,
             '_lookup_attr': cls.lookup_attr,
             '_lookup_item': cls.lookup_item,
-            'UndefinedError': UndefinedError
+            'UndefinedError': UndefinedError,
         }
     globals = classmethod(globals)
 
@@ -270,18 +269,22 @@
         return val
     lookup_name = classmethod(lookup_name)
 
-    def lookup_attr(cls, data, obj, key):
+    def lookup_attr(cls, obj, key):
         __traceback_hide__ = True
-        val = getattr(obj, key, UNDEFINED)
-        if val is UNDEFINED:
-            try:
-                val = obj[key]
-            except (KeyError, TypeError):
-                val = cls.undefined(key, owner=obj)
+        try:
+            val = getattr(obj, key)
+        except AttributeError:
+            if hasattr(obj.__class__, key):
+                raise
+            else:
+                try:
+                    val = obj[key]
+                except (KeyError, TypeError):
+                    val = cls.undefined(key, owner=obj)
         return val
     lookup_attr = classmethod(lookup_attr)
 
-    def lookup_item(cls, data, obj, key):
+    def lookup_item(cls, obj, key):
         __traceback_hide__ = True
         if len(key) == 1:
             key = key[0]
@@ -754,12 +757,12 @@
 
     def visitGetattr(self, node):
         return ast.CallFunc(ast.Name('_lookup_attr'), [
-            ast.Name('__data__'), self.visit(node.expr),
+            self.visit(node.expr),
             ast.Const(node.attrname)
         ])
 
     def visitSubscript(self, node):
         return ast.CallFunc(ast.Name('_lookup_item'), [
-            ast.Name('__data__'), self.visit(node.expr),
+            self.visit(node.expr),
             ast.Tuple([self.visit(sub) for sub in node.subs])
         ])
--- a/genshi/template/loader.py
+++ b/genshi/template/loader.py
@@ -82,7 +82,10 @@
         
         :param search_path: a list of absolute path names that should be
                             searched for template files, or a string containing
-                            a single absolute path
+                            a single absolute path; alternatively, any item on
+                            the list may be a ''load function'' that is passed
+                            a filename and returns a file-like object and some
+                            metadata
         :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
@@ -109,7 +112,7 @@
         self.search_path = search_path
         if self.search_path is None:
             self.search_path = []
-        elif isinstance(self.search_path, basestring):
+        elif not isinstance(self.search_path, (list, tuple)):
             self.search_path = [self.search_path]
 
         self.auto_reload = auto_reload
@@ -130,9 +133,9 @@
     def load(self, filename, relative_to=None, cls=None, 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 ignored.
+        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 ignored.
         
         If the requested template is not found, a `TemplateNotFound` exception
         is raised. Otherwise, a `Template` object is returned that represents
@@ -155,24 +158,25 @@
         :param encoding: the encoding of the template to load; defaults to the
                          ``default_encoding`` of the loader instance
         :return: the loaded `Template` instance
-        :raises TemplateNotFound: if a template with the given name could not be
-                                  found
+        :raises TemplateNotFound: if a template with the given name could not
+                                  be found
         """
         if cls is None:
             cls = self.default_class
-        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)
+        cachekey = 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]:
+                tmpl = self._cache[cachekey]
+                if not self.auto_reload:
+                    return tmpl
+                mtime = self._mtime[cachekey]
+                if mtime and mtime == os.path.getmtime(tmpl.filepath):
                     return tmpl
             except KeyError, OSError:
                 pass
@@ -190,41 +194,135 @@
                 # template is on the search path
                 dirname = os.path.dirname(relative_to)
                 if dirname not in search_path:
-                    search_path = search_path + [dirname]
+                    search_path = list(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)
+            for loadfunc in search_path:
+                if isinstance(loadfunc, basestring):
+                    loadfunc = directory(loadfunc)
                 try:
-                    fileobj = open(filepath, 'U')
+                    dirname, filename, fileobj, mtime = loadfunc(filename)
+                except IOError:
+                    continue
+                else:
                     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
+                            # so that nested includes work properly without a
                             # search path
                             filename = os.path.join(dirname, filename)
                             dirname = ''
-                        tmpl = cls(fileobj, basedir=dirname, filename=filename,
-                                   loader=self, encoding=encoding,
-                                   lookup=self.variable_lookup,
-                                   allow_exec=self.allow_exec)
+                        tmpl = self.instantiate(cls, fileobj, dirname,
+                                                filename, encoding=encoding)
                         if self.callback:
                             self.callback(tmpl)
-                        self._cache[filename] = tmpl
-                        self._mtime[filename] = os.path.getmtime(filepath)
+                        self._cache[cachekey] = tmpl
+                        self._mtime[cachekey] = mtime
                     finally:
-                        fileobj.close()
+                        if hasattr(fileobj, 'close'):
+                            fileobj.close()
                     return tmpl
-                except IOError:
-                    continue
 
             raise TemplateNotFound(filename, search_path)
 
         finally:
             self._lock.release()
+
+    def instantiate(self, cls, fileobj, dirname, filename, encoding=None):
+        """Instantiate and return the `Template` object based on the given
+        class and parameters.
+        
+        This function is intended for subclasses to override if they need to
+        implement special template instantiation logic. Code that just uses
+        the `TemplateLoader` should use the `load` method instead.
+        
+        :param cls: the class of the template object to instantiate
+        :param fileobj: a readable file-like object containing the template
+                        source
+        :param dirname: the name of the base directory containing the template
+                        file
+        :param filename: the name of the template file, relative to the given
+                         base directory
+        :param encoding: the encoding of the template to load; defaults to the
+                         ``default_encoding`` of the loader instance
+        :return: the loaded `Template` instance
+        :rtype: `Template`
+        """
+        if encoding is None:
+            encoding = self.default_encoding
+        return cls(fileobj, basedir=dirname, filename=filename, loader=self,
+                   encoding=encoding, lookup=self.variable_lookup,
+                   allow_exec=self.allow_exec)
+
+    def directory(path):
+        """Loader factory for loading templates from a local directory.
+        
+        :param path: the path to the local directory containing the templates
+        :return: the loader function to load templates from the given directory
+        :rtype: ``function``
+        """
+        def _load_from_directory(filename):
+            filepath = os.path.join(path, filename)
+            fileobj = open(filepath, 'U')
+            return path, filename, fileobj, os.path.getmtime(filepath)
+        return _load_from_directory
+    directory = staticmethod(directory)
+
+    def package(name, path):
+        """Loader factory for loading templates from egg package data.
+        
+        :param name: the name of the package containing the resources
+        :param path: the path inside the package data
+        :return: the loader function to load templates from the given package
+        :rtype: ``function``
+        """
+        from pkg_resources import resource_stream
+        def _load_from_package(filename):
+            filepath = os.path.join(path, filename)
+            return path, filename, resource_stream(name, filepath), None
+        return _load_from_package
+    package = staticmethod(package)
+
+    def prefixed(**delegates):
+        """Factory for a load function that delegates to other loaders
+        depending on the prefix of the requested template path.
+        
+        The prefix is stripped from the filename when passing on the load
+        request to the delegate.
+        
+        >>> load = prefixed(
+        ...     app1 = lambda filename: ('app1', filename, None, None),
+        ...     app2 = lambda filename: ('app2', filename, None, None)
+        ... )
+        >>> print load('app1/foo.html')
+        ('', 'app1/foo.html', None, None)
+        >>> print load('app2/bar.html')
+        ('', 'app2/bar.html', None, None)
+
+        :param delegates: mapping of path prefixes to loader functions
+        :return: the loader function
+        :rtype: ``function``
+        """
+        def _dispatch_by_prefix(filename):
+            for prefix, delegate in delegates.items():
+                if filename.startswith(prefix):
+                    if isinstance(delegate, basestring):
+                        delegate = directory(delegate)
+                    path, _, fileobj, mtime = delegate(
+                        filename[len(prefix):].lstrip('/\\')
+                    )
+                    dirname = path[len(prefix):].rstrip('/\\')
+                    return dirname, filename, fileobj, mtime
+            raise TemplateNotFound(filename, delegates.keys())
+        return _dispatch_by_prefix
+    prefixed = staticmethod(prefixed)
+
+directory = TemplateLoader.directory
+package = TemplateLoader.package
+prefixed = TemplateLoader.prefixed
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -226,7 +226,7 @@
         assert len(streams) == 1
         return streams[0]
 
-    def _match(self, stream, ctxt, match_set=None):
+    def _match(self, stream, ctxt, match_set=None, **vars):
         """Internal stream filter that applies any defined match templates
         to the stream.
         """
@@ -263,10 +263,26 @@
                 (test, path, template, hints, namespaces, directives) = \
                     match_template
                 if test(event, namespaces, ctxt) is True:
+                    post_match_templates = \
+                        match_set.after_template(match_template)
+                    
                     if 'match_once' in hints:
+
+                        # need to save this before we nuke
+                        # match_template from match_set
+                        pre_match_templates = \
+                            match_set.before_template(match_template, False)
+                        
+                        # forcibly remove this template from this and
+                        # all child match sets
                         match_set.remove(match_template)
                         del match_candidates[idx]
                         idx -= 1
+                    else:
+                        inclusive = True
+                        if 'not_recursive' in hints:
+                            inclusive=False
+                        pre_match_templates = match_set.before_template(match_template, inclusive)
 
                     # Let the remaining match templates know about the event so
                     # they get a chance to update their internal state
@@ -276,38 +292,37 @@
                     # Consume and store all events until an end event
                     # corresponding to this start event is encountered
                     inner = _strip(stream)
-                    if 'match_once' not in hints \
-                            and 'not_recursive' not in hints:
-                        inner = self._match(inner, ctxt,
-                                            MatchSet.single_match(match_template))
-                    content = list(self._include(chain([event], inner, tail),
-                                                 ctxt))
+                    if pre_match_templates:
+                        inner = self._match(inner, ctxt, pre_match_templates)
+                    content = self._include(chain([event], inner, tail), ctxt)
+                    if 'not_buffered' not in hints:
+                        content = list(content)
 
                     # Now tell all the match templates about the
                     # END event (tail[0])
-                    for test in [mt[0] for mt in match_candidates]:
-                        test(tail[0], namespaces, ctxt, updateonly=True)
+                    if tail:
+                        for test in [mt[0] for mt in match_candidates]:
+                            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))
+                    vars = dict(select=select)
 
                     # Recursively process the output
-                    template = _apply_directives(template, ctxt, directives)
-                    remaining = match_set
-                    if 'match_once' not in hints:
-                        # match has not been removed, so we need an
-                        # exclusion matchset
-                        remaining = match_set.with_exclusion(match_template)
-                        
-                    body = self._exec(self._eval(self._flatten(template, ctxt),
-                                                 ctxt), ctxt)
-                    for event in self._match(body, ctxt, remaining):
+                    template = _apply_directives(template, directives, ctxt,
+                                                 **vars)
+                    for event in self._match(
+                            self._exec(
+                                self._eval(
+                                    self._flatten(template, ctxt, **vars),
+                                    ctxt, **vars),
+                                ctxt, **vars),
+                            ctxt, post_match_templates,
+                            **vars):
                         yield event
 
-                    ctxt.pop()
                     break
 
             else: # no matches
--- a/genshi/template/match.py
+++ b/genshi/template/match.py
@@ -2,6 +2,7 @@
 from genshi.path import CHILD, LocalNameTest
 
 from copy import copy
+from itertools import ifilter
 
 def is_simple_path(path):
     """
@@ -35,24 +36,25 @@
 
     If the path is more complex like "xyz[k=z]" then then that match
     will always be returned by ``find_matches``.  """
-    def __init__(self, parent=None, exclude=None):
+    def __init__(self, parent=None,
+                 min_index=None,
+                 max_index=None):
         """
         If a parent is given, it means this is a wrapper around another
         set.
         
-        If exclude is given, it means include everything in the
-        parent, but exclude a specific match template.
         """
         self.parent = parent
 
-        self.current_index = 0
-
         if parent is None:
             # merely for indexing. Note that this is shared between
             # all MatchSets that share the same root parent. We don't
             # have to worry about exclusions here
             self.match_order = {}
             
+            self.min_index = None
+            self.max_index = None
+
             # tag_templates are match templates whose path are simply
             # a tag, like "body" or "img"
             self.tag_templates = {}
@@ -61,23 +63,23 @@
             # as ones with complex paths like "[class=container]"
             self.other_templates = []
 
-            # exclude is a list of templates to ignore when iterating
-            # through templates
-            self.exclude = []
-            if exclude is not None:
-                self.exclude.append(exclude)
         else:
             # We have a parent: Just copy references to member
-            # variables in parent so that there's no performance loss,
-            # but make our own exclusion set, so we don't have to
-            # chain exclusions across a chain of MatchSets
+            # variables in parent so that there's no performance loss
+            self.max_index = parent.max_index
+            self.min_index = parent.min_index
             self.match_order = parent.match_order
             self.tag_templates = parent.tag_templates
             self.other_templates = parent.other_templates
+
+        if max_index is not None:
+            assert self.max_index is None or max_index <= self.max_index
+            self.max_index = max_index
+
+        if min_index is not None:
+            assert self.min_index is None or min_index > self.min_index
+            self.min_index = min_index
         
-            self.exclude = copy(parent.exclude)
-            if exclude is not None:
-                self.exclude.append(exclude)
     
     def add(self, match_template):
         """
@@ -91,7 +93,6 @@
         
         path = match_template[1]
 
-        self.current_index += 1
         if is_simple_path(path):
             # special cache of tag
             tag_name = path.paths[0][0][1].name
@@ -133,15 +134,22 @@
         return match_set
     single_match = classmethod(single_match)
 
-    def with_exclusion(self, exclude):
+    def before_template(self, match_template, inclusive):
+        cls = type(self)
+        max_index = self.match_order[id(match_template)]
+        if not inclusive:
+            max_index -= 1
+        return cls(parent=self, max_index=max_index)
+    
+    def after_template(self, match_template):
         """
-        Factory for creating a MatchSet based on another MatchSet, but
-        with certain templates excluded
+        Factory for creating a MatchSet that only matches templates after
+        the given match
         """
-        cls = self.__class__
-        new_match_set = cls(parent=self, exclude=exclude)
-        return new_match_set
-            
+        cls = type(self)
+        min_index = self.match_order[id(match_template)] + 1
+        return cls(parent=self, min_index=min_index)
+    
     def find_raw_matches(self, event):
         """ Return a list of all valid templates that can be used for the
         given event. Ordering is funky because we first check
@@ -167,8 +175,20 @@
         """
 
         # remove exclusions
-        matches = filter(lambda template: template not in self.exclude,
-                         self.find_raw_matches(event))
+        def can_match(template):
+            # make sure that 
+            if (self.min_index is not None and
+                self.match_order[id(template)] < self.min_index):
+                return False
+
+            if (self.max_index is not None and 
+                self.match_order[id(template)] > self.max_index):
+                return False
+
+            return True
+        
+        matches = ifilter(can_match,
+                          self.find_raw_matches(event))
 
         # sort the results according to the order they were added
         return sorted(matches, key=lambda v: self.match_order[id(v)])
@@ -177,6 +197,21 @@
         """
         allow this to behave as a list
         """
+
+        # this is easy - before the first element there is nothing
+        if self.max_index == -1:
+            return False
+
+        # this isn't always right because match_order may shrink, but
+        # you'll never get a false-negative
+        if self.min_index == len(self.match_order):
+            return False
+
+        # check for a range that is completely constrained
+        if self.min_index is not None and self.max_index is not None:
+            if self.min_index >= self.max_index:
+                return False
+            
         return bool(self.tag_templates or self.other_templates)
 
     def __str__(self):
@@ -184,8 +219,7 @@
         if self.parent:
             parent = ": child of 0x%x" % id(self.parent)
 
-        exclude = ""
-        if self.exclude:
-            exclude = " / excluding %d items" % len(self.exclude)
-            
-        return "<MatchSet 0x%x %d tag templates, %d other templates%s%s>" % (id(self), len(self.tag_templates), len(self.other_templates), parent, exclude)
+        return "<MatchSet 0x%x %d tag templates, %d other templates, range=[%s:%s]%s>" % (
+            id(self), len(self.tag_templates), len(self.other_templates),
+            self.min_index, self.max_index,
+            parent)
--- a/genshi/template/tests/__init__.py
+++ b/genshi/template/tests/__init__.py
@@ -16,7 +16,7 @@
 
 def suite():
     from genshi.template.tests import base, directives, eval, interpolation, \
-                                      loader, markup, plugin, text
+                                      loader, markup, plugin, text, match
     suite = unittest.TestSuite()
     suite.addTest(base.suite())
     suite.addTest(directives.suite())
@@ -26,6 +26,7 @@
     suite.addTest(markup.suite())
     suite.addTest(plugin.suite())
     suite.addTest(text.suite())
+    suite.addTest(match.suite())
     return suite
 
 if __name__ == '__main__':
--- a/genshi/template/tests/directives.py
+++ b/genshi/template/tests/directives.py
@@ -631,6 +631,32 @@
           </body>
         </html>""", str(tmpl.generate()))
 
+    def test_recursive_match_3(self):
+        tmpl = MarkupTemplate("""<test xmlns:py="http://genshi.edgewall.org/">
+          <py:match path="b[@type='bullet']">
+            <bullet>${select('*|text()')}</bullet>
+          </py:match>
+          <py:match path="group[@type='bullet']">
+            <ul>${select('*')}</ul>
+          </py:match>
+          <py:match path="b">
+            <generic>${select('*|text()')}</generic>
+          </py:match>
+
+          <b>
+            <group type="bullet">
+              <b type="bullet">1</b>
+              <b type="bullet">2</b>
+            </group>
+          </b>
+        </test>
+        """)
+        self.assertEqual("""<test>
+            <generic>
+            <ul><bullet>1</bullet><bullet>2</bullet></ul>
+          </generic>
+        </test>""", str(tmpl.generate()))
+
     def test_not_match_self(self):
         """
         See http://genshi.edgewall.org/ticket/77
--- a/genshi/template/tests/eval.py
+++ b/genshi/template/tests/eval.py
@@ -342,11 +342,16 @@
 
     def test_getattr_exception(self):
         class Something(object):
-            def prop(self):
+            def prop_a(self):
                 raise NotImplementedError
-            prop = property(prop)
+            prop_a = property(prop_a)
+            def prop_b(self):
+                raise AttributeError
+            prop_b = property(prop_b)
         self.assertRaises(NotImplementedError,
-                          Expression('s.prop').evaluate, {'s': Something()})
+                          Expression('s.prop_a').evaluate, {'s': Something()})
+        self.assertRaises(AttributeError,
+                          Expression('s.prop_b').evaluate, {'s': Something()})
 
     def test_getitem_undefined_string(self):
         class Something(object):
--- a/genshi/template/tests/loader.py
+++ b/genshi/template/tests/loader.py
@@ -104,6 +104,34 @@
               <div>Included</div>
             </html>""", tmpl.generate().render())
 
+    def test_relative_include_samesubdir(self):
+        file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<div>Included tmpl1.html</div>""")
+        finally:
+            file1.close()
+
+        os.mkdir(os.path.join(self.dirname, 'sub'))
+        file2 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w')
+        try:
+            file2.write("""<div>Included sub/tmpl1.html</div>""")
+        finally:
+            file2.close()
+
+        file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w')
+        try:
+            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl1.html" />
+            </html>""")
+        finally:
+            file3.close()
+
+        loader = TemplateLoader([self.dirname])
+        tmpl = loader.load('sub/tmpl2.html')
+        self.assertEqual("""<html>
+              <div>Included sub/tmpl1.html</div>
+            </html>""", tmpl.generate().render())
+
     def test_relative_include_without_search_path(self):
         file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w')
         try:
@@ -172,6 +200,67 @@
           <div>Included</div>
         </html>""", tmpl2.generate().render())
 
+    def test_relative_absolute_template_preferred(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', 'tmpl1.html'), 'w')
+        try:
+            file2.write("""<div>Included from sub</div>""")
+        finally:
+            file2.close()
+
+        file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w')
+        try:
+            file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl1.html" />
+            </html>""")
+        finally:
+            file3.close()
+
+        loader = TemplateLoader()
+        tmpl = loader.load(os.path.abspath(os.path.join(self.dirname, 'sub',
+                                                        'tmpl2.html')))
+        self.assertEqual("""<html>
+              <div>Included from sub</div>
+            </html>""", tmpl.generate().render())
+
+    def test_abspath_caching(self):
+        abspath = os.path.join(self.dirname, 'abs')
+        os.mkdir(abspath)
+        file1 = open(os.path.join(abspath, 'tmpl1.html'), 'w')
+        try:
+            file1.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="tmpl2.html" />
+            </html>""")
+        finally:
+            file1.close()
+
+        file2 = open(os.path.join(abspath, 'tmpl2.html'), 'w')
+        try:
+            file2.write("""<div>Included from abspath.</div>""")
+        finally:
+            file2.close()
+
+        searchpath = os.path.join(self.dirname, 'searchpath')
+        os.mkdir(searchpath)
+        file3 = open(os.path.join(searchpath, 'tmpl2.html'), 'w')
+        try:
+            file3.write("""<div>Included from searchpath.</div>""")
+        finally:
+            file3.close()
+
+        loader = TemplateLoader(searchpath)
+        tmpl1 = loader.load(os.path.join(abspath, 'tmpl1.html'))
+        self.assertEqual("""<html>
+              <div>Included from searchpath.</div>
+            </html>""", tmpl1.generate().render())
+        assert 'tmpl2.html' in loader._cache
+
     def test_load_with_default_encoding(self):
         f = open(os.path.join(self.dirname, 'tmpl.html'), 'w')
         try:
@@ -219,6 +308,108 @@
               <p>Hello, hello</p>
             </html>""", tmpl.generate().render())
 
+    def test_prefix_delegation_to_directories(self):
+        """
+        Test prefix delegation with the following layout:
+        
+        templates/foo.html
+        sub1/templates/tmpl1.html
+        sub2/templates/tmpl2.html
+        
+        Where sub1 and sub2 are prefixes, and both tmpl1.html and tmpl2.html
+        incldue foo.html.
+        """
+        dir1 = os.path.join(self.dirname, 'templates')
+        os.mkdir(dir1)
+        file1 = open(os.path.join(dir1, 'foo.html'), 'w')
+        try:
+            file1.write("""<div>Included foo</div>""")
+        finally:
+            file1.close()
+
+        dir2 = os.path.join(self.dirname, 'sub1', 'templates')
+        os.makedirs(dir2)
+        file2 = open(os.path.join(dir2, 'tmpl1.html'), 'w')
+        try:
+            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="../foo.html" /> from sub1
+            </html>""")
+        finally:
+            file2.close()
+
+        dir3 = os.path.join(self.dirname, 'sub2', 'templates')
+        os.makedirs(dir3)
+        file3 = open(os.path.join(dir3, 'tmpl2.html'), 'w')
+        try:
+            file3.write("""<div>tmpl2</div>""")
+        finally:
+            file3.close()
+
+        loader = TemplateLoader([dir1, TemplateLoader.prefixed(
+            sub1 = os.path.join(dir2),
+            sub2 = os.path.join(dir3)
+        )])
+        tmpl = loader.load('sub1/tmpl1.html')
+        self.assertEqual("""<html>
+              <div>Included foo</div> from sub1
+            </html>""", tmpl.generate().render())
+
+    def test_prefix_delegation_to_directories_with_subdirs(self):
+        """
+        Test prefix delegation with the following layout:
+        
+        templates/foo.html
+        sub1/templates/tmpl1.html
+        sub1/templates/tmpl2.html
+        sub1/templates/bar/tmpl3.html
+        
+        Where sub1 is a prefix, and tmpl1.html includes all the others.
+        """
+        dir1 = os.path.join(self.dirname, 'templates')
+        os.mkdir(dir1)
+        file1 = open(os.path.join(dir1, 'foo.html'), 'w')
+        try:
+            file1.write("""<div>Included foo</div>""")
+        finally:
+            file1.close()
+
+        dir2 = os.path.join(self.dirname, 'sub1', 'templates')
+        os.makedirs(dir2)
+        file2 = open(os.path.join(dir2, 'tmpl1.html'), 'w')
+        try:
+            file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude">
+              <xi:include href="../foo.html" /> from sub1
+              <xi:include href="tmpl2.html" /> from sub1
+              <xi:include href="bar/tmpl3.html" /> from sub1
+            </html>""")
+        finally:
+            file2.close()
+
+        file3 = open(os.path.join(dir2, 'tmpl2.html'), 'w')
+        try:
+            file3.write("""<div>tmpl2</div>""")
+        finally:
+            file3.close()
+
+        dir3 = os.path.join(self.dirname, 'sub1', 'templates', 'bar')
+        os.makedirs(dir3)
+        file4 = open(os.path.join(dir3, 'tmpl3.html'), 'w')
+        try:
+            file4.write("""<div>bar/tmpl3</div>""")
+        finally:
+            file4.close()
+
+        loader = TemplateLoader([dir1, TemplateLoader.prefixed(
+            sub1 = os.path.join(dir2),
+            sub2 = os.path.join(dir3)
+        )])
+        tmpl = loader.load('sub1/tmpl1.html')
+        self.assertEqual("""<html>
+              <div>Included foo</div> from sub1
+              <div>tmpl2</div> from sub1
+              <div>bar/tmpl3</div> from sub1
+            </html>""", tmpl.generate().render())
+
 
 def suite():
     suite = unittest.TestSuite()
--- a/genshi/template/tests/markup.py
+++ b/genshi/template/tests/markup.py
@@ -611,6 +611,22 @@
           </body>
         </html>""", tmpl.generate().render())
 
+    def test_with_in_match(self): 
+        xml = ("""<html xmlns:py="http://genshi.edgewall.org/">
+          <py:match path="body/p">
+            <h1>${select('text()')}</h1>
+            ${select('.')}
+          </py:match>
+          <body><p py:with="foo='bar'">${foo}</p></body>
+        </html>""")
+        tmpl = MarkupTemplate(xml, filename='test.html')
+        self.assertEqual("""<html>
+          <body>
+            <h1>bar</h1>
+            <p>bar</p>
+          </body>
+        </html>""", tmpl.generate().render())
+
     def test_nested_include_matches(self):
         # See ticket #157
         dirname = tempfile.mkdtemp(suffix='genshi_test')
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/match.py
@@ -0,0 +1,141 @@
+
+import unittest
+
+from genshi.core import START, QName
+from genshi.path import Path
+from genshi.template.match import MatchSet
+
+class MatchSetTestCase(unittest.TestCase):
+
+    def make_template(self, path):
+            
+        template = (path.test(ignore_context=True),
+                    path, [], set(), [], [])
+        return template
+
+    def make_tag_event(self, tag):
+        return (START, (QName(unicode(tag)), None), 0)
+    
+    def test_simple_match(self):
+
+        m = MatchSet()
+        t1 = self.make_template(Path("tag"))
+        m.add(t1)
+        
+        t2 = self.make_template(Path("tag2"))
+        m.add(t2)
+
+        result = m.find_matches(self.make_tag_event("tag"))
+
+        assert t1 in result
+        assert t2 not in result
+
+    def test_after(self):
+
+        m = MatchSet()
+        t1 = self.make_template(Path("tag"))
+        m.add(t1)
+        t2 = self.make_template(Path("tag2"))
+        m.add(t2)
+
+        m2 = m.after_template(t1)
+
+        result = m2.find_matches(self.make_tag_event("tag"))
+        
+        assert t1 not in result
+
+        result = m2.find_matches(self.make_tag_event("tag2"))
+
+        assert t2 in result
+        
+    def test_before_exclusive(self):
+
+        m = MatchSet()
+        t1 = self.make_template(Path("tag"))
+        m.add(t1)
+        t2 = self.make_template(Path("tag2"))
+        m.add(t2)
+
+        m2 = m.before_template(t2, False)
+
+        result = m2.find_matches(self.make_tag_event("tag2"))
+
+        assert t2 not in result
+
+        result = m2.find_matches(self.make_tag_event("tag"))
+        
+        assert t1 in result
+        
+        m3 = m.before_template(t1, False)
+
+        assert not m3
+        
+        result = m3.find_matches(self.make_tag_event("tag"))
+        
+        assert t1 not in result
+
+        
+    def test_before_inclusive(self):
+
+        m = MatchSet()
+        t1 = self.make_template(Path("tag"))
+        m.add(t1)
+        t2 = self.make_template(Path("tag2"))
+        m.add(t2)
+
+        t3 = self.make_template(Path("tag3"))
+        m.add(t3)
+
+        m2 = m.before_template(t2, True)
+
+        result = m2.find_matches(self.make_tag_event("tag2"))
+
+        assert t2 in result
+
+        result = m2.find_matches(self.make_tag_event("tag"))
+
+        assert t1 in result
+
+    def test_remove(self):
+
+        m = MatchSet()
+        t1 = self.make_template(Path("tag"))
+        m.add(t1)
+        t2 = self.make_template(Path("tag2"))
+        m.add(t2)
+
+        m.remove(t1)
+
+        result = m.find_matches(self.make_tag_event("tag"))
+
+        assert t1 not in result
+
+        result = m.find_matches(self.make_tag_event("tag2"))
+
+        assert t2 in result
+
+    def test_empty_range(self):
+        m = MatchSet()
+        t1 = self.make_template(Path("tag"))
+        m.add(t1)
+        t2 = self.make_template(Path("tag2"))
+        m.add(t2)
+
+        m2 = m.after_template(t1)
+        m3 = m2.before_template(t2, False)
+
+        assert not m3
+
+
+        
+        
+              
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(MatchSetTestCase, 'test'))
+    return suite
+
+test_suite = suite()
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
+
--- a/genshi/template/tests/text.py
+++ b/genshi/template/tests/text.py
@@ -232,6 +232,27 @@
 Included
 ----- Included data above this line -----""", tmpl.generate().render())
 
+    def test_include_expr(self):
+         file1 = open(os.path.join(self.dirname, 'tmpl1.txt'), 'w')
+         try:
+             file1.write("Included")
+         finally:
+             file1.close()
+ 
+         file2 = open(os.path.join(self.dirname, 'tmpl2.txt'), 'w')
+         try:
+             file2.write("""----- Included data below this line -----
+    {% include ${'%s.txt' % ('tmpl1',)} %}
+    ----- Included data above this line -----""")
+         finally:
+             file2.close()
+
+         loader = TemplateLoader([self.dirname])
+         tmpl = loader.load('tmpl2.txt', cls=NewTextTemplate)
+         self.assertEqual("""----- Included data below this line -----
+    Included
+    ----- Included data above this line -----""", tmpl.generate().render())
+
 
 def suite():
     suite = unittest.TestSuite()
--- a/genshi/template/text.py
+++ b/genshi/template/text.py
@@ -28,11 +28,12 @@
 
 import re
 
+from genshi.core import TEXT
 from genshi.template.base import BadDirectiveError, Template, \
                                  TemplateSyntaxError, EXEC, INCLUDE, SUB
 from genshi.template.eval import Suite
 from genshi.template.directives import *
-from genshi.template.directives import Directive, _apply_directives
+from genshi.template.directives import Directive
 from genshi.template.interpolation import interpolate
 
 __all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate']
@@ -188,7 +189,11 @@
 
             if command == 'include':
                 pos = (self.filename, lineno, 0)
-                stream.append((INCLUDE, (value.strip(), None, []), pos))
+                value = list(interpolate(value, self.basedir, self.filename,
+                                         lineno, 0, lookup=self.lookup))
+                if len(value) == 1 and value[0][0] is TEXT:
+                    value = value[0][1]
+                stream.append((INCLUDE, (value, None, []), pos))
 
             elif command == 'python':
                 if not self.allow_exec:
--- a/genshi/tests/core.py
+++ b/genshi/tests/core.py
@@ -14,6 +14,10 @@
 import doctest
 import pickle
 from StringIO import StringIO
+try:
+    from cStringIO import StringIO as cStringIO
+except ImportError:
+    cStringIO = StringIO
 import unittest
 
 from genshi import core
@@ -35,6 +39,18 @@
         xml = XML('<li>Über uns</li>')
         self.assertEqual('<li>&#220;ber uns</li>', xml.render(encoding='ascii'))
 
+    def test_render_output_stream_utf8(self):
+        xml = XML('<li>Über uns</li>')
+        strio = cStringIO()
+        self.assertEqual(None, xml.render(out=strio))
+        self.assertEqual('<li>Über uns</li>', strio.getvalue())
+
+    def test_render_output_stream_unicode(self):
+        xml = XML('<li>Über uns</li>')
+        strio = StringIO()
+        self.assertEqual(None, xml.render(encoding=None, out=strio))
+        self.assertEqual(u'<li>Über uns</li>', strio.getvalue())
+
     def test_pickle(self):
         xml = XML('<li>Foo</li>')
         buf = StringIO()
--- a/genshi/tests/output.py
+++ b/genshi/tests/output.py
@@ -228,7 +228,7 @@
     def test_xml_space(self):
         text = '<foo xml:space="preserve"> Do not mess  \n\n with me </foo>'
         output = XML(text).render(XHTMLSerializer)
-        self.assertEqual(text, output)
+        self.assertEqual('<foo> Do not mess  \n\n with me </foo>', output)
 
     def test_empty_script(self):
         text = """<html xmlns="http://www.w3.org/1999/xhtml">
Copyright (C) 2012-2017 Edgewall Software