# HG changeset patch # User cmlenz # Date 1236974666 0 # Node ID de82830f8816d57ec397ce43b13e138fe8c635e9 # Parent eb8aa869048043145cd880a7cac2c0ae5eee71ca inline branch: synced with trunk@1038. diff --git a/ChangeLog b/ChangeLog --- 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 diff --git a/genshi/core.py b/genshi/core.py --- 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('"', '"') return cls(text) - escape = classmethod(escape) def unescape(self): """Reverse-escapes &, <, >, and \" and returns a `unicode` object. diff --git a/genshi/output.py b/genshi/output.py --- 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('' % data) + yield _emit(kind, data, Markup('' % 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('' % data) + yield _emit(kind, data, Markup('' % 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('' % data) + yield _emit(kind, data, Markup('' % 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('>' % tag) else: buf.append('>') - yield Markup(u''.join(buf)) + yield _emit(kind, data, Markup(u''.join(buf))) elif kind is END: - yield Markup('' % data) + yield _emit(kind, data, Markup('' % 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('' % data) + yield _emit(kind, data, Markup('' % 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('' % data) + yield _emit(kind, data, Markup('' % 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('' % 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('' % data) + yield _emit(kind, data, Markup('' % 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('' % data) + yield _emit(kind, data, Markup('' % 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('' % data) + yield _emit(kind, data, Markup('' % 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: diff --git a/genshi/path.py b/genshi/path.py --- 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 diff --git a/genshi/template/base.py b/genshi/template/base.py --- 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 diff --git a/genshi/template/directives.py b/genshi/template/directives.py --- 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() diff --git a/genshi/template/eval.py b/genshi/template/eval.py --- 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'): diff --git a/genshi/template/loader.py b/genshi/template/loader.py --- 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 diff --git a/genshi/template/markup.py b/genshi/template/markup.py --- 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): diff --git a/genshi/template/tests/markup.py b/genshi/template/tests/markup.py --- a/genshi/template/tests/markup.py +++ b/genshi/template/tests/markup.py @@ -75,6 +75,10 @@ tmpl = MarkupTemplate('') self.assertEqual('', str(tmpl.generate())) + def test_empty_attr_interpolated(self): + tmpl = MarkupTemplate('') + self.assertEqual('', str(tmpl.generate(attr=''))) + def test_bad_directive_error(self): xml = '

' try: