changeset 830:de82830f8816 experimental-inline

inline branch: synced with trunk@1038.
author cmlenz
date Fri, 13 Mar 2009 20:04:26 +0000
parents eb8aa8690480
children 09cc3627654c
files ChangeLog genshi/core.py genshi/output.py genshi/path.py genshi/template/base.py genshi/template/directives.py genshi/template/eval.py genshi/template/loader.py genshi/template/markup.py genshi/template/tests/markup.py
diffstat 10 files changed, 194 insertions(+), 113 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -3,6 +3,7 @@
 (???, from branches/stable/0.6.x)
 
  * Support for Python 2.3 has been dropped.
+ * Added caching in the serilization stage for improved performance.
 
 
 Version 0.5.2
--- a/genshi/core.py
+++ b/genshi/core.py
@@ -468,6 +468,7 @@
         return Markup(unicode(self).join([escape(item, quotes=escape_quotes)
                                           for item in seq]))
 
+    @classmethod
     def escape(cls, text, quotes=True):
         """Create a Markup instance from a string and escape special characters
         it may contain (<, >, & and \").
@@ -501,7 +502,6 @@
         if quotes:
             text = text.replace('"', '&#34;')
         return cls(text)
-    escape = classmethod(escape)
 
     def unescape(self):
         """Reverse-escapes &, <, >, and \" and returns a `unicode` object.
--- a/genshi/output.py
+++ b/genshi/output.py
@@ -129,6 +129,7 @@
     )
     SVG = SVG_FULL
 
+    @classmethod
     def get(cls, name):
         """Return the ``(name, pubid, sysid)`` tuple of the ``DOCTYPE``
         declaration for the specified name.
@@ -164,7 +165,6 @@
             'svg-basic': cls.SVG_BASIC,
             'svg-tiny': cls.SVG_TINY
         }.get(name.lower())
-    get = classmethod(get)
 
 
 class XMLSerializer(object):
@@ -179,7 +179,7 @@
     _PRESERVE_SPACE = frozenset()
 
     def __init__(self, doctype=None, strip_whitespace=True,
-                 namespace_prefixes=None):
+                 namespace_prefixes=None, cache=True):
         """Initialize the XML serializer.
         
         :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
@@ -188,42 +188,60 @@
                         defined in `DocType.get`
         :param strip_whitespace: whether extraneous whitespace should be
                                  stripped from the output
+        :param cache: whether to cache the text output per event, which
+                      improves performance for repetitive markup
         :note: Changed in 0.4.2: The  `doctype` parameter can now be a string.
+        :note: Changed in 0.6: The `cache` parameter was added
         """
         self.filters = [EmptyTagFilter()]
         if strip_whitespace:
             self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
-        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes))
+        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes,
+                                               cache=cache))
         if doctype:
             self.filters.append(DocTypeInserter(doctype))
+        self.cache = cache
 
     def __call__(self, stream):
         have_decl = have_doctype = False
         in_cdata = False
 
+        cache = {}
+        cache_get = cache.get
+        if self.cache:
+            def _emit(kind, input, output):
+                cache[kind, input] = output
+                return output
+        else:
+            def _emit(kind, input, output):
+                return output
+
         for filter_ in self.filters:
             stream = filter_(stream)
         for kind, data, pos in stream:
+            cached = cache_get((kind, data))
+            if cached is not None:
+                yield cached
 
-            if kind is START or kind is EMPTY:
+            elif kind is START or kind is EMPTY:
                 tag, attrib = data
                 buf = ['<', tag]
                 for attr, value in attrib:
                     buf += [' ', attr, '="', escape(value), '"']
                 buf.append(kind is EMPTY and '/>' or '>')
-                yield Markup(u''.join(buf))
+                yield _emit(kind, data, Markup(u''.join(buf)))
 
             elif kind is END:
-                yield Markup('</%s>' % data)
+                yield _emit(kind, data, Markup('</%s>' % data))
 
             elif kind is TEXT:
                 if in_cdata:
-                    yield data
+                    yield _emit(kind, data, data)
                 else:
-                    yield escape(data, quotes=False)
+                    yield _emit(kind, data, escape(data, quotes=False))
 
             elif kind is COMMENT:
-                yield Markup('<!--%s-->' % data)
+                yield _emit(kind, data, Markup('<!--%s-->' % data))
 
             elif kind is XML_DECL and not have_decl:
                 version, encoding, standalone = data
@@ -259,7 +277,7 @@
                 in_cdata = False
 
             elif kind is PI:
-                yield Markup('<?%s %s?>' % data)
+                yield _emit(kind, data, Markup('<?%s %s?>' % data))
 
 
 class XHTMLSerializer(XMLSerializer):
@@ -283,17 +301,19 @@
     ])
 
     def __init__(self, doctype=None, strip_whitespace=True,
-                 namespace_prefixes=None, drop_xml_decl=True):
+                 namespace_prefixes=None, drop_xml_decl=True, cache=True):
         super(XHTMLSerializer, self).__init__(doctype, False)
         self.filters = [EmptyTagFilter()]
         if strip_whitespace:
             self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
         namespace_prefixes = namespace_prefixes or {}
         namespace_prefixes['http://www.w3.org/1999/xhtml'] = ''
-        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes))
+        self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes,
+                                               cache=cache))
         if doctype:
             self.filters.append(DocTypeInserter(doctype))
         self.drop_xml_decl = drop_xml_decl
+        self.cache = cache
 
     def __call__(self, stream):
         boolean_attrs = self._BOOLEAN_ATTRS
@@ -302,11 +322,24 @@
         have_decl = have_doctype = False
         in_cdata = False
 
+        cache = {}
+        cache_get = cache.get
+        if self.cache:
+            def _emit(kind, input, output):
+                cache[kind, input] = output
+                return output
+        else:
+            def _emit(kind, input, output):
+                return output
+
         for filter_ in self.filters:
             stream = filter_(stream)
         for kind, data, pos in stream:
+            cached = cache_get((kind, data))
+            if cached is not None:
+                yield cached
 
-            if kind is START or kind is EMPTY:
+            elif kind is START or kind is EMPTY:
                 tag, attrib = data
                 buf = ['<', tag]
                 for attr, value in attrib:
@@ -324,19 +357,19 @@
                         buf.append('></%s>' % tag)
                 else:
                     buf.append('>')
-                yield Markup(u''.join(buf))
+                yield _emit(kind, data, Markup(u''.join(buf)))
 
             elif kind is END:
-                yield Markup('</%s>' % data)
+                yield _emit(kind, data, Markup('</%s>' % data))
 
             elif kind is TEXT:
                 if in_cdata:
-                    yield data
+                    yield _emit(kind, data, data)
                 else:
-                    yield escape(data, quotes=False)
+                    yield _emit(kind, data, escape(data, quotes=False))
 
             elif kind is COMMENT:
-                yield Markup('<!--%s-->' % data)
+                yield _emit(kind, data, Markup('<!--%s-->' % data))
 
             elif kind is DOCTYPE and not have_doctype:
                 name, pubid, sysid = data
@@ -372,7 +405,7 @@
                 in_cdata = False
 
             elif kind is PI:
-                yield Markup('<?%s %s?>' % data)
+                yield _emit(kind, data, Markup('<?%s %s?>' % data))
 
 
 class HTMLSerializer(XHTMLSerializer):
@@ -389,7 +422,7 @@
         QName('style'), QName('http://www.w3.org/1999/xhtml}style')
     ])
 
-    def __init__(self, doctype=None, strip_whitespace=True):
+    def __init__(self, doctype=None, strip_whitespace=True, cache=True):
         """Initialize the HTML serializer.
         
         :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
@@ -397,6 +430,9 @@
                         of the generated output
         :param strip_whitespace: whether extraneous whitespace should be
                                  stripped from the output
+        :param cache: whether to cache the text output per event, which
+                      improves performance for repetitive markup
+        :note: Changed in 0.6: The `cache` parameter was added
         """
         super(HTMLSerializer, self).__init__(doctype, False)
         self.filters = [EmptyTagFilter()]
@@ -405,9 +441,10 @@
                                                  self._NOESCAPE_ELEMS))
         self.filters.append(NamespaceFlattener(prefixes={
             'http://www.w3.org/1999/xhtml': ''
-        }))
+        }, cache=cache))
         if doctype:
             self.filters.append(DocTypeInserter(doctype))
+        self.cache = True
 
     def __call__(self, stream):
         boolean_attrs = self._BOOLEAN_ATTRS
@@ -416,11 +453,28 @@
         have_doctype = False
         noescape = False
 
+        cache = {}
+        cache_get = cache.get
+        if self.cache:
+            def _emit(kind, input, output):
+                cache[kind, input] = output
+                return output
+        else:
+            def _emit(kind, input, output):
+                return output
+
         for filter_ in self.filters:
             stream = filter_(stream)
-        for kind, data, pos in stream:
+        for kind, data, _ in stream:
+            output = cache_get((kind, data))
+            if output is not None:
+                yield output
+                if kind is START or kind is EMPTY and data[0] in noescape_elems:
+                    noescape = True
+                elif kind is END:
+                    noescape = False
 
-            if kind is START or kind is EMPTY:
+            elif kind is START or kind is EMPTY:
                 tag, attrib = data
                 buf = ['<', tag]
                 for attr, value in attrib:
@@ -436,22 +490,22 @@
                 if kind is EMPTY:
                     if tag not in empty_elems:
                         buf.append('</%s>' % tag)
-                yield Markup(u''.join(buf))
+                yield _emit(kind, data, Markup(u''.join(buf)))
                 if tag in noescape_elems:
                     noescape = True
 
             elif kind is END:
-                yield Markup('</%s>' % data)
+                yield _emit(kind, data, Markup('</%s>' % data))
                 noescape = False
 
             elif kind is TEXT:
                 if noescape:
-                    yield data
+                    yield _emit(kind, data, data)
                 else:
-                    yield escape(data, quotes=False)
+                    yield _emit(kind, data, escape(data, quotes=False))
 
             elif kind is COMMENT:
-                yield Markup('<!--%s-->' % data)
+                yield _emit(kind, data, Markup('<!--%s-->' % data))
 
             elif kind is DOCTYPE and not have_doctype:
                 name, pubid, sysid = data
@@ -467,7 +521,7 @@
                 have_doctype = True
 
             elif kind is PI:
-                yield Markup('<?%s %s?>' % data)
+                yield _emit(kind, data, Markup('<?%s %s?>' % data))
 
 
 class TextSerializer(object):
@@ -561,17 +615,41 @@
     END u'doc'
     """
 
-    def __init__(self, prefixes=None):
+    def __init__(self, prefixes=None, cache=True):
         self.prefixes = {XML_NAMESPACE.uri: 'xml'}
         if prefixes is not None:
             self.prefixes.update(prefixes)
+        self.cache = cache
 
     def __call__(self, stream):
+        cache = {}
+        cache_get = cache.get
+        if self.cache:
+            def _emit(kind, input, output, pos):
+                cache[kind, input] = output
+                return kind, output, pos
+        else:
+            def _emit(kind, input, output, pos):
+                return output
+
         prefixes = dict([(v, [k]) for k, v in self.prefixes.items()])
         namespaces = {XML_NAMESPACE.uri: ['xml']}
         def _push_ns(prefix, uri):
             namespaces.setdefault(uri, []).append(prefix)
             prefixes.setdefault(prefix, []).append(uri)
+            cache.clear()
+        def _pop_ns(prefix):
+            uris = prefixes.get(prefix)
+            uri = uris.pop()
+            if not uris:
+                del prefixes[prefix]
+            if uri not in uris or uri != uris[-1]:
+                uri_prefixes = namespaces[uri]
+                uri_prefixes.pop()
+                if not uri_prefixes:
+                    del namespaces[uri]
+            cache.clear()
+            return uri
 
         ns_attrs = []
         _push_ns_attr = ns_attrs.append
@@ -586,8 +664,11 @@
         _gen_prefix = _gen_prefix().next
 
         for kind, data, pos in stream:
+            output = cache_get((kind, data))
+            if output is not None:
+                yield kind, output, pos
 
-            if kind is START or kind is EMPTY:
+            elif kind is START or kind is EMPTY:
                 tag, attrs = data
 
                 tagname = tag.localname
@@ -616,7 +697,7 @@
                             attrname = u'%s:%s' % (prefix, attrname)
                     new_attrs.append((attrname, value))
 
-                yield kind, (tagname, Attrs(ns_attrs + new_attrs)), pos
+                yield _emit(kind, data, (tagname, Attrs(ns_attrs + new_attrs)), pos)
                 del ns_attrs[:]
 
             elif kind is END:
@@ -626,7 +707,7 @@
                     prefix = namespaces[tagns][-1]
                     if prefix:
                         tagname = u'%s:%s' % (prefix, tagname)
-                yield kind, tagname, pos
+                yield _emit(kind, data, tagname, pos)
 
             elif kind is START_NS:
                 prefix, uri = data
@@ -637,15 +718,7 @@
 
             elif kind is END_NS:
                 if data in prefixes:
-                    uris = prefixes.get(data)
-                    uri = uris.pop()
-                    if not uris:
-                        del prefixes[data]
-                    if uri not in uris or uri != uris[-1]:
-                        uri_prefixes = namespaces[uri]
-                        uri_prefixes.pop()
-                        if not uri_prefixes:
-                            del namespaces[uri]
+                    uri = _pop_ns(data)
                     if ns_attrs:
                         attr = _make_ns_attr(data, uri)
                         if attr in ns_attrs:
--- a/genshi/path.py
+++ b/genshi/path.py
@@ -65,12 +65,12 @@
     DESCENDANT_OR_SELF = 'descendant-or-self'
     SELF = 'self'
 
+    @classmethod
     def forname(cls, name):
         """Return the axis constant for the given name, or `None` if no such
         axis was defined.
         """
         return getattr(cls, name.upper().replace('-', '_'), None)
-    forname = classmethod(forname)
 
 
 ATTRIBUTE = Axis.ATTRIBUTE
@@ -674,8 +674,13 @@
 
     # Tokenizer
 
-    at_end = property(lambda self: self.pos == len(self.tokens) - 1)
-    cur_token = property(lambda self: self.tokens[self.pos])
+    @property
+    def at_end(self):
+        return self.pos == len(self.tokens) - 1
+
+    @property
+    def cur_token(self):
+        return self.tokens[self.pos]
 
     def next_token(self):
         self.pos += 1
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -13,12 +13,7 @@
 
 """Basic templating functionality."""
 
-try:
-    from collections import deque
-except ImportError:
-    class deque(list):
-        def appendleft(self, x): self.insert(0, x)
-        def popleft(self): return self.pop(0)
+from collections import deque
 import os
 from StringIO import StringIO
 import sys
@@ -254,7 +249,7 @@
         """Pop the top-most scope from the stack."""
 
 
-def _apply_directives(stream, directives, ctxt, **vars):
+def _apply_directives(stream, directives, ctxt, vars):
     """Apply the given directives to the stream.
     
     :param stream: the stream the directives should be applied to
@@ -268,7 +263,8 @@
         stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
     return stream
 
-def _eval_expr(expr, ctxt, **vars):
+
+def _eval_expr(expr, ctxt, vars=None):
     """Evaluate the given `Expression` object.
     
     :param expr: the expression to evaluate
@@ -284,7 +280,8 @@
         ctxt.pop()
     return retval
 
-def _exec_suite(suite, ctxt, **vars):
+
+def _exec_suite(suite, ctxt, vars=None):
     """Execute the given `Suite` object.
     
     :param suite: the code suite to execute
@@ -424,12 +421,12 @@
         if self.loader:
             self.filters.append(self._include)
 
-    def _get_stream(self):
+    @property
+    def stream(self):
         if not self._prepared:
             self._stream = list(self._prepare(self._stream))
             self._prepared = True
         return self._stream
-    stream = property(_get_stream)
 
     def _parse(self, source, encoding):
         """Parse the template.
@@ -553,22 +550,20 @@
                 # this point, so do some evaluation
                 tag, attrs = data
                 new_attrs = []
-                for name, substream in attrs:
-                    if type(substream) is list:
-                        values = []
-                        for event in self._flatten(substream, ctxt, **vars):
-                            if event[0] is TEXT:
-                                values.append(event[1])
-                        value = [x for x in values if x is not None]
-                        if not value:
+                for name, value in attrs:
+                    if type(value) is list: # this is an interpolated string
+                        values = [event[1]
+                            for event in self._flatten(value, ctxt, **vars)
+                            if event[0] is TEXT and event[1] is not None
+                        ]
+                        if not values:
                             continue
-                    else:
-                        value = substream
-                    new_attrs.append((name, u''.join(value)))
+                        value = u''.join(values)
+                    new_attrs.append((name, value))
                 yield kind, (tag, Attrs(new_attrs)), pos
 
             elif kind is EXPR:
-                result = _eval_expr(data, ctxt, **vars)
+                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
@@ -585,12 +580,12 @@
                         yield TEXT, unicode(result), pos
 
             elif kind is EXEC:
-                _exec_suite(data, ctxt, **vars)
+                _exec_suite(data, ctxt, vars)
 
             elif kind is SUB:
                 # This event is a list of directives and a list of nested
                 # events to which those directives should be applied
-                substream = _apply_directives(data[1], data[0], ctxt, **vars)
+                substream = _apply_directives(data[1], data[0], ctxt, vars)
                 for event in self._flatten(substream, ctxt, **vars):
                     yield event
 
--- a/genshi/template/directives.py
+++ b/genshi/template/directives.py
@@ -60,6 +60,7 @@
                  offset=-1):
         self.expr = self._parse_expr(value, template, lineno, offset)
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         """Called after the template stream has been completely parsed.
         
@@ -80,7 +81,6 @@
         stream associated with the directive.
         """
         return cls(value, template, namespaces, *pos[1:]), stream
-    attach = classmethod(attach)
 
     def __call__(self, stream, directives, ctxt, **vars):
         """Apply the directive to the given stream.
@@ -100,6 +100,7 @@
             expr = ' "%s"' % self.expr.source
         return '<%s%s>' % (self.__class__.__name__, expr)
 
+    @classmethod
     def _parse_expr(cls, expr, template, lineno=-1, offset=-1):
         """Parses the given expression, raising a useful error message when a
         syntax error is encountered.
@@ -112,7 +113,6 @@
                                                                   cls.tagname)
             raise TemplateSyntaxError(err, template.filepath, lineno,
                                       offset + (err.offset or 0))
-    _parse_expr = classmethod(_parse_expr)
 
 
 def _assignment(ast):
@@ -166,7 +166,7 @@
     def __call__(self, stream, directives, ctxt, **vars):
         def _generate():
             kind, (tag, attrib), pos  = stream.next()
-            attrs = _eval_expr(self.expr, ctxt, **vars)
+            attrs = _eval_expr(self.expr, ctxt, vars)
             if attrs:
                 if isinstance(attrs, Stream):
                     try:
@@ -182,7 +182,7 @@
             for event in stream:
                 yield event
 
-        return _apply_directives(_generate(), directives, ctxt, **vars)
+        return _apply_directives(_generate(), directives, ctxt, vars)
 
 
 class ContentDirective(Directive):
@@ -202,6 +202,7 @@
     """
     __slots__ = []
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         if type(value) is dict:
             raise TemplateSyntaxError('The content directive can not be used '
@@ -209,7 +210,6 @@
                                       *pos[1:])
         expr = cls._parse_expr(value, template, *pos[1:])
         return None, [stream[0], (EXPR, expr, pos),  stream[-1]]
-    attach = classmethod(attach)
 
 
 class DefDirective(Directive):
@@ -281,12 +281,12 @@
         else:
             self.name = ast.id
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         if type(value) is dict:
             value = value.get('function')
         return super(DefDirective, cls).attach(template, stream, value,
                                                namespaces, pos)
-    attach = classmethod(attach)
 
     def __call__(self, stream, directives, ctxt, **vars):
         stream = list(stream)
@@ -301,14 +301,14 @@
                     if name in kwargs:
                         val = kwargs.pop(name)
                     else:
-                        val = _eval_expr(self.defaults.get(name), ctxt, **vars)
+                        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, directives, ctxt, **vars):
+            for event in _apply_directives(stream, directives, ctxt, vars):
                 yield event
             ctxt.pop()
         function.__name__ = self.name
@@ -350,15 +350,15 @@
         self.filename = template.filepath
         Directive.__init__(self, value, template, namespaces, lineno, offset)
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         if type(value) is dict:
             value = value.get('each')
         return super(ForDirective, cls).attach(template, stream, value,
                                                namespaces, pos)
-    attach = classmethod(attach)
 
     def __call__(self, stream, directives, ctxt, **vars):
-        iterable = _eval_expr(self.expr, ctxt, **vars)
+        iterable = _eval_expr(self.expr, ctxt, vars)
         if iterable is None:
             return
 
@@ -368,7 +368,7 @@
         for item in iterable:
             assign(scope, item)
             ctxt.push(scope)
-            for event in _apply_directives(stream, directives, ctxt, **vars):
+            for event in _apply_directives(stream, directives, ctxt, vars):
                 yield event
             ctxt.pop()
 
@@ -391,17 +391,17 @@
     """
     __slots__ = []
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         if type(value) is dict:
             value = value.get('test')
         return super(IfDirective, cls).attach(template, stream, value,
                                               namespaces, pos)
-    attach = classmethod(attach)
 
     def __call__(self, stream, directives, ctxt, **vars):
-        value = _eval_expr(self.expr, ctxt, **vars)
+        value = _eval_expr(self.expr, ctxt, vars)
         if value:
-            return _apply_directives(stream, directives, ctxt, **vars)
+            return _apply_directives(stream, directives, ctxt, vars)
         return []
 
 
@@ -431,6 +431,7 @@
         self.namespaces = namespaces or {}
         self.hints = hints or ()
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         hints = []
         if type(value) is dict:
@@ -443,7 +444,6 @@
             value = value.get('path')
         return cls(value, template, frozenset(hints), namespaces, *pos[1:]), \
                stream
-    attach = classmethod(attach)
 
     def __call__(self, stream, directives, ctxt, **vars):
         ctxt._match_templates.append((self.path.test(ignore_context=True),
@@ -483,6 +483,7 @@
     """
     __slots__ = []
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         if type(value) is dict:
             value = value.get('value')
@@ -491,7 +492,6 @@
                                       template.filepath, *pos[1:])
         expr = cls._parse_expr(value, template, *pos[1:])
         return None, [(EXPR, expr, pos)]
-    attach = classmethod(attach)
 
 
 class StripDirective(Directive):
@@ -529,7 +529,7 @@
 
     def __call__(self, stream, directives, ctxt, **vars):
         def _generate():
-            if _eval_expr(self.expr, ctxt, **vars):
+            if _eval_expr(self.expr, ctxt, vars):
                 stream.next() # skip start tag
                 previous = stream.next()
                 for event in stream:
@@ -538,14 +538,14 @@
             else:
                 for event in stream:
                     yield event
-        return _apply_directives(_generate(), directives, ctxt, **vars)
+        return _apply_directives(_generate(), directives, ctxt, vars)
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         if not value:
             return None, stream[1:-1]
         return super(StripDirective, cls).attach(template, stream, value,
                                                  namespaces, pos)
-    attach = classmethod(attach)
 
 
 class ChooseDirective(Directive):
@@ -589,19 +589,19 @@
     """
     __slots__ = ['matched', 'value']
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         if type(value) is dict:
             value = value.get('test')
         return super(ChooseDirective, cls).attach(template, stream, value,
                                                   namespaces, pos)
-    attach = classmethod(attach)
 
     def __call__(self, stream, directives, ctxt, **vars):
         info = [False, bool(self.expr), None]
         if self.expr:
-            info[2] = _eval_expr(self.expr, ctxt, **vars)
+            info[2] = _eval_expr(self.expr, ctxt, vars)
         ctxt._choice_stack.append(info)
-        for event in _apply_directives(stream, directives, ctxt, **vars):
+        for event in _apply_directives(stream, directives, ctxt, vars):
             yield event
         ctxt._choice_stack.pop()
 
@@ -618,12 +618,12 @@
         Directive.__init__(self, value, template, namespaces, lineno, offset)
         self.filename = template.filepath
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         if type(value) is dict:
             value = value.get('test')
         return super(WhenDirective, cls).attach(template, stream, value,
                                                 namespaces, pos)
-    attach = classmethod(attach)
 
     def __call__(self, stream, directives, ctxt, **vars):
         info = ctxt._choice_stack and ctxt._choice_stack[-1]
@@ -640,16 +640,16 @@
         if info[1]:
             value = info[2]
             if self.expr:
-                matched = value == _eval_expr(self.expr, ctxt, **vars)
+                matched = value == _eval_expr(self.expr, ctxt, vars)
             else:
                 matched = bool(value)
         else:
-            matched = bool(_eval_expr(self.expr, ctxt, **vars))
+            matched = bool(_eval_expr(self.expr, ctxt, vars))
         info[0] = matched
         if not matched:
             return []
 
-        return _apply_directives(stream, directives, ctxt, **vars)
+        return _apply_directives(stream, directives, ctxt, vars)
 
 
 class OtherwiseDirective(Directive):
@@ -674,7 +674,7 @@
             return []
         info[0] = True
 
-        return _apply_directives(stream, directives, ctxt, **vars)
+        return _apply_directives(stream, directives, ctxt, vars)
 
 
 class WithDirective(Directive):
@@ -712,21 +712,21 @@
             raise TemplateSyntaxError(err, template.filepath, lineno,
                                       offset + (err.offset or 0))
 
+    @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
         if type(value) is dict:
             value = value.get('vars')
         return super(WithDirective, cls).attach(template, stream, value,
                                                 namespaces, pos)
-    attach = classmethod(attach)
 
     def __call__(self, stream, directives, ctxt, **vars):
         frame = {}
         ctxt.push(frame)
         for targets, expr in self.vars:
-            value = _eval_expr(expr, ctxt, **vars)
+            value = _eval_expr(expr, ctxt, vars)
             for _, assign in targets:
                 assign(frame, value)
-        for event in _apply_directives(stream, directives, 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
@@ -281,6 +281,7 @@
 class LookupBase(object):
     """Abstract base class for variable lookup implementations."""
 
+    @classmethod
     def globals(cls, data):
         """Construct the globals dictionary to use as the execution context for
         the expression or suite.
@@ -293,8 +294,8 @@
             '_star_import_patch': _star_import_patch,
             'UndefinedError': UndefinedError,
         }
-    globals = classmethod(globals)
 
+    @classmethod
     def lookup_name(cls, data, name):
         __traceback_hide__ = True
         val = data.get(name, UNDEFINED)
@@ -303,8 +304,8 @@
             if val is UNDEFINED:
                 val = cls.undefined(name)
         return val
-    lookup_name = classmethod(lookup_name)
 
+    @classmethod
     def lookup_attr(cls, obj, key):
         __traceback_hide__ = True
         try:
@@ -318,8 +319,8 @@
                 except (KeyError, TypeError):
                     val = cls.undefined(key, owner=obj)
         return val
-    lookup_attr = classmethod(lookup_attr)
 
+    @classmethod
     def lookup_item(cls, obj, key):
         __traceback_hide__ = True
         if len(key) == 1:
@@ -333,8 +334,8 @@
                     val = cls.undefined(key, owner=obj)
                 return val
             raise
-    lookup_item = classmethod(lookup_item)
 
+    @classmethod
     def undefined(cls, key, owner=UNDEFINED):
         """Can be overridden by subclasses to specify behavior when undefined
         variables are accessed.
@@ -343,7 +344,6 @@
         :param owner: the owning object, if the variable is accessed as a member
         """
         raise NotImplementedError
-    undefined = classmethod(undefined)
 
 
 class LenientLookup(LookupBase):
@@ -369,11 +369,12 @@
     
     :see: `StrictLookup`
     """
+
+    @classmethod
     def undefined(cls, key, owner=UNDEFINED):
         """Return an ``Undefined`` object."""
         __traceback_hide__ = True
         return Undefined(key, owner=owner)
-    undefined = classmethod(undefined)
 
 
 class StrictLookup(LookupBase):
@@ -397,11 +398,12 @@
         ...
     UndefinedError: {} has no member named "nil"
     """
+
+    @classmethod
     def undefined(cls, key, owner=UNDEFINED):
         """Raise an ``UndefinedError`` immediately."""
         __traceback_hide__ = True
         raise UndefinedError(key, owner=owner)
-    undefined = classmethod(undefined)
 
 
 def _parse(source, mode='eval'):
--- a/genshi/template/loader.py
+++ b/genshi/template/loader.py
@@ -264,6 +264,7 @@
                    encoding=encoding, lookup=self.variable_lookup,
                    allow_exec=self.allow_exec)
 
+    @staticmethod
     def directory(path):
         """Loader factory for loading templates from a local directory.
         
@@ -279,8 +280,8 @@
                 return mtime == os.path.getmtime(filepath)
             return filepath, filename, fileobj, _uptodate
         return _load_from_directory
-    directory = staticmethod(directory)
 
+    @staticmethod
     def package(name, path):
         """Loader factory for loading templates from egg package data.
         
@@ -294,8 +295,8 @@
             filepath = os.path.join(path, filename)
             return filepath, filename, resource_stream(name, filepath), None
         return _load_from_package
-    package = staticmethod(package)
 
+    @staticmethod
     def prefixed(**delegates):
         """Factory for a load function that delegates to other loaders
         depending on the prefix of the requested template path.
@@ -327,7 +328,7 @@
                     return filepath, filename, fileobj, uptodate
             raise TemplateNotFound(filename, delegates.keys())
         return _dispatch_by_prefix
-    prefixed = staticmethod(prefixed)
+
 
 directory = TemplateLoader.directory
 package = TemplateLoader.package
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -356,7 +356,7 @@
                         pre_end -= 1
                     inner = _strip(stream)
                     if pre_end > 0:
-                        inner = self._match(inner, ctxt, end=pre_end)
+                        inner = self._match(inner, ctxt, end=pre_end, **vars)
                     content = self._include(chain([event], inner, tail), ctxt)
                     if 'not_buffered' not in hints:
                         content = list(content)
@@ -371,7 +371,7 @@
 
                     # Recursively process the output
                     template = _apply_directives(template, directives, ctxt,
-                                                 **vars)
+                                                 vars)
                     for event in self._match(self._flatten(template, ctxt,
                                                            **vars),
                                              ctxt, start=idx + 1, **vars):
--- a/genshi/template/tests/markup.py
+++ b/genshi/template/tests/markup.py
@@ -75,6 +75,10 @@
         tmpl = MarkupTemplate('<root attr=""/>')
         self.assertEqual('<root attr=""/>', str(tmpl.generate()))
 
+    def test_empty_attr_interpolated(self):
+        tmpl = MarkupTemplate('<root attr="$attr"/>')
+        self.assertEqual('<root attr=""/>', str(tmpl.generate(attr='')))
+
     def test_bad_directive_error(self):
         xml = '<p xmlns:py="http://genshi.edgewall.org/" py:do="nothing" />'
         try:
Copyright (C) 2012-2017 Edgewall Software