diff options
Diffstat (limited to 'websdk/genshi/template/directives.py')
-rw-r--r-- | websdk/genshi/template/directives.py | 725 |
1 files changed, 725 insertions, 0 deletions
diff --git a/websdk/genshi/template/directives.py b/websdk/genshi/template/directives.py new file mode 100644 index 0000000..e2c9424 --- /dev/null +++ b/websdk/genshi/template/directives.py @@ -0,0 +1,725 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Implementation of the various template directives.""" + +from genshi.core import QName, Stream +from genshi.path import Path +from genshi.template.base import TemplateRuntimeError, TemplateSyntaxError, \ + EXPR, _apply_directives, _eval_expr +from genshi.template.eval import Expression, ExpressionASTTransformer, \ + _ast, _parse + +__all__ = ['AttrsDirective', 'ChooseDirective', 'ContentDirective', + 'DefDirective', 'ForDirective', 'IfDirective', 'MatchDirective', + 'OtherwiseDirective', 'ReplaceDirective', 'StripDirective', + 'WhenDirective', 'WithDirective'] +__docformat__ = 'restructuredtext en' + + +class DirectiveMeta(type): + """Meta class for template directives.""" + + def __new__(cls, name, bases, d): + d['tagname'] = name.lower().replace('directive', '') + return type.__new__(cls, name, bases, d) + + +class Directive(object): + """Abstract base class for template directives. + + A directive is basically a callable that takes three positional arguments: + ``ctxt`` is the template data context, ``stream`` is an iterable over the + events that the directive applies to, and ``directives`` is is a list of + other directives on the same stream that need to be applied. + + Directives can be "anonymous" or "registered". Registered directives can be + applied by the template author using an XML attribute with the + corresponding name in the template. Such directives should be subclasses of + this base class that can be instantiated with the value of the directive + attribute as parameter. + + Anonymous directives are simply functions conforming to the protocol + described above, and can only be applied programmatically (for example by + template filters). + """ + __metaclass__ = DirectiveMeta + __slots__ = ['expr'] + + def __init__(self, value, template=None, namespaces=None, lineno=-1, + 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. + + :param template: the `Template` object + :param stream: the event stream associated with the directive + :param value: the argument value for the directive; if the directive was + specified as an element, this will be an `Attrs` instance + with all specified attributes, otherwise it will be a + `unicode` object with just the attribute value + :param namespaces: a mapping of namespace URIs to prefixes + :param pos: a ``(filename, lineno, offset)`` tuple describing the + location where the directive was found in the source + + This class method should return a ``(directive, stream)`` tuple. If + ``directive`` is not ``None``, it should be an instance of the `Directive` + class, and gets added to the list of directives applied to the substream + at runtime. `stream` is an event stream that replaces the original + stream associated with the directive. + """ + return cls(value, template, namespaces, *pos[1:]), stream + + def __call__(self, stream, directives, ctxt, **vars): + """Apply the directive to the given stream. + + :param stream: the event stream + :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 + + def __repr__(self): + expr = '' + if getattr(self, 'expr', None) is not None: + expr = ' "%s"' % self.expr.source + return '<%s%s>' % (type(self).__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. + """ + try: + return expr and Expression(expr, template.filepath, lineno, + lookup=template.lookup) or None + except SyntaxError, err: + err.msg += ' in expression "%s" of "%s" directive' % (expr, + cls.tagname) + raise TemplateSyntaxError(err, template.filepath, lineno, + offset + (err.offset or 0)) + + +def _assignment(ast): + """Takes the AST representation of an assignment, and returns a + function that applies the assignment of a given value to a dictionary. + """ + def _names(node): + if isinstance(node, _ast.Tuple): + return tuple([_names(child) for child in node.elts]) + elif isinstance(node, _ast.Name): + return node.id + def _assign(data, value, names=_names(ast)): + if type(names) is tuple: + for idx in range(len(names)): + _assign(data, value[idx], names[idx]) + else: + data[names] = value + return _assign + + +class AttrsDirective(Directive): + """Implementation of the ``py:attrs`` template directive. + + The value of the ``py:attrs`` attribute should be a dictionary or a sequence + of ``(name, value)`` tuples. The items in that dictionary or sequence are + added as attributes to the element: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:attrs="foo">Bar</li> + ... </ul>''') + >>> print(tmpl.generate(foo={'class': 'collapse'})) + <ul> + <li class="collapse">Bar</li> + </ul> + >>> print(tmpl.generate(foo=[('class', 'collapse')])) + <ul> + <li class="collapse">Bar</li> + </ul> + + If the value evaluates to ``None`` (or any other non-truth value), no + attributes are added: + + >>> print(tmpl.generate(foo=None)) + <ul> + <li>Bar</li> + </ul> + """ + __slots__ = [] + + def __call__(self, stream, directives, ctxt, **vars): + def _generate(): + kind, (tag, attrib), pos = stream.next() + attrs = _eval_expr(self.expr, ctxt, vars) + if attrs: + if isinstance(attrs, Stream): + try: + attrs = iter(attrs).next() + except StopIteration: + attrs = [] + elif not isinstance(attrs, list): # assume it's a dict + attrs = attrs.items() + attrib -= [name for name, val in attrs if val is None] + attrib |= [(QName(name), unicode(val).strip()) for name, val + in attrs if val is not None] + yield kind, (tag, attrib), pos + for event in stream: + yield event + + return _apply_directives(_generate(), directives, ctxt, vars) + + +class ContentDirective(Directive): + """Implementation of the ``py:content`` template directive. + + This directive replaces the content of the element with the result of + evaluating the value of the ``py:content`` attribute: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:content="bar">Hello</li> + ... </ul>''') + >>> print(tmpl.generate(bar='Bye')) + <ul> + <li>Bye</li> + </ul> + """ + __slots__ = [] + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + raise TemplateSyntaxError('The content directive can not be used ' + 'as an element', template.filepath, + *pos[1:]) + expr = cls._parse_expr(value, template, *pos[1:]) + return None, [stream[0], (EXPR, expr, pos), stream[-1]] + + +class DefDirective(Directive): + """Implementation of the ``py:def`` template directive. + + This directive can be used to create "Named Template Functions", which + are template snippets that are not actually output during normal + processing, but rather can be expanded from expressions in other places + in the template. + + A named template function can be used just like a normal Python function + from template expressions: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <p py:def="echo(greeting, name='world')" class="message"> + ... ${greeting}, ${name}! + ... </p> + ... ${echo('Hi', name='you')} + ... </div>''') + >>> print(tmpl.generate(bar='Bye')) + <div> + <p class="message"> + Hi, you! + </p> + </div> + + If a function does not require parameters, the parenthesis can be omitted + in the definition: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <p py:def="helloworld" class="message"> + ... Hello, world! + ... </p> + ... ${helloworld()} + ... </div>''') + >>> print(tmpl.generate(bar='Bye')) + <div> + <p class="message"> + Hello, world! + </p> + </div> + """ + __slots__ = ['name', 'args', 'star_args', 'dstar_args', 'defaults'] + + def __init__(self, args, template, namespaces=None, lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + ast = _parse(args).body + self.args = [] + self.star_args = None + self.dstar_args = None + self.defaults = {} + if isinstance(ast, _ast.Call): + self.name = ast.func.id + for arg in ast.args: + # only names + self.args.append(arg.id) + for kwd in ast.keywords: + self.args.append(kwd.arg) + exp = Expression(kwd.value, template.filepath, + lineno, lookup=template.lookup) + self.defaults[kwd.arg] = exp + if getattr(ast, 'starargs', None): + self.star_args = ast.starargs.id + if getattr(ast, 'kwargs', None): + self.dstar_args = ast.kwargs.id + 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) + + def __call__(self, stream, directives, ctxt, **vars): + stream = list(stream) + + def function(*args, **kwargs): + scope = {} + args = list(args) # make mutable + for name in self.args: + if args: + scope[name] = args.pop(0) + else: + if name in kwargs: + val = kwargs.pop(name) + else: + 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): + yield event + ctxt.pop() + function.__name__ = self.name + + # Store the function reference in the bottom context frame so that it + # doesn't get popped off before processing the template has finished + # FIXME: this makes context data mutable as a side-effect + ctxt.frames[-1][self.name] = function + + return [] + + def __repr__(self): + return '<%s "%s">' % (type(self).__name__, self.name) + + +class ForDirective(Directive): + """Implementation of the ``py:for`` template directive for repeating an + element based on an iterable in the context data. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:for="item in items">${item}</li> + ... </ul>''') + >>> print(tmpl.generate(items=[1, 2, 3])) + <ul> + <li>1</li><li>2</li><li>3</li> + </ul> + """ + __slots__ = ['assign', 'filename'] + + def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): + if ' in ' not in value: + raise TemplateSyntaxError('"in" keyword missing in "for" directive', + template.filepath, lineno, offset) + assign, value = value.split(' in ', 1) + ast = _parse(assign, 'exec') + value = 'iter(%s)' % value.strip() + self.assign = _assignment(ast.body[0].value) + 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) + + def __call__(self, stream, directives, ctxt, **vars): + iterable = _eval_expr(self.expr, ctxt, vars) + if iterable is None: + return + + assign = self.assign + scope = {} + stream = list(stream) + for item in iterable: + assign(scope, item) + ctxt.push(scope) + for event in _apply_directives(stream, directives, ctxt, vars): + yield event + ctxt.pop() + + def __repr__(self): + return '<%s>' % type(self).__name__ + + +class IfDirective(Directive): + """Implementation of the ``py:if`` template directive for conditionally + excluding elements from being output. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <b py:if="foo">${bar}</b> + ... </div>''') + >>> print(tmpl.generate(foo=True, bar='Hello')) + <div> + <b>Hello</b> + </div> + """ + __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) + + def __call__(self, stream, directives, ctxt, **vars): + value = _eval_expr(self.expr, ctxt, vars) + if value: + return _apply_directives(stream, directives, ctxt, vars) + return [] + + +class MatchDirective(Directive): + """Implementation of the ``py:match`` template directive. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:match="greeting"> + ... Hello ${select('@name')} + ... </span> + ... <greeting name="Dude" /> + ... </div>''') + >>> print(tmpl.generate()) + <div> + <span> + Hello Dude + </span> + </div> + """ + __slots__ = ['path', 'namespaces', 'hints'] + + def __init__(self, value, template, hints=None, namespaces=None, + lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + self.path = Path(value, template.filepath, lineno) + self.namespaces = namespaces or {} + self.hints = hints or () + + @classmethod + 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': + hints.append('not_recursive') + value = value.get('path') + return cls(value, template, frozenset(hints), namespaces, *pos[1:]), \ + stream + + def __call__(self, stream, directives, ctxt, **vars): + ctxt._match_templates.append((self.path.test(ignore_context=True), + self.path, list(stream), self.hints, + self.namespaces, directives)) + return [] + + def __repr__(self): + return '<%s "%s">' % (type(self).__name__, self.path.source) + + +class ReplaceDirective(Directive): + """Implementation of the ``py:replace`` template directive. + + This directive replaces the element with the result of evaluating the + value of the ``py:replace`` attribute: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:replace="bar">Hello</span> + ... </div>''') + >>> print(tmpl.generate(bar='Bye')) + <div> + Bye + </div> + + This directive is equivalent to ``py:content`` combined with ``py:strip``, + providing a less verbose way to achieve the same effect: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:content="bar" py:strip="">Hello</span> + ... </div>''') + >>> print(tmpl.generate(bar='Bye')) + <div> + Bye + </div> + """ + __slots__ = [] + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('value') + if not value: + raise TemplateSyntaxError('missing value for "replace" directive', + template.filepath, *pos[1:]) + expr = cls._parse_expr(value, template, *pos[1:]) + return None, [(EXPR, expr, pos)] + + +class StripDirective(Directive): + """Implementation of the ``py:strip`` template directive. + + When the value of the ``py:strip`` attribute evaluates to ``True``, the + element is stripped from the output + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <div py:strip="True"><b>foo</b></div> + ... </div>''') + >>> print(tmpl.generate()) + <div> + <b>foo</b> + </div> + + Leaving the attribute value empty is equivalent to a truth value. + + This directive is particulary interesting for named template functions or + match templates that do not generate a top-level element: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <div py:def="echo(what)" py:strip=""> + ... <b>${what}</b> + ... </div> + ... ${echo('foo')} + ... </div>''') + >>> print(tmpl.generate()) + <div> + <b>foo</b> + </div> + """ + __slots__ = [] + + def __call__(self, stream, directives, ctxt, **vars): + def _generate(): + if not self.expr or _eval_expr(self.expr, ctxt, vars): + stream.next() # skip start tag + previous = stream.next() + for event in stream: + yield previous + previous = event + else: + for event in stream: + yield event + return _apply_directives(_generate(), directives, ctxt, vars) + + +class ChooseDirective(Directive): + """Implementation of the ``py:choose`` directive for conditionally selecting + one of several body elements to display. + + If the ``py:choose`` expression is empty the expressions of nested + ``py:when`` directives are tested for truth. The first true ``py:when`` + body is output. If no ``py:when`` directive is matched then the fallback + directive ``py:otherwise`` will be used. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" + ... py:choose=""> + ... <span py:when="0 == 1">0</span> + ... <span py:when="1 == 1">1</span> + ... <span py:otherwise="">2</span> + ... </div>''') + >>> print(tmpl.generate()) + <div> + <span>1</span> + </div> + + If the ``py:choose`` directive contains an expression, the nested + ``py:when`` directives are tested for equality to the ``py:choose`` + expression: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" + ... py:choose="2"> + ... <span py:when="1">1</span> + ... <span py:when="2">2</span> + ... </div>''') + >>> print(tmpl.generate()) + <div> + <span>2</span> + </div> + + Behavior is undefined if a ``py:choose`` block contains content outside a + ``py:when`` or ``py:otherwise`` block. Behavior is also undefined if a + ``py:otherwise`` occurs before ``py:when`` blocks. + """ + __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) + + def __call__(self, stream, directives, ctxt, **vars): + info = [False, bool(self.expr), None] + if self.expr: + info[2] = _eval_expr(self.expr, ctxt, vars) + ctxt._choice_stack.append(info) + for event in _apply_directives(stream, directives, ctxt, vars): + yield event + ctxt._choice_stack.pop() + + +class WhenDirective(Directive): + """Implementation of the ``py:when`` directive for nesting in a parent with + the ``py:choose`` directive. + + See the documentation of the `ChooseDirective` for usage. + """ + __slots__ = ['filename'] + + def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): + 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) + + 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 ' + 'inside a "choose" directive', + self.filename, *stream.next()[2][1:]) + if info[0]: + return [] + if not self.expr and not info[1]: + raise TemplateRuntimeError('either "choose" or "when" directive ' + 'must have a test expression', + self.filename, *stream.next()[2][1:]) + if info[1]: + value = info[2] + if self.expr: + matched = value == _eval_expr(self.expr, ctxt, vars) + else: + matched = bool(value) + else: + matched = bool(_eval_expr(self.expr, ctxt, vars)) + info[0] = matched + if not matched: + return [] + + return _apply_directives(stream, directives, ctxt, vars) + + +class OtherwiseDirective(Directive): + """Implementation of the ``py:otherwise`` directive for nesting in a parent + with the ``py:choose`` directive. + + See the documentation of `ChooseDirective` for usage. + """ + __slots__ = ['filename'] + + def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + self.filename = template.filepath + + 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 ' + 'used inside a "choose" directive', + self.filename, *stream.next()[2][1:]) + if info[0]: + return [] + info[0] = True + + return _apply_directives(stream, directives, ctxt, vars) + + +class WithDirective(Directive): + """Implementation of the ``py:with`` template directive, which allows + shorthand access to variables and expressions. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:with="y=7; z=x+10">$x $y $z</span> + ... </div>''') + >>> print(tmpl.generate(x=42)) + <div> + <span>42 7 52</span> + </div> + """ + __slots__ = ['vars'] + + def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + self.vars = [] + value = value.strip() + try: + ast = _parse(value, 'exec') + for node in ast.body: + if not isinstance(node, _ast.Assign): + raise TemplateSyntaxError('only assignment allowed in ' + 'value of the "with" directive', + template.filepath, lineno, offset) + self.vars.append(([_assignment(n) for n in node.targets], + Expression(node.value, template.filepath, + lineno, lookup=template.lookup))) + except SyntaxError, err: + err.msg += ' in expression "%s" of "%s" directive' % (value, + self.tagname) + 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) + + def __call__(self, stream, directives, ctxt, **vars): + frame = {} + ctxt.push(frame) + for targets, expr in self.vars: + value = _eval_expr(expr, ctxt, vars) + for assign in targets: + assign(frame, value) + for event in _apply_directives(stream, directives, ctxt, vars): + yield event + ctxt.pop() + + def __repr__(self): + return '<%s>' % (type(self).__name__) |