# HG changeset patch
# User cmlenz
# Date 1163001015 0
# Node ID 7763f7aec949f1a2db369c4cfa7abf6936e59041
# Parent 8e651f5f2ee0594bd2cd3db9c31350486ee4508c
Refactoring: `genshi.template` is now a package, it was getting way to crowded in that file.
diff --git a/ChangeLog b/ChangeLog
--- a/ChangeLog
+++ b/ChangeLog
@@ -24,6 +24,8 @@
* Fix XPath traversal in match templates. Previously, `div/p` would be treated
the same as `div//p`, i.e. it would match all descendants and not just the
immediate children.
+ * Split up the `genshi.template` module into multiple modules inside the new
+ `genshi.template` package.
Version 0.3.4
http://svn.edgewall.org/repos/genshi/tags/0.3.4/
diff --git a/UPGRADE.txt b/UPGRADE.txt
--- a/UPGRADE.txt
+++ b/UPGRADE.txt
@@ -1,6 +1,16 @@
Upgrading Genshi
================
+Upgrading from Genshi 0.3.x to 0.4.x
+------------------------------------
+
+The `genshi.template` module has been refactored into a package with
+multiple modules. While code using the normal templating APIs should
+continue to work without problems, you should make sure to remove any
+leftover traces of the `template.py` file on the installation path.
+This is not necessary when Genshi was installed as a Python egg.
+
+
Upgrading from Markup
---------------------
diff --git a/genshi/__init__.py b/genshi/__init__.py
--- a/genshi/__init__.py
+++ b/genshi/__init__.py
@@ -17,39 +17,6 @@
The design is centered around the concept of streams of markup events (similar
in concept to SAX parsing events) which can be processed in a uniform manner
independently of where or how they are produced.
-
-
-Generating content
-------------------
-
-Literal XML and HTML text can be used to easily produce markup streams
-via helper functions in the `genshi.input` module:
-
->>> from genshi.input import XML
->>> doc = XML('
My document')
-
-This results in a `Stream` object that can be used in a number of way.
-
->>> doc.render(method='html', encoding='utf-8')
-'My document'
-
->>> from genshi.input import HTML
->>> doc = HTML('My document')
->>> doc.render(method='html', encoding='utf-8')
-'My document'
-
->>> title = doc.select('head/title')
->>> title.render(method='html', encoding='utf-8')
-'My document'
-
-
-Markup streams can also be generated programmatically using the
-`genshi.builder` module:
-
->>> from genshi.builder import tag
->>> doc = tag.doc(tag.title('My document'), lang='en')
->>> doc.generate().render(method='html')
-'My document'
"""
from genshi.core import *
diff --git a/genshi/eval.py b/genshi/eval.py
deleted file mode 100644
--- a/genshi/eval.py
+++ /dev/null
@@ -1,427 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 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/.
-
-"""Support for "safe" evaluation of Python expressions."""
-
-import __builtin__
-from compiler import ast, parse
-from compiler.pycodegen import ExpressionCodeGenerator
-import new
-
-__all__ = ['Expression', 'Undefined']
-
-
-class Expression(object):
- """Evaluates Python expressions used in templates.
-
- >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
- >>> Expression('test').evaluate(data)
- 'Foo'
-
- >>> Expression('items[0]').evaluate(data)
- 1
- >>> Expression('items[-1]').evaluate(data)
- 3
- >>> Expression('dict["some"]').evaluate(data)
- 'thing'
-
- Similar to e.g. Javascript, expressions in templates can use the dot
- notation for attribute access to access items in mappings:
-
- >>> Expression('dict.some').evaluate(data)
- 'thing'
-
- This also works the other way around: item access can be used to access
- any object attribute (meaning there's no use for `getattr()` in templates):
-
- >>> class MyClass(object):
- ... myattr = 'Bar'
- >>> data = dict(mine=MyClass(), key='myattr')
- >>> Expression('mine.myattr').evaluate(data)
- 'Bar'
- >>> Expression('mine["myattr"]').evaluate(data)
- 'Bar'
- >>> Expression('mine[key]').evaluate(data)
- 'Bar'
-
- All of the standard Python operators are available to template expressions.
- Built-in functions such as `len()` are also available in template
- expressions:
-
- >>> data = dict(items=[1, 2, 3])
- >>> Expression('len(items)').evaluate(data)
- 3
- """
- __slots__ = ['source', 'code']
-
- def __init__(self, source, filename=None, lineno=-1):
- """Create the expression, either from a string, or from an AST node.
-
- @param source: either a string containing the source code of the
- expression, or an AST node
- @param filename: the (preferably absolute) name of the file containing
- the expression
- @param lineno: the number of the line on which the expression was found
- """
- if isinstance(source, basestring):
- self.source = source
- self.code = _compile(_parse(source), self.source, filename=filename,
- lineno=lineno)
- else:
- assert isinstance(source, ast.Node)
- self.source = '?'
- self.code = _compile(ast.Expression(source), filename=filename,
- lineno=lineno)
-
- def __repr__(self):
- return 'Expression(%r)' % self.source
-
- def evaluate(self, data, nocall=False):
- """Evaluate the expression against the given data dictionary.
-
- @param data: a mapping containing the data to evaluate against
- @param nocall: if true, the result of the evaluation is not called if
- if it is a callable
- @return: the result of the evaluation
- """
- retval = eval(self.code, {'data': data,
- '_lookup_name': _lookup_name,
- '_lookup_attr': _lookup_attr,
- '_lookup_item': _lookup_item},
- {'data': data})
- if not nocall and type(retval) is not Undefined and callable(retval):
- retval = retval()
- return retval
-
-
-class Undefined(object):
- """Represents a reference to an undefined variable.
-
- Unlike the Python runtime, template expressions can refer to an undefined
- variable without causing a `NameError` to be raised. The result will be an
- instance of the `Undefined´ class, which is treated the same as `False` in
- conditions, and acts as an empty collection in iterations:
-
- >>> foo = Undefined('foo')
- >>> bool(foo)
- False
- >>> list(foo)
- []
- >>> print foo
- undefined
-
- However, calling an undefined variable, or trying to access an attribute
- of that variable, will raise an exception that includes the name used to
- reference that undefined variable.
-
- >>> foo('bar')
- Traceback (most recent call last):
- ...
- NameError: Variable "foo" is not defined
-
- >>> foo.bar
- Traceback (most recent call last):
- ...
- NameError: Variable "foo" is not defined
- """
- __slots__ = ['_name']
-
- def __init__(self, name):
- self._name = name
-
- def __call__(self, *args, **kwargs):
- __traceback_hide__ = True
- self.throw()
-
- def __getattr__(self, name):
- __traceback_hide__ = True
- self.throw()
-
- def __iter__(self):
- return iter([])
-
- def __nonzero__(self):
- return False
-
- def __repr__(self):
- return 'undefined'
-
- def throw(self):
- __traceback_hide__ = True
- raise NameError('Variable "%s" is not defined' % self._name)
-
-
-def _parse(source, mode='eval'):
- if isinstance(source, unicode):
- source = '\xef\xbb\xbf' + source.encode('utf-8')
- return parse(source, mode)
-
-def _compile(node, source=None, filename=None, lineno=-1):
- tree = ExpressionASTTransformer().visit(node)
- if isinstance(filename, unicode):
- # unicode file names not allowed for code objects
- filename = filename.encode('utf-8', 'replace')
- elif not filename:
- filename = ''
- tree.filename = filename
- if lineno <= 0:
- lineno = 1
-
- gen = ExpressionCodeGenerator(tree)
- gen.optimized = True
- code = gen.getCode()
-
- # We'd like to just set co_firstlineno, but it's readonly. So we need to
- # clone the code object while adjusting the line number
- return new.code(0, code.co_nlocals, code.co_stacksize,
- code.co_flags | 0x0040, code.co_code, code.co_consts,
- code.co_names, code.co_varnames, filename,
- '' % (repr(source or '?').replace("'", '"')),
- lineno, code.co_lnotab, (), ())
-
-BUILTINS = __builtin__.__dict__.copy()
-BUILTINS['Undefined'] = Undefined
-
-def _lookup_name(data, name, locals_=None):
- __traceback_hide__ = True
- val = Undefined
- if locals_:
- val = locals_.get(name, val)
- if val is Undefined:
- val = data.get(name, val)
- if val is Undefined:
- val = BUILTINS.get(name, val)
- if val is not Undefined or name == 'Undefined':
- return val
- else:
- return val
- else:
- return val
- return val(name)
-
-def _lookup_attr(data, obj, key):
- __traceback_hide__ = True
- if type(obj) is Undefined:
- obj.throw()
- if hasattr(obj, key):
- return getattr(obj, key)
- try:
- return obj[key]
- except (KeyError, TypeError):
- return Undefined(key)
-
-def _lookup_item(data, obj, key):
- __traceback_hide__ = True
- if type(obj) is Undefined:
- obj.throw()
- if len(key) == 1:
- key = key[0]
- try:
- return obj[key]
- except (KeyError, IndexError, TypeError), e:
- if isinstance(key, basestring):
- val = getattr(obj, key, Undefined)
- if val is Undefined:
- val = Undefined(key)
- return val
- raise
-
-
-class ASTTransformer(object):
- """General purpose base class for AST transformations.
-
- Every visitor method can be overridden to return an AST node that has been
- altered or replaced in some way.
- """
- _visitors = {}
-
- def visit(self, node, *args, **kwargs):
- v = self._visitors.get(node.__class__)
- if not v:
- v = getattr(self, 'visit%s' % node.__class__.__name__)
- self._visitors[node.__class__] = v
- return v(node, *args, **kwargs)
-
- def visitExpression(self, node, *args, **kwargs):
- node.node = self.visit(node.node, *args, **kwargs)
- return node
-
- # Functions & Accessors
-
- def visitCallFunc(self, node, *args, **kwargs):
- node.node = self.visit(node.node, *args, **kwargs)
- node.args = [self.visit(x, *args, **kwargs) for x in node.args]
- if node.star_args:
- node.star_args = self.visit(node.star_args, *args, **kwargs)
- if node.dstar_args:
- node.dstar_args = self.visit(node.dstar_args, *args, **kwargs)
- return node
-
- def visitLambda(self, node, *args, **kwargs):
- node.code = self.visit(node.code, *args, **kwargs)
- node.filename = '' # workaround for bug in pycodegen
- return node
-
- def visitGetattr(self, node, *args, **kwargs):
- node.expr = self.visit(node.expr, *args, **kwargs)
- return node
-
- def visitSubscript(self, node, *args, **kwargs):
- node.expr = self.visit(node.expr, *args, **kwargs)
- node.subs = [self.visit(x, *args, **kwargs) for x in node.subs]
- return node
-
- # Operators
-
- def _visitBoolOp(self, node, *args, **kwargs):
- node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
- return node
- visitAnd = visitOr = visitBitand = visitBitor = _visitBoolOp
-
- def _visitBinOp(self, node, *args, **kwargs):
- node.left = self.visit(node.left, *args, **kwargs)
- node.right = self.visit(node.right, *args, **kwargs)
- return node
- visitAdd = visitSub = _visitBinOp
- visitDiv = visitFloorDiv = visitMod = visitMul = visitPower = _visitBinOp
- visitLeftShift = visitRightShift = _visitBinOp
-
- def visitCompare(self, node, *args, **kwargs):
- node.expr = self.visit(node.expr, *args, **kwargs)
- node.ops = [(op, self.visit(n, *args, **kwargs)) for op, n in node.ops]
- return node
-
- def _visitUnaryOp(self, node, *args, **kwargs):
- node.expr = self.visit(node.expr, *args, **kwargs)
- return node
- visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp
- visitBackquote = _visitUnaryOp
-
- # Identifiers, Literals and Comprehensions
-
- def _visitDefault(self, node, *args, **kwargs):
- return node
- visitAssName = visitAssTuple = _visitDefault
- visitConst = visitName = _visitDefault
-
- def visitDict(self, node, *args, **kwargs):
- node.items = [(self.visit(k, *args, **kwargs),
- self.visit(v, *args, **kwargs)) for k, v in node.items]
- return node
-
- def visitGenExpr(self, node, *args, **kwargs):
- node.code = self.visit(node.code, *args, **kwargs)
- node.filename = '' # workaround for bug in pycodegen
- return node
-
- def visitGenExprFor(self, node, *args, **kwargs):
- node.assign = self.visit(node.assign, *args, **kwargs)
- node.iter = self.visit(node.iter, *args, **kwargs)
- node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
- return node
-
- def visitGenExprIf(self, node, *args, **kwargs):
- node.test = self.visit(node.test, *args, **kwargs)
- return node
-
- def visitGenExprInner(self, node, *args, **kwargs):
- node.expr = self.visit(node.expr, *args, **kwargs)
- node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
- return node
-
- def visitKeyword(self, node, *args, **kwargs):
- node.expr = self.visit(node.expr, *args, **kwargs)
- return node
-
- def visitList(self, node, *args, **kwargs):
- node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
- return node
-
- def visitListComp(self, node, *args, **kwargs):
- node.expr = self.visit(node.expr, *args, **kwargs)
- node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
- return node
-
- def visitListCompFor(self, node, *args, **kwargs):
- node.assign = self.visit(node.assign, *args, **kwargs)
- node.list = self.visit(node.list, *args, **kwargs)
- node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
- return node
-
- def visitListCompIf(self, node, *args, **kwargs):
- node.test = self.visit(node.test, *args, **kwargs)
- return node
-
- def visitSlice(self, node, *args, **kwargs):
- node.expr = self.visit(node.expr, locals_=True, *args, **kwargs)
- if node.lower is not None:
- node.lower = self.visit(node.lower, *args, **kwargs)
- if node.upper is not None:
- node.upper = self.visit(node.upper, *args, **kwargs)
- return node
-
- def visitSliceobj(self, node, *args, **kwargs):
- node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
- return node
-
- def visitTuple(self, node, *args, **kwargs):
- node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
- return node
-
-
-class ExpressionASTTransformer(ASTTransformer):
- """Concrete AST transformer that implements the AST transformations needed
- for template expressions.
- """
-
- def visitConst(self, node, locals_=False):
- if isinstance(node.value, str):
- return ast.Const(node.value.decode('utf-8'))
- return node
-
- def visitGenExprIf(self, node, *args, **kwargs):
- node.test = self.visit(node.test, locals_=True)
- return node
-
- def visitGenExprInner(self, node, *args, **kwargs):
- node.expr = self.visit(node.expr, locals_=True)
- node.quals = [self.visit(x) for x in node.quals]
- return node
-
- def visitGetattr(self, node, locals_=False):
- return ast.CallFunc(ast.Name('_lookup_attr'), [
- ast.Name('data'), self.visit(node.expr, locals_=locals_),
- ast.Const(node.attrname)
- ])
-
- def visitLambda(self, node, locals_=False):
- node.code = self.visit(node.code, locals_=True)
- node.filename = '' # workaround for bug in pycodegen
- return node
-
- def visitListComp(self, node, locals_=False):
- node.expr = self.visit(node.expr, locals_=True)
- node.quals = [self.visit(qual, locals_=True) for qual in node.quals]
- return node
-
- def visitName(self, node, locals_=False):
- func_args = [ast.Name('data'), ast.Const(node.name)]
- if locals_:
- func_args.append(ast.CallFunc(ast.Name('locals'), []))
- return ast.CallFunc(ast.Name('_lookup_name'), func_args)
-
- def visitSubscript(self, node, locals_=False):
- return ast.CallFunc(ast.Name('_lookup_item'), [
- ast.Name('data'), self.visit(node.expr, locals_=locals_),
- ast.Tuple([self.visit(sub, locals_=locals_) for sub in node.subs])
- ])
diff --git a/genshi/plugin.py b/genshi/plugin.py
deleted file mode 100644
--- a/genshi/plugin.py
+++ /dev/null
@@ -1,156 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006 Edgewall Software
-# Copyright (C) 2006 Matthew Good
-# 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/.
-
-"""Basic support for the template engine plugin API used by TurboGears and
-CherryPy/Buffet.
-"""
-
-from pkg_resources import resource_filename
-
-from genshi.eval import Undefined
-from genshi.input import ET, HTML, XML
-from genshi.output import DocType
-from genshi.template import Context, MarkupTemplate, Template, TemplateLoader, \
- TextTemplate
-
-
-class ConfigurationError(Exception):
- """Exception raised when invalid plugin options are encountered."""
-
-
-class AbstractTemplateEnginePlugin(object):
- """Implementation of the plugin API."""
-
- template_class = None
- extension = None
-
- def __init__(self, extra_vars_func=None, options=None):
- self.get_extra_vars = extra_vars_func
- if options is None:
- options = {}
- self.options = options
-
- self.default_encoding = options.get('genshi.default_encoding', 'utf-8')
- auto_reload = options.get('genshi.auto_reload', '1').lower() \
- in ('1', 'yes', 'true')
- search_path = options.get('genshi.search_path', '').split(':')
- try:
- max_cache_size = int(options.get('genshi.max_cache_size', 25))
- except ValueError:
- raise ConfigurationError('Invalid value for max_cache_size: "%s"' %
- max_cache_size)
-
- self.loader = TemplateLoader(filter(None, search_path),
- auto_reload=auto_reload,
- max_cache_size=max_cache_size)
-
- def load_template(self, templatename, template_string=None):
- """Find a template specified in python 'dot' notation, or load one from
- a string.
- """
- if template_string is not None:
- return self.template_class(template_string)
-
- divider = templatename.rfind('.')
- if divider >= 0:
- package = templatename[:divider]
- basename = templatename[divider + 1:] + self.extension
- templatename = resource_filename(package, basename)
-
- return self.loader.load(templatename, cls=self.template_class)
-
- def _get_render_options(self, format=None):
- if format is None:
- format = self.default_format
- kwargs = {'method': format}
- if self.default_encoding:
- kwargs['encoding'] = self.default_encoding
- return kwargs
-
- def render(self, info, format=None, fragment=False, template=None):
- """Render the template to a string using the provided info."""
- kwargs = self._get_render_options(format=format)
- return self.transform(info, template).render(**kwargs)
-
- def transform(self, info, template):
- """Render the output to an event stream."""
- if not isinstance(template, Template):
- template = self.load_template(template)
- ctxt = Context(**info)
-
- # Some functions for Kid compatibility
- def defined(name):
- return ctxt.get(name, Undefined) is not Undefined
- ctxt['defined'] = defined
- def value_of(name, default=None):
- return ctxt.get(name, default)
- ctxt['value_of'] = value_of
-
- return template.generate(ctxt)
-
-
-class MarkupTemplateEnginePlugin(AbstractTemplateEnginePlugin):
- """Implementation of the plugin API for markup templates."""
-
- template_class = MarkupTemplate
- extension = '.html'
-
- doctypes = {'html': DocType.HTML, 'html-strict': DocType.HTML_STRICT,
- 'html-transitional': DocType.HTML_TRANSITIONAL,
- 'xhtml': DocType.XHTML, 'xhtml-strict': DocType.XHTML_STRICT,
- 'xhtml-transitional': DocType.XHTML_TRANSITIONAL}
-
- def __init__(self, extra_vars_func=None, options=None):
- AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
-
- doctype = options.get('genshi.default_doctype')
- if doctype and doctype not in self.doctypes:
- raise ConfigurationError('Unknown doctype "%s"' % doctype)
- self.default_doctype = self.doctypes.get(doctype)
-
- format = options.get('genshi.default_format', 'html')
- if format not in ('html', 'xhtml', 'xml', 'text'):
- raise ConfigurationError('Unknown output format "%s"' % format)
- self.default_format = format
-
- def _get_render_options(self, format=None):
- kwargs = super(MarkupTemplateEnginePlugin,
- self)._get_render_options(format)
- if self.default_doctype:
- kwargs['doctype'] = self.default_doctype
- return kwargs
-
- def transform(self, info, template):
- """Render the output to an event stream."""
- data = {'ET': ET, 'HTML': HTML, 'XML': XML}
- if self.get_extra_vars:
- data.update(self.get_extra_vars())
- data.update(info)
- return super(MarkupTemplateEnginePlugin, self).transform(data, template)
-
-
-class TextTemplateEnginePlugin(AbstractTemplateEnginePlugin):
- """Implementation of the plugin API for text templates."""
-
- template_class = TextTemplate
- extension = '.txt'
- default_format = 'text'
-
- def transform(self, info, template):
- """Render the output to an event stream."""
- data = {}
- if self.get_extra_vars:
- data.update(self.get_extra_vars())
- data.update(info)
- return super(TextTemplateEnginePlugin, self).transform(data, template)
diff --git a/genshi/template.py b/genshi/template/__init__.py
rename from genshi/template.py
rename to genshi/template/__init__.py
--- a/genshi/template.py
+++ b/genshi/template/__init__.py
@@ -13,1396 +13,9 @@
"""Implementation of the template engine."""
-from itertools import chain
-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)
-import compiler
-import os
-import re
-from StringIO import StringIO
-try:
- import threading
-except ImportError:
- import dummy_threading as threading
-
-from genshi.core import Attrs, Namespace, Stream, StreamEventKind, _ensure
-from genshi.core import START, END, START_NS, END_NS, TEXT, COMMENT
-from genshi.eval import Expression, _parse
-from genshi.input import XMLParser
-from genshi.path import Path
-from genshi.util import LRUCache
-
-__all__ = ['BadDirectiveError', 'MarkupTemplate', 'Template', 'TemplateError',
- 'TemplateSyntaxError', 'TemplateNotFound', 'TemplateLoader',
- 'TextTemplate']
-
-
-class TemplateError(Exception):
- """Base exception class for errors related to template processing."""
-
-
-class TemplateSyntaxError(TemplateError):
- """Exception raised when an expression in a template causes a Python syntax
- error."""
-
- def __init__(self, message, filename='', lineno=-1, offset=-1):
- if isinstance(message, SyntaxError) and message.lineno is not None:
- message = str(message).replace(' (line %d)' % message.lineno, '')
- self.msg = message
- message = '%s (%s, line %d)' % (self.msg, filename, lineno)
- TemplateError.__init__(self, message)
- self.filename = filename
- self.lineno = lineno
- self.offset = offset
-
-
-class BadDirectiveError(TemplateSyntaxError):
- """Exception raised when an unknown directive is encountered when parsing
- a template.
-
- An unknown directive is any attribute using the namespace for directives,
- with a local name that doesn't match any registered directive.
- """
-
- def __init__(self, name, filename='', lineno=-1):
- message = 'bad directive "%s"' % name
- TemplateSyntaxError.__init__(self, message, filename, lineno)
-
-
-class TemplateRuntimeError(TemplateError):
- """Exception raised when an the evualation of a Python expression in a
- template causes an error."""
-
- def __init__(self, message, filename='', lineno=-1, offset=-1):
- self.msg = message
- message = '%s (%s, line %d)' % (self.msg, filename, lineno)
- TemplateError.__init__(self, message)
- self.filename = filename
- self.lineno = lineno
- self.offset = offset
-
-
-class TemplateNotFound(TemplateError):
- """Exception raised when a specific template file could not be found."""
-
- def __init__(self, name, search_path):
- TemplateError.__init__(self, 'Template "%s" not found' % name)
- self.search_path = search_path
-
-
-class Context(object):
- """Container for template input data.
-
- A context provides a stack of scopes (represented by dictionaries).
-
- Template directives such as loops can push a new scope on the stack with
- data that should only be available inside the loop. When the loop
- terminates, that scope can get popped off the stack again.
-
- >>> ctxt = Context(one='foo', other=1)
- >>> ctxt.get('one')
- 'foo'
- >>> ctxt.get('other')
- 1
- >>> ctxt.push(dict(one='frost'))
- >>> ctxt.get('one')
- 'frost'
- >>> ctxt.get('other')
- 1
- >>> ctxt.pop()
- {'one': 'frost'}
- >>> ctxt.get('one')
- 'foo'
- """
-
- def __init__(self, **data):
- self.frames = deque([data])
- self.pop = self.frames.popleft
- self.push = self.frames.appendleft
- self._match_templates = []
-
- def __repr__(self):
- return repr(list(self.frames))
-
- def __setitem__(self, key, value):
- """Set a variable in the current scope."""
- self.frames[0][key] = value
-
- def _find(self, key, default=None):
- """Retrieve a given variable's value and the frame it was found in.
-
- Intented for internal use by directives.
- """
- for frame in self.frames:
- if key in frame:
- return frame[key], frame
- return default, None
-
- def get(self, key, default=None):
- """Get a variable's value, starting at the current scope and going
- upward.
- """
- for frame in self.frames:
- if key in frame:
- return frame[key]
- return default
- __getitem__ = get
-
- def push(self, data):
- """Push a new scope on the stack."""
-
- def pop(self):
- """Pop the top-most scope from the stack."""
-
-
-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).
- """
- __slots__ = ['expr']
-
- def __init__(self, value, namespaces=None, filename=None, lineno=-1,
- offset=-1):
- try:
- self.expr = value and Expression(value, filename, lineno) or None
- except SyntaxError, err:
- err.msg += ' in expression "%s" of "%s" directive' % (value,
- self.tagname)
- raise TemplateSyntaxError(err, filename, lineno,
- offset + (err.offset or 0))
-
- def __call__(self, stream, ctxt, directives):
- raise NotImplementedError
-
- def __repr__(self):
- expr = ''
- if self.expr is not None:
- expr = ' "%s"' % self.expr.source
- return '<%s%s>' % (self.__class__.__name__, expr)
-
- def tagname(self):
- """Return the local tag name of the directive as it is used in
- templates.
- """
- return self.__class__.__name__.lower().replace('directive', '')
- tagname = property(tagname)
-
-
-def _apply_directives(stream, ctxt, directives):
- """Apply the given directives to the stream."""
- if directives:
- stream = directives[0](iter(stream), ctxt, directives[1:])
- return stream
-
-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, (compiler.ast.AssTuple, compiler.ast.Tuple)):
- return tuple([_names(child) for child in node.nodes])
- elif isinstance(node, (compiler.ast.AssName, compiler.ast.Name)):
- return node.name
- 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:
-
- >>> tmpl = MarkupTemplate('''
-
- If the value evaluates to `None` (or any other non-truth value), no
- attributes are added:
-
- >>> print tmpl.generate(foo=None)
-
-
Bar
-
- """
- __slots__ = []
-
- def __call__(self, stream, ctxt, directives):
- def _generate():
- kind, (tag, attrib), pos = stream.next()
- attrs = self.expr.evaluate(ctxt)
- if attrs:
- attrib = Attrs(attrib[:])
- 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()
- for name, value in attrs:
- if value is None:
- attrib.remove(name)
- else:
- attrib.set(name, unicode(value).strip())
- yield kind, (tag, attrib), pos
- for event in stream:
- yield event
-
- return _apply_directives(_generate(), ctxt, directives)
-
-
-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:
-
- >>> tmpl = MarkupTemplate('''
- ...
Hello
- ...
''')
- >>> print tmpl.generate(bar='Bye')
-
-
Bye
-
- """
- __slots__ = []
-
- def __call__(self, stream, ctxt, directives):
- def _generate():
- yield stream.next()
- yield EXPR, self.expr, (None, -1, -1)
- event = stream.next()
- for next in stream:
- event = next
- yield event
-
- return _apply_directives(_generate(), ctxt, directives)
-
-
-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:
-
- >>> tmpl = MarkupTemplate('''
- ...
- ... ${greeting}, ${name}!
- ...
- ... ${echo('Hi', name='you')}
- ...
''')
- >>> print tmpl.generate(bar='Bye')
-
-
- Hi, you!
-
-
-
- If a function does not require parameters, the parenthesis can be omitted
- both when defining and when calling it:
-
- >>> tmpl = MarkupTemplate('''
- ...
- ... Hello, world!
- ...
- ... ${helloworld}
- ...
''')
- >>> print tmpl.generate(bar='Bye')
-
-
- Hello, world!
-
-
- """
- __slots__ = ['name', 'args', 'defaults']
-
- ATTRIBUTE = 'function'
-
- def __init__(self, args, namespaces=None, filename=None, lineno=-1,
- offset=-1):
- Directive.__init__(self, None, namespaces, filename, lineno, offset)
- ast = _parse(args).node
- self.args = []
- self.defaults = {}
- if isinstance(ast, compiler.ast.CallFunc):
- self.name = ast.node.name
- for arg in ast.args:
- if isinstance(arg, compiler.ast.Keyword):
- self.args.append(arg.name)
- self.defaults[arg.name] = Expression(arg.expr, filename,
- lineno)
- else:
- self.args.append(arg.name)
- else:
- self.name = ast.name
-
- def __call__(self, stream, ctxt, directives):
- 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 = self.defaults.get(name).evaluate(ctxt)
- scope[name] = val
- ctxt.push(scope)
- for event in _apply_directives(stream, ctxt, directives):
- yield event
- ctxt.pop()
- try:
- function.__name__ = self.name
- except TypeError:
- # Function name can't be set in Python 2.3
- pass
-
- # 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">' % (self.__class__.__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.
-
- >>> tmpl = MarkupTemplate('''
- ...
${item}
- ...
''')
- >>> print tmpl.generate(items=[1, 2, 3])
-
-
1
2
3
-
- """
- __slots__ = ['assign', 'filename']
-
- ATTRIBUTE = 'each'
-
- def __init__(self, value, namespaces=None, filename=None, lineno=-1,
- offset=-1):
- if ' in ' not in value:
- raise TemplateSyntaxError('"in" keyword missing in "for" directive',
- filename, lineno, offset)
- assign, value = value.split(' in ', 1)
- ast = _parse(assign, 'exec')
- self.assign = _assignment(ast.node.nodes[0].expr)
- self.filename = filename
- Directive.__init__(self, value.strip(), namespaces, filename, lineno,
- offset)
-
- def __call__(self, stream, ctxt, directives):
- iterable = self.expr.evaluate(ctxt)
- if iterable is None:
- return
-
- assign = self.assign
- scope = {}
- stream = list(stream)
- try:
- iterator = iter(iterable)
- for item in iterator:
- assign(scope, item)
- ctxt.push(scope)
- for event in _apply_directives(stream, ctxt, directives):
- yield event
- ctxt.pop()
- except TypeError, e:
- raise TemplateRuntimeError(str(e), self.filename, *stream[0][2][1:])
-
- def __repr__(self):
- return '<%s>' % self.__class__.__name__
-
-
-class IfDirective(Directive):
- """Implementation of the `py:if` template directive for conditionally
- excluding elements from being output.
-
- >>> tmpl = MarkupTemplate('''
- """
- __slots__ = ['path', 'namespaces']
-
- ATTRIBUTE = 'path'
-
- def __init__(self, value, namespaces=None, filename=None, lineno=-1,
- offset=-1):
- Directive.__init__(self, None, namespaces, filename, lineno, offset)
- self.path = Path(value, filename, lineno)
- if namespaces is None:
- namespaces = {}
- self.namespaces = namespaces.copy()
-
- def __call__(self, stream, ctxt, directives):
- ctxt._match_templates.append((self.path.test(ignore_context=True),
- self.path, list(stream), self.namespaces,
- directives))
- return []
-
- def __repr__(self):
- return '<%s "%s">' % (self.__class__.__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:
-
- >>> tmpl = MarkupTemplate('''
- ... Hello
- ...
''')
- >>> print tmpl.generate(bar='Bye')
-
- Bye
-
-
- This directive is equivalent to `py:content` combined with `py:strip`,
- providing a less verbose way to achieve the same effect:
-
- >>> tmpl = MarkupTemplate('''
- ... Hello
- ...
''')
- >>> print tmpl.generate(bar='Bye')
-
- Bye
-
- """
- __slots__ = []
-
- def __call__(self, stream, ctxt, directives):
- yield EXPR, self.expr, (None, -1, -1)
-
-
-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
-
- >>> tmpl = MarkupTemplate('''
- ...
foo
- ...
''')
- >>> print tmpl.generate()
-
- foo
-
-
- 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('''
- ...
- ... ${what}
- ...
- ... ${echo('foo')}
- ...
''')
- >>> print tmpl.generate()
-
- foo
-
- """
- __slots__ = []
-
- def __call__(self, stream, ctxt, directives):
- def _generate():
- if self.expr:
- strip = self.expr.evaluate(ctxt)
- else:
- strip = True
- if strip:
- 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(), ctxt, directives)
-
-
-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.
-
- >>> tmpl = MarkupTemplate('''
- ... 0
- ... 1
- ... 2
- ...
''')
- >>> print tmpl.generate()
-
- 1
-
-
- If the `py:choose` directive contains an expression, the nested `py:when`
- directives are tested for equality to the `py:choose` expression:
-
- >>> tmpl = MarkupTemplate('''
- ... 1
- ... 2
- ...
''')
- >>> print tmpl.generate()
-
- 2
-
-
- 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']
-
- ATTRIBUTE = 'test'
-
- def __call__(self, stream, ctxt, directives):
- frame = dict({'_choose.matched': False})
- if self.expr:
- frame['_choose.value'] = self.expr.evaluate(ctxt)
- ctxt.push(frame)
- for event in _apply_directives(stream, ctxt, directives):
- yield event
- ctxt.pop()
-
-
-class WhenDirective(Directive):
- """Implementation of the `py:when` directive for nesting in a parent with
- the `py:choose` directive.
-
- See the documentation of `py:choose` for usage.
- """
- __slots__ = ['filename']
-
- ATTRIBUTE = 'test'
-
- def __init__(self, value, namespaces=None, filename=None, lineno=-1,
- offset=-1):
- Directive.__init__(self, value, namespaces, filename, lineno, offset)
- self.filename = filename
-
- def __call__(self, stream, ctxt, directives):
- matched, frame = ctxt._find('_choose.matched')
- if not frame:
- raise TemplateRuntimeError('"when" directives can only be used '
- 'inside a "choose" directive',
- self.filename, *stream.next()[2][1:])
- if matched:
- return []
- if not self.expr and '_choose.value' not in frame:
- raise TemplateRuntimeError('either "choose" or "when" directive '
- 'must have a test expression',
- self.filename, *stream.next()[2][1:])
- if '_choose.value' in frame:
- value = frame['_choose.value']
- if self.expr:
- matched = value == self.expr.evaluate(ctxt)
- else:
- matched = bool(value)
- else:
- matched = bool(self.expr.evaluate(ctxt))
- frame['_choose.matched'] = matched
- if not matched:
- return []
-
- return _apply_directives(stream, ctxt, directives)
-
-
-class OtherwiseDirective(Directive):
- """Implementation of the `py:otherwise` directive for nesting in a parent
- with the `py:choose` directive.
-
- See the documentation of `py:choose` for usage.
- """
- __slots__ = ['filename']
-
- def __init__(self, value, namespaces=None, filename=None, lineno=-1,
- offset=-1):
- Directive.__init__(self, None, namespaces, filename, lineno, offset)
- self.filename = filename
-
- def __call__(self, stream, ctxt, directives):
- matched, frame = ctxt._find('_choose.matched')
- if not frame:
- raise TemplateRuntimeError('an "otherwise" directive can only be '
- 'used inside a "choose" directive',
- self.filename, *stream.next()[2][1:])
- if matched:
- return []
- frame['_choose.matched'] = True
-
- return _apply_directives(stream, ctxt, directives)
-
-
-class WithDirective(Directive):
- """Implementation of the `py:with` template directive, which allows
- shorthand access to variables and expressions.
-
- >>> tmpl = MarkupTemplate('''
- ... $x $y $z
- ...
''')
- >>> print tmpl.generate(x=42)
-
- 42 7 52
-
- """
- __slots__ = ['vars']
-
- ATTRIBUTE = 'vars'
-
- def __init__(self, value, namespaces=None, filename=None, lineno=-1,
- offset=-1):
- Directive.__init__(self, None, namespaces, filename, lineno, offset)
- self.vars = []
- value = value.strip()
- try:
- ast = _parse(value, 'exec').node
- for node in ast.nodes:
- if isinstance(node, compiler.ast.Discard):
- continue
- elif not isinstance(node, compiler.ast.Assign):
- raise TemplateSyntaxError('only assignment allowed in '
- 'value of the "with" directive',
- filename, lineno, offset)
- self.vars.append(([_assignment(n) for n in node.nodes],
- Expression(node.expr, filename, lineno)))
- except SyntaxError, err:
- err.msg += ' in expression "%s" of "%s" directive' % (value,
- self.tagname)
- raise TemplateSyntaxError(err, filename, lineno,
- offset + (err.offset or 0))
-
- def __call__(self, stream, ctxt, directives):
- frame = {}
- ctxt.push(frame)
- for targets, expr in self.vars:
- value = expr.evaluate(ctxt, nocall=True)
- for assign in targets:
- assign(frame, value)
- for event in _apply_directives(stream, ctxt, directives):
- yield event
- ctxt.pop()
-
- def __repr__(self):
- return '<%s>' % (self.__class__.__name__)
-
-
-class TemplateMeta(type):
- """Meta class for templates."""
-
- def __new__(cls, name, bases, d):
- if 'directives' in d:
- d['_dir_by_name'] = dict(d['directives'])
- d['_dir_order'] = [directive[1] for directive in d['directives']]
-
- return type.__new__(cls, name, bases, d)
-
-
-class Template(object):
- """Abstract template base class.
-
- This class implements most of the template processing model, but does not
- specify the syntax of templates.
- """
- __metaclass__ = TemplateMeta
-
- EXPR = StreamEventKind('EXPR') # an expression
- SUB = StreamEventKind('SUB') # a "subprogram"
-
- def __init__(self, source, basedir=None, filename=None, loader=None,
- encoding=None):
- """Initialize a template from either a string or a file-like object."""
- if isinstance(source, basestring):
- self.source = StringIO(source)
- else:
- self.source = source
- self.basedir = basedir
- self.filename = filename
- if basedir and filename:
- self.filepath = os.path.join(basedir, filename)
- else:
- self.filepath = filename
-
- self.filters = [self._flatten, self._eval]
-
- self.stream = self._parse(encoding)
-
- def __repr__(self):
- return '<%s "%s">' % (self.__class__.__name__, self.filename)
-
- def _parse(self, encoding):
- """Parse the template.
-
- The parsing stage parses the template and constructs a list of
- directives that will be executed in the render stage. The input is
- split up into literal output (text that does not depend on the context
- data) and directives or expressions.
- """
- raise NotImplementedError
-
- _FULL_EXPR_RE = re.compile(r'(?>> tmpl = MarkupTemplate('''
- ...
${item}
- ...
''')
- >>> print tmpl.generate(items=[1, 2, 3])
-
-
1
2
3
-
- """
- NAMESPACE = Namespace('http://genshi.edgewall.org/')
-
- directives = [('def', DefDirective),
- ('match', MatchDirective),
- ('when', WhenDirective),
- ('otherwise', OtherwiseDirective),
- ('for', ForDirective),
- ('if', IfDirective),
- ('choose', ChooseDirective),
- ('with', WithDirective),
- ('replace', ReplaceDirective),
- ('content', ContentDirective),
- ('attrs', AttrsDirective),
- ('strip', StripDirective)]
-
- def __init__(self, source, basedir=None, filename=None, loader=None,
- encoding=None):
- """Initialize a template from either a string or a file-like object."""
- Template.__init__(self, source, basedir=basedir, filename=filename,
- loader=loader, encoding=encoding)
-
- self.filters.append(self._match)
- if loader:
- from genshi.filters import IncludeFilter
- self.filters.append(IncludeFilter(loader))
-
- def _parse(self, encoding):
- """Parse the template from an XML document."""
- stream = [] # list of events of the "compiled" template
- dirmap = {} # temporary mapping of directives to elements
- ns_prefix = {}
- depth = 0
-
- for kind, data, pos in XMLParser(self.source, filename=self.filename,
- encoding=encoding):
-
- if kind is START_NS:
- # Strip out the namespace declaration for template directives
- prefix, uri = data
- ns_prefix[prefix] = uri
- if uri != self.NAMESPACE:
- stream.append((kind, data, pos))
-
- elif kind is END_NS:
- uri = ns_prefix.pop(data, None)
- if uri and uri != self.NAMESPACE:
- stream.append((kind, data, pos))
-
- elif kind is START:
- # Record any directive attributes in start tags
- tag, attrib = data
- directives = []
- strip = False
-
- if tag in self.NAMESPACE:
- cls = self._dir_by_name.get(tag.localname)
- if cls is None:
- raise BadDirectiveError(tag.localname, self.filepath,
- pos[1])
- value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '')
- directives.append(cls(value, ns_prefix, self.filepath,
- pos[1], pos[2]))
- strip = True
-
- new_attrib = []
- for name, value in attrib:
- if name in self.NAMESPACE:
- cls = self._dir_by_name.get(name.localname)
- if cls is None:
- raise BadDirectiveError(name.localname,
- self.filepath, pos[1])
- directives.append(cls(value, ns_prefix, self.filepath,
- pos[1], pos[2]))
- else:
- if value:
- value = list(self._interpolate(value, self.basedir,
- *pos))
- if len(value) == 1 and value[0][0] is TEXT:
- value = value[0][1]
- else:
- value = [(TEXT, u'', pos)]
- new_attrib.append((name, value))
-
- if directives:
- index = self._dir_order.index
- directives.sort(lambda a, b: cmp(index(a.__class__),
- index(b.__class__)))
- dirmap[(depth, tag)] = (directives, len(stream), strip)
-
- stream.append((kind, (tag, Attrs(new_attrib)), pos))
- depth += 1
-
- elif kind is END:
- depth -= 1
- stream.append((kind, data, pos))
-
- # If there have have directive attributes with the corresponding
- # start tag, move the events inbetween into a "subprogram"
- if (depth, data) in dirmap:
- directives, start_offset, strip = dirmap.pop((depth, data))
- substream = stream[start_offset:]
- if strip:
- substream = substream[1:-1]
- stream[start_offset:] = [(SUB, (directives, substream),
- pos)]
-
- elif kind is TEXT:
- for kind, data, pos in self._interpolate(data, self.basedir,
- *pos):
- stream.append((kind, data, pos))
-
- elif kind is COMMENT:
- if not data.lstrip().startswith('!'):
- stream.append((kind, data, pos))
-
- else:
- stream.append((kind, data, pos))
-
- return stream
-
- def _match(self, stream, ctxt, match_templates=None):
- """Internal stream filter that applies any defined match templates
- to the stream.
- """
- if match_templates is None:
- match_templates = ctxt._match_templates
-
- tail = []
- def _strip(stream):
- depth = 1
- while 1:
- event = stream.next()
- if event[0] is START:
- depth += 1
- elif event[0] is END:
- depth -= 1
- if depth > 0:
- yield event
- else:
- tail[:] = [event]
- break
-
- for event in stream:
-
- # We (currently) only care about start and end events for matching
- # We might care about namespace events in the future, though
- if not match_templates or (event[0] is not START and
- event[0] is not END):
- yield event
- continue
-
- for idx, (test, path, template, namespaces, directives) in \
- enumerate(match_templates):
-
- if test(event, namespaces, ctxt) is True:
-
- # Let the remaining match templates know about the event so
- # they get a chance to update their internal state
- for test in [mt[0] for mt in match_templates[idx + 1:]]:
- test(event, namespaces, ctxt, updateonly=True)
-
- # Consume and store all events until an end event
- # corresponding to this start event is encountered
- content = chain([event], self._match(_strip(stream), ctxt),
- tail)
- for filter_ in self.filters[3:]:
- content = filter_(content, ctxt)
- content = list(content)
-
- for test in [mt[0] for mt in match_templates]:
- 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))
-
- # Recursively process the output
- template = _apply_directives(template, ctxt, directives)
- for event in self._match(self._eval(self._flatten(template,
- ctxt),
- ctxt), ctxt,
- match_templates[:idx] +
- match_templates[idx + 1:]):
- yield event
-
- ctxt.pop()
- break
-
- else: # no matches
- yield event
-
-
-class TextTemplate(Template):
- """Implementation of a simple text-based template engine.
-
- >>> tmpl = TextTemplate('''Dear $name,
- ...
- ... We have the following items for you:
- ... #for item in items
- ... * $item
- ... #end
- ...
- ... All the best,
- ... Foobar''')
- >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text')
- Dear Joe,
-
- We have the following items for you:
- * 1
- * 2
- * 3
-
- All the best,
- Foobar
- """
- directives = [('def', DefDirective),
- ('when', WhenDirective),
- ('otherwise', OtherwiseDirective),
- ('for', ForDirective),
- ('if', IfDirective),
- ('choose', ChooseDirective),
- ('with', WithDirective)]
-
- _DIRECTIVE_RE = re.compile(r'^\s*(? offset:
- text = source[offset:start]
- for kind, data, pos in self._interpolate(text, self.basedir,
- self.filename, lineno):
- stream.append((kind, data, pos))
- lineno += len(text.splitlines())
-
- text = source[start:end].lstrip()[1:]
- lineno += len(text.splitlines())
- directive = text.split(None, 1)
- if len(directive) > 1:
- command, value = directive
- else:
- command, value = directive[0], None
-
- if command == 'end':
- depth -= 1
- if depth in dirmap:
- directive, start_offset = dirmap.pop(depth)
- substream = stream[start_offset:]
- stream[start_offset:] = [(SUB, ([directive], substream),
- (self.filepath, lineno, 0))]
- elif command != '#':
- cls = self._dir_by_name.get(command)
- if cls is None:
- raise BadDirectiveError(command)
- directive = cls(value, None, self.filepath, lineno, 0)
- dirmap[depth] = (directive, len(stream))
- depth += 1
-
- offset = end
-
- if offset < len(source):
- text = source[offset:].replace('\\#', '#')
- for kind, data, pos in self._interpolate(text, self.basedir,
- self.filename, lineno):
- stream.append((kind, data, pos))
-
- return stream
-
-
-class TemplateLoader(object):
- """Responsible for loading templates from files on the specified search
- path.
-
- >>> import tempfile
- >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
- >>> os.write(fd, '
$var
')
- 11
- >>> os.close(fd)
-
- The template loader accepts a list of directory paths that are then used
- when searching for template files, in the given order:
-
- >>> loader = TemplateLoader([os.path.dirname(path)])
-
- The `load()` method first checks the template cache whether the requested
- template has already been loaded. If not, it attempts to locate the
- template file, and returns the corresponding `Template` object:
-
- >>> template = loader.load(os.path.basename(path))
- >>> isinstance(template, MarkupTemplate)
- True
-
- Template instances are cached: requesting a template with the same name
- results in the same instance being returned:
-
- >>> loader.load(os.path.basename(path)) is template
- True
-
- >>> os.remove(path)
- """
- def __init__(self, search_path=None, auto_reload=False,
- default_encoding=None, max_cache_size=25):
- """Create the template laoder.
-
- @param search_path: a list of absolute path names that should be
- searched for template files, or a string containing a single
- absolute path
- @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
- templates; defaults to UTF-8
- @param max_cache_size: the maximum number of templates to keep in the
- cache
- """
- self.search_path = search_path
- if self.search_path is None:
- self.search_path = []
- elif isinstance(self.search_path, basestring):
- self.search_path = [self.search_path]
- self.auto_reload = auto_reload
- self.default_encoding = default_encoding
- self._cache = LRUCache(max_cache_size)
- self._mtime = {}
- self._lock = threading.Lock()
-
- def load(self, filename, relative_to=None, cls=MarkupTemplate,
- 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 not bypassed.
-
- If requested template is not found, a `TemplateNotFound` exception is
- raised. Otherwise, a `Template` object is returned that represents the
- parsed template.
-
- Template instances are cached to avoid having to parse the same
- template file more than once. Thus, subsequent calls of this method
- with the same template file name will return the same `Template`
- object (unless the `auto_reload` option is enabled and the file was
- changed since the last parse.)
-
- If the `relative_to` parameter is provided, the `filename` is
- interpreted as being relative to that path.
-
- @param filename: the relative path of the template file to load
- @param relative_to: the filename of the template from which the new
- template is being loaded, or `None` if the template is being loaded
- directly
- @param cls: the class of the template object to instantiate
- @param encoding: the encoding of the template to load; defaults to the
- `default_encoding` of the loader instance
- """
- 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)
-
- 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]:
- return tmpl
- except KeyError:
- pass
-
- search_path = self.search_path
- isabs = False
-
- if os.path.isabs(filename):
- # Bypass the search path if the requested filename is absolute
- search_path = [os.path.dirname(filename)]
- isabs = True
-
- elif relative_to and os.path.isabs(relative_to):
- # Make sure that the directory containing the including
- # template is on the search path
- dirname = os.path.dirname(relative_to)
- if dirname not in search_path:
- search_path = 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)
- try:
- fileobj = open(filepath, 'U')
- 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
- # search path
- filename = os.path.join(dirname, filename)
- dirname = ''
- tmpl = cls(fileobj, basedir=dirname, filename=filename,
- loader=self, encoding=encoding)
- finally:
- fileobj.close()
- self._cache[filename] = tmpl
- self._mtime[filename] = os.path.getmtime(filepath)
- return tmpl
- except IOError:
- continue
-
- raise TemplateNotFound(filename, search_path)
-
- finally:
- self._lock.release()
+from genshi.template.core import Context, Template, TemplateError, \
+ TemplateRuntimeError, TemplateSyntaxError, \
+ BadDirectiveError
+from genshi.template.loader import TemplateLoader, TemplateNotFound
+from genshi.template.markup import MarkupTemplate
+from genshi.template.text import TextTemplate
diff --git a/genshi/template/core.py b/genshi/template/core.py
new file mode 100644
--- /dev/null
+++ b/genshi/template/core.py
@@ -0,0 +1,381 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+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)
+import os
+import re
+from StringIO import StringIO
+
+from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
+from genshi.template.eval import Expression
+
+__all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError',
+ 'TemplateSyntaxError', 'BadDirectiveError']
+
+
+class TemplateError(Exception):
+ """Base exception class for errors related to template processing."""
+
+
+class TemplateRuntimeError(TemplateError):
+ """Exception raised when an the evualation of a Python expression in a
+ template causes an error."""
+
+ def __init__(self, message, filename='', lineno=-1, offset=-1):
+ self.msg = message
+ message = '%s (%s, line %d)' % (self.msg, filename, lineno)
+ TemplateError.__init__(self, message)
+ self.filename = filename
+ self.lineno = lineno
+ self.offset = offset
+
+
+class TemplateSyntaxError(TemplateError):
+ """Exception raised when an expression in a template causes a Python syntax
+ error."""
+
+ def __init__(self, message, filename='', lineno=-1, offset=-1):
+ if isinstance(message, SyntaxError) and message.lineno is not None:
+ message = str(message).replace(' (line %d)' % message.lineno, '')
+ self.msg = message
+ message = '%s (%s, line %d)' % (self.msg, filename, lineno)
+ TemplateError.__init__(self, message)
+ self.filename = filename
+ self.lineno = lineno
+ self.offset = offset
+
+
+class BadDirectiveError(TemplateSyntaxError):
+ """Exception raised when an unknown directive is encountered when parsing
+ a template.
+
+ An unknown directive is any attribute using the namespace for directives,
+ with a local name that doesn't match any registered directive.
+ """
+
+ def __init__(self, name, filename='', lineno=-1):
+ message = 'bad directive "%s"' % name
+ TemplateSyntaxError.__init__(self, message, filename, lineno)
+
+
+class Context(object):
+ """Container for template input data.
+
+ A context provides a stack of scopes (represented by dictionaries).
+
+ Template directives such as loops can push a new scope on the stack with
+ data that should only be available inside the loop. When the loop
+ terminates, that scope can get popped off the stack again.
+
+ >>> ctxt = Context(one='foo', other=1)
+ >>> ctxt.get('one')
+ 'foo'
+ >>> ctxt.get('other')
+ 1
+ >>> ctxt.push(dict(one='frost'))
+ >>> ctxt.get('one')
+ 'frost'
+ >>> ctxt.get('other')
+ 1
+ >>> ctxt.pop()
+ {'one': 'frost'}
+ >>> ctxt.get('one')
+ 'foo'
+ """
+
+ def __init__(self, **data):
+ self.frames = deque([data])
+ self.pop = self.frames.popleft
+ self.push = self.frames.appendleft
+ self._match_templates = []
+
+ def __repr__(self):
+ return repr(list(self.frames))
+
+ def __setitem__(self, key, value):
+ """Set a variable in the current scope."""
+ self.frames[0][key] = value
+
+ def _find(self, key, default=None):
+ """Retrieve a given variable's value and the frame it was found in.
+
+ Intented for internal use by directives.
+ """
+ for frame in self.frames:
+ if key in frame:
+ return frame[key], frame
+ return default, None
+
+ def get(self, key, default=None):
+ """Get a variable's value, starting at the current scope and going
+ upward.
+ """
+ for frame in self.frames:
+ if key in frame:
+ return frame[key]
+ return default
+ __getitem__ = get
+
+ def push(self, data):
+ """Push a new scope on the stack."""
+
+ def pop(self):
+ """Pop the top-most scope from the stack."""
+
+
+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).
+ """
+ __slots__ = ['expr']
+
+ def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+ offset=-1):
+ try:
+ self.expr = value and Expression(value, filename, lineno) or None
+ except SyntaxError, err:
+ err.msg += ' in expression "%s" of "%s" directive' % (value,
+ self.tagname)
+ raise TemplateSyntaxError(err, filename, lineno,
+ offset + (err.offset or 0))
+
+ def __call__(self, stream, ctxt, directives):
+ raise NotImplementedError
+
+ def __repr__(self):
+ expr = ''
+ if self.expr is not None:
+ expr = ' "%s"' % self.expr.source
+ return '<%s%s>' % (self.__class__.__name__, expr)
+
+ def tagname(self):
+ """Return the local tag name of the directive as it is used in
+ templates.
+ """
+ return self.__class__.__name__.lower().replace('directive', '')
+ tagname = property(tagname)
+
+
+def _apply_directives(stream, ctxt, directives):
+ """Apply the given directives to the stream."""
+ if directives:
+ stream = directives[0](iter(stream), ctxt, directives[1:])
+ return stream
+
+
+class TemplateMeta(type):
+ """Meta class for templates."""
+
+ def __new__(cls, name, bases, d):
+ if 'directives' in d:
+ d['_dir_by_name'] = dict(d['directives'])
+ d['_dir_order'] = [directive[1] for directive in d['directives']]
+
+ return type.__new__(cls, name, bases, d)
+
+
+class Template(object):
+ """Abstract template base class.
+
+ This class implements most of the template processing model, but does not
+ specify the syntax of templates.
+ """
+ __metaclass__ = TemplateMeta
+
+ EXPR = StreamEventKind('EXPR') # an expression
+ SUB = StreamEventKind('SUB') # a "subprogram"
+
+ def __init__(self, source, basedir=None, filename=None, loader=None,
+ encoding=None):
+ """Initialize a template from either a string or a file-like object."""
+ if isinstance(source, basestring):
+ self.source = StringIO(source)
+ else:
+ self.source = source
+ self.basedir = basedir
+ self.filename = filename
+ if basedir and filename:
+ self.filepath = os.path.join(basedir, filename)
+ else:
+ self.filepath = filename
+
+ self.filters = [self._flatten, self._eval]
+
+ self.stream = self._parse(encoding)
+
+ def __repr__(self):
+ return '<%s "%s">' % (self.__class__.__name__, self.filename)
+
+ def _parse(self, encoding):
+ """Parse the template.
+
+ The parsing stage parses the template and constructs a list of
+ directives that will be executed in the render stage. The input is
+ split up into literal output (text that does not depend on the context
+ data) and directives or expressions.
+ """
+ raise NotImplementedError
+
+ _FULL_EXPR_RE = re.compile(r'(?>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''
+
+ If the value evaluates to `None` (or any other non-truth value), no
+ attributes are added:
+
+ >>> print tmpl.generate(foo=None)
+
+
Bar
+
+ """
+ __slots__ = []
+
+ def __call__(self, stream, ctxt, directives):
+ def _generate():
+ kind, (tag, attrib), pos = stream.next()
+ attrs = self.expr.evaluate(ctxt)
+ if attrs:
+ attrib = Attrs(attrib[:])
+ 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()
+ for name, value in attrs:
+ if value is None:
+ attrib.remove(name)
+ else:
+ attrib.set(name, unicode(value).strip())
+ yield kind, (tag, attrib), pos
+ for event in stream:
+ yield event
+
+ return _apply_directives(_generate(), ctxt, directives)
+
+
+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('''
+ ...
Hello
+ ...
''')
+ >>> print tmpl.generate(bar='Bye')
+
+
Bye
+
+ """
+ __slots__ = []
+
+ def __call__(self, stream, ctxt, directives):
+ def _generate():
+ yield stream.next()
+ yield EXPR, self.expr, (None, -1, -1)
+ event = stream.next()
+ for next in stream:
+ event = next
+ yield event
+
+ return _apply_directives(_generate(), ctxt, directives)
+
+
+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('''
+ ...
+ ... ${greeting}, ${name}!
+ ...
+ ... ${echo('Hi', name='you')}
+ ...
''')
+ >>> print tmpl.generate(bar='Bye')
+
+
+ Hi, you!
+
+
+
+ If a function does not require parameters, the parenthesis can be omitted
+ both when defining and when calling it:
+
+ >>> tmpl = MarkupTemplate('''
+ ...
+ ... Hello, world!
+ ...
+ ... ${helloworld}
+ ...
''')
+ >>> print tmpl.generate(bar='Bye')
+
+
+ Hello, world!
+
+
+ """
+ __slots__ = ['name', 'args', 'defaults']
+
+ ATTRIBUTE = 'function'
+
+ def __init__(self, args, namespaces=None, filename=None, lineno=-1,
+ offset=-1):
+ Directive.__init__(self, None, namespaces, filename, lineno, offset)
+ ast = _parse(args).node
+ self.args = []
+ self.defaults = {}
+ if isinstance(ast, compiler.ast.CallFunc):
+ self.name = ast.node.name
+ for arg in ast.args:
+ if isinstance(arg, compiler.ast.Keyword):
+ self.args.append(arg.name)
+ self.defaults[arg.name] = Expression(arg.expr, filename,
+ lineno)
+ else:
+ self.args.append(arg.name)
+ else:
+ self.name = ast.name
+
+ def __call__(self, stream, ctxt, directives):
+ 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 = self.defaults.get(name).evaluate(ctxt)
+ scope[name] = val
+ ctxt.push(scope)
+ for event in _apply_directives(stream, ctxt, directives):
+ yield event
+ ctxt.pop()
+ try:
+ function.__name__ = self.name
+ except TypeError:
+ # Function name can't be set in Python 2.3
+ pass
+
+ # 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">' % (self.__class__.__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('''
+ ...
${item}
+ ...
''')
+ >>> print tmpl.generate(items=[1, 2, 3])
+
+
1
2
3
+
+ """
+ __slots__ = ['assign', 'filename']
+
+ ATTRIBUTE = 'each'
+
+ def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+ offset=-1):
+ if ' in ' not in value:
+ raise TemplateSyntaxError('"in" keyword missing in "for" directive',
+ filename, lineno, offset)
+ assign, value = value.split(' in ', 1)
+ ast = _parse(assign, 'exec')
+ self.assign = _assignment(ast.node.nodes[0].expr)
+ self.filename = filename
+ Directive.__init__(self, value.strip(), namespaces, filename, lineno,
+ offset)
+
+ def __call__(self, stream, ctxt, directives):
+ iterable = self.expr.evaluate(ctxt)
+ if iterable is None:
+ return
+
+ assign = self.assign
+ scope = {}
+ stream = list(stream)
+ try:
+ iterator = iter(iterable)
+ for item in iterator:
+ assign(scope, item)
+ ctxt.push(scope)
+ for event in _apply_directives(stream, ctxt, directives):
+ yield event
+ ctxt.pop()
+ except TypeError, e:
+ raise TemplateRuntimeError(str(e), self.filename, *stream[0][2][1:])
+
+ def __repr__(self):
+ return '<%s>' % self.__class__.__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('''
+ """
+ __slots__ = ['path', 'namespaces']
+
+ ATTRIBUTE = 'path'
+
+ def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+ offset=-1):
+ Directive.__init__(self, None, namespaces, filename, lineno, offset)
+ self.path = Path(value, filename, lineno)
+ if namespaces is None:
+ namespaces = {}
+ self.namespaces = namespaces.copy()
+
+ def __call__(self, stream, ctxt, directives):
+ ctxt._match_templates.append((self.path.test(ignore_context=True),
+ self.path, list(stream), self.namespaces,
+ directives))
+ return []
+
+ def __repr__(self):
+ return '<%s "%s">' % (self.__class__.__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('''
+ ... Hello
+ ...
''')
+ >>> print tmpl.generate(bar='Bye')
+
+ Bye
+
+
+ This directive is equivalent to `py:content` combined with `py:strip`,
+ providing a less verbose way to achieve the same effect:
+
+ >>> tmpl = MarkupTemplate('''
+ ... Hello
+ ...
''')
+ >>> print tmpl.generate(bar='Bye')
+
+ Bye
+
+ """
+ __slots__ = []
+
+ def __call__(self, stream, ctxt, directives):
+ yield EXPR, self.expr, (None, -1, -1)
+
+
+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('''
+ ...
foo
+ ...
''')
+ >>> print tmpl.generate()
+
+ foo
+
+
+ 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('''
+ ...
+ ... ${what}
+ ...
+ ... ${echo('foo')}
+ ...
''')
+ >>> print tmpl.generate()
+
+ foo
+
+ """
+ __slots__ = []
+
+ def __call__(self, stream, ctxt, directives):
+ def _generate():
+ if self.expr:
+ strip = self.expr.evaluate(ctxt)
+ else:
+ strip = True
+ if strip:
+ 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(), ctxt, directives)
+
+
+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('''
+ ... 0
+ ... 1
+ ... 2
+ ...
''')
+ >>> print tmpl.generate()
+
+ 1
+
+
+ If the `py:choose` directive contains an expression, the nested `py:when`
+ directives are tested for equality to the `py:choose` expression:
+
+ >>> tmpl = MarkupTemplate('''
+ ... 1
+ ... 2
+ ...
''')
+ >>> print tmpl.generate()
+
+ 2
+
+
+ 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']
+
+ ATTRIBUTE = 'test'
+
+ def __call__(self, stream, ctxt, directives):
+ frame = dict({'_choose.matched': False})
+ if self.expr:
+ frame['_choose.value'] = self.expr.evaluate(ctxt)
+ ctxt.push(frame)
+ for event in _apply_directives(stream, ctxt, directives):
+ yield event
+ ctxt.pop()
+
+
+class WhenDirective(Directive):
+ """Implementation of the `py:when` directive for nesting in a parent with
+ the `py:choose` directive.
+
+ See the documentation of `py:choose` for usage.
+ """
+ __slots__ = ['filename']
+
+ ATTRIBUTE = 'test'
+
+ def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+ offset=-1):
+ Directive.__init__(self, value, namespaces, filename, lineno, offset)
+ self.filename = filename
+
+ def __call__(self, stream, ctxt, directives):
+ matched, frame = ctxt._find('_choose.matched')
+ if not frame:
+ raise TemplateRuntimeError('"when" directives can only be used '
+ 'inside a "choose" directive',
+ self.filename, *stream.next()[2][1:])
+ if matched:
+ return []
+ if not self.expr and '_choose.value' not in frame:
+ raise TemplateRuntimeError('either "choose" or "when" directive '
+ 'must have a test expression',
+ self.filename, *stream.next()[2][1:])
+ if '_choose.value' in frame:
+ value = frame['_choose.value']
+ if self.expr:
+ matched = value == self.expr.evaluate(ctxt)
+ else:
+ matched = bool(value)
+ else:
+ matched = bool(self.expr.evaluate(ctxt))
+ frame['_choose.matched'] = matched
+ if not matched:
+ return []
+
+ return _apply_directives(stream, ctxt, directives)
+
+
+class OtherwiseDirective(Directive):
+ """Implementation of the `py:otherwise` directive for nesting in a parent
+ with the `py:choose` directive.
+
+ See the documentation of `py:choose` for usage.
+ """
+ __slots__ = ['filename']
+
+ def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+ offset=-1):
+ Directive.__init__(self, None, namespaces, filename, lineno, offset)
+ self.filename = filename
+
+ def __call__(self, stream, ctxt, directives):
+ matched, frame = ctxt._find('_choose.matched')
+ if not frame:
+ raise TemplateRuntimeError('an "otherwise" directive can only be '
+ 'used inside a "choose" directive',
+ self.filename, *stream.next()[2][1:])
+ if matched:
+ return []
+ frame['_choose.matched'] = True
+
+ return _apply_directives(stream, ctxt, directives)
+
+
+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('''
+ ... $x $y $z
+ ...
''')
+ >>> print tmpl.generate(x=42)
+
+ 42 7 52
+
+ """
+ __slots__ = ['vars']
+
+ ATTRIBUTE = 'vars'
+
+ def __init__(self, value, namespaces=None, filename=None, lineno=-1,
+ offset=-1):
+ Directive.__init__(self, None, namespaces, filename, lineno, offset)
+ self.vars = []
+ value = value.strip()
+ try:
+ ast = _parse(value, 'exec').node
+ for node in ast.nodes:
+ if isinstance(node, compiler.ast.Discard):
+ continue
+ elif not isinstance(node, compiler.ast.Assign):
+ raise TemplateSyntaxError('only assignment allowed in '
+ 'value of the "with" directive',
+ filename, lineno, offset)
+ self.vars.append(([_assignment(n) for n in node.nodes],
+ Expression(node.expr, filename, lineno)))
+ except SyntaxError, err:
+ err.msg += ' in expression "%s" of "%s" directive' % (value,
+ self.tagname)
+ raise TemplateSyntaxError(err, filename, lineno,
+ offset + (err.offset or 0))
+
+ def __call__(self, stream, ctxt, directives):
+ frame = {}
+ ctxt.push(frame)
+ for targets, expr in self.vars:
+ value = expr.evaluate(ctxt, nocall=True)
+ for assign in targets:
+ assign(frame, value)
+ for event in _apply_directives(stream, ctxt, directives):
+ yield event
+ ctxt.pop()
+
+ def __repr__(self):
+ return '<%s>' % (self.__class__.__name__)
diff --git a/genshi/template/eval.py b/genshi/template/eval.py
new file mode 100644
--- /dev/null
+++ b/genshi/template/eval.py
@@ -0,0 +1,427 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+"""Support for "safe" evaluation of Python expressions."""
+
+import __builtin__
+from compiler import ast, parse
+from compiler.pycodegen import ExpressionCodeGenerator
+import new
+
+__all__ = ['Expression', 'Undefined']
+
+
+class Expression(object):
+ """Evaluates Python expressions used in templates.
+
+ >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
+ >>> Expression('test').evaluate(data)
+ 'Foo'
+
+ >>> Expression('items[0]').evaluate(data)
+ 1
+ >>> Expression('items[-1]').evaluate(data)
+ 3
+ >>> Expression('dict["some"]').evaluate(data)
+ 'thing'
+
+ Similar to e.g. Javascript, expressions in templates can use the dot
+ notation for attribute access to access items in mappings:
+
+ >>> Expression('dict.some').evaluate(data)
+ 'thing'
+
+ This also works the other way around: item access can be used to access
+ any object attribute (meaning there's no use for `getattr()` in templates):
+
+ >>> class MyClass(object):
+ ... myattr = 'Bar'
+ >>> data = dict(mine=MyClass(), key='myattr')
+ >>> Expression('mine.myattr').evaluate(data)
+ 'Bar'
+ >>> Expression('mine["myattr"]').evaluate(data)
+ 'Bar'
+ >>> Expression('mine[key]').evaluate(data)
+ 'Bar'
+
+ All of the standard Python operators are available to template expressions.
+ Built-in functions such as `len()` are also available in template
+ expressions:
+
+ >>> data = dict(items=[1, 2, 3])
+ >>> Expression('len(items)').evaluate(data)
+ 3
+ """
+ __slots__ = ['source', 'code']
+
+ def __init__(self, source, filename=None, lineno=-1):
+ """Create the expression, either from a string, or from an AST node.
+
+ @param source: either a string containing the source code of the
+ expression, or an AST node
+ @param filename: the (preferably absolute) name of the file containing
+ the expression
+ @param lineno: the number of the line on which the expression was found
+ """
+ if isinstance(source, basestring):
+ self.source = source
+ self.code = _compile(_parse(source), self.source, filename=filename,
+ lineno=lineno)
+ else:
+ assert isinstance(source, ast.Node)
+ self.source = '?'
+ self.code = _compile(ast.Expression(source), filename=filename,
+ lineno=lineno)
+
+ def __repr__(self):
+ return 'Expression(%r)' % self.source
+
+ def evaluate(self, data, nocall=False):
+ """Evaluate the expression against the given data dictionary.
+
+ @param data: a mapping containing the data to evaluate against
+ @param nocall: if true, the result of the evaluation is not called if
+ if it is a callable
+ @return: the result of the evaluation
+ """
+ retval = eval(self.code, {'data': data,
+ '_lookup_name': _lookup_name,
+ '_lookup_attr': _lookup_attr,
+ '_lookup_item': _lookup_item},
+ {'data': data})
+ if not nocall and type(retval) is not Undefined and callable(retval):
+ retval = retval()
+ return retval
+
+
+class Undefined(object):
+ """Represents a reference to an undefined variable.
+
+ Unlike the Python runtime, template expressions can refer to an undefined
+ variable without causing a `NameError` to be raised. The result will be an
+ instance of the `Undefined´ class, which is treated the same as `False` in
+ conditions, and acts as an empty collection in iterations:
+
+ >>> foo = Undefined('foo')
+ >>> bool(foo)
+ False
+ >>> list(foo)
+ []
+ >>> print foo
+ undefined
+
+ However, calling an undefined variable, or trying to access an attribute
+ of that variable, will raise an exception that includes the name used to
+ reference that undefined variable.
+
+ >>> foo('bar')
+ Traceback (most recent call last):
+ ...
+ NameError: Variable "foo" is not defined
+
+ >>> foo.bar
+ Traceback (most recent call last):
+ ...
+ NameError: Variable "foo" is not defined
+ """
+ __slots__ = ['_name']
+
+ def __init__(self, name):
+ self._name = name
+
+ def __call__(self, *args, **kwargs):
+ __traceback_hide__ = True
+ self.throw()
+
+ def __getattr__(self, name):
+ __traceback_hide__ = True
+ self.throw()
+
+ def __iter__(self):
+ return iter([])
+
+ def __nonzero__(self):
+ return False
+
+ def __repr__(self):
+ return 'undefined'
+
+ def throw(self):
+ __traceback_hide__ = True
+ raise NameError('Variable "%s" is not defined' % self._name)
+
+
+def _parse(source, mode='eval'):
+ if isinstance(source, unicode):
+ source = '\xef\xbb\xbf' + source.encode('utf-8')
+ return parse(source, mode)
+
+def _compile(node, source=None, filename=None, lineno=-1):
+ tree = ExpressionASTTransformer().visit(node)
+ if isinstance(filename, unicode):
+ # unicode file names not allowed for code objects
+ filename = filename.encode('utf-8', 'replace')
+ elif not filename:
+ filename = ''
+ tree.filename = filename
+ if lineno <= 0:
+ lineno = 1
+
+ gen = ExpressionCodeGenerator(tree)
+ gen.optimized = True
+ code = gen.getCode()
+
+ # We'd like to just set co_firstlineno, but it's readonly. So we need to
+ # clone the code object while adjusting the line number
+ return new.code(0, code.co_nlocals, code.co_stacksize,
+ code.co_flags | 0x0040, code.co_code, code.co_consts,
+ code.co_names, code.co_varnames, filename,
+ '' % (repr(source or '?').replace("'", '"')),
+ lineno, code.co_lnotab, (), ())
+
+BUILTINS = __builtin__.__dict__.copy()
+BUILTINS['Undefined'] = Undefined
+
+def _lookup_name(data, name, locals_=None):
+ __traceback_hide__ = True
+ val = Undefined
+ if locals_:
+ val = locals_.get(name, val)
+ if val is Undefined:
+ val = data.get(name, val)
+ if val is Undefined:
+ val = BUILTINS.get(name, val)
+ if val is not Undefined or name == 'Undefined':
+ return val
+ else:
+ return val
+ else:
+ return val
+ return val(name)
+
+def _lookup_attr(data, obj, key):
+ __traceback_hide__ = True
+ if type(obj) is Undefined:
+ obj.throw()
+ if hasattr(obj, key):
+ return getattr(obj, key)
+ try:
+ return obj[key]
+ except (KeyError, TypeError):
+ return Undefined(key)
+
+def _lookup_item(data, obj, key):
+ __traceback_hide__ = True
+ if type(obj) is Undefined:
+ obj.throw()
+ if len(key) == 1:
+ key = key[0]
+ try:
+ return obj[key]
+ except (KeyError, IndexError, TypeError), e:
+ if isinstance(key, basestring):
+ val = getattr(obj, key, Undefined)
+ if val is Undefined:
+ val = Undefined(key)
+ return val
+ raise
+
+
+class ASTTransformer(object):
+ """General purpose base class for AST transformations.
+
+ Every visitor method can be overridden to return an AST node that has been
+ altered or replaced in some way.
+ """
+ _visitors = {}
+
+ def visit(self, node, *args, **kwargs):
+ v = self._visitors.get(node.__class__)
+ if not v:
+ v = getattr(self, 'visit%s' % node.__class__.__name__)
+ self._visitors[node.__class__] = v
+ return v(node, *args, **kwargs)
+
+ def visitExpression(self, node, *args, **kwargs):
+ node.node = self.visit(node.node, *args, **kwargs)
+ return node
+
+ # Functions & Accessors
+
+ def visitCallFunc(self, node, *args, **kwargs):
+ node.node = self.visit(node.node, *args, **kwargs)
+ node.args = [self.visit(x, *args, **kwargs) for x in node.args]
+ if node.star_args:
+ node.star_args = self.visit(node.star_args, *args, **kwargs)
+ if node.dstar_args:
+ node.dstar_args = self.visit(node.dstar_args, *args, **kwargs)
+ return node
+
+ def visitLambda(self, node, *args, **kwargs):
+ node.code = self.visit(node.code, *args, **kwargs)
+ node.filename = '' # workaround for bug in pycodegen
+ return node
+
+ def visitGetattr(self, node, *args, **kwargs):
+ node.expr = self.visit(node.expr, *args, **kwargs)
+ return node
+
+ def visitSubscript(self, node, *args, **kwargs):
+ node.expr = self.visit(node.expr, *args, **kwargs)
+ node.subs = [self.visit(x, *args, **kwargs) for x in node.subs]
+ return node
+
+ # Operators
+
+ def _visitBoolOp(self, node, *args, **kwargs):
+ node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
+ return node
+ visitAnd = visitOr = visitBitand = visitBitor = _visitBoolOp
+
+ def _visitBinOp(self, node, *args, **kwargs):
+ node.left = self.visit(node.left, *args, **kwargs)
+ node.right = self.visit(node.right, *args, **kwargs)
+ return node
+ visitAdd = visitSub = _visitBinOp
+ visitDiv = visitFloorDiv = visitMod = visitMul = visitPower = _visitBinOp
+ visitLeftShift = visitRightShift = _visitBinOp
+
+ def visitCompare(self, node, *args, **kwargs):
+ node.expr = self.visit(node.expr, *args, **kwargs)
+ node.ops = [(op, self.visit(n, *args, **kwargs)) for op, n in node.ops]
+ return node
+
+ def _visitUnaryOp(self, node, *args, **kwargs):
+ node.expr = self.visit(node.expr, *args, **kwargs)
+ return node
+ visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp
+ visitBackquote = _visitUnaryOp
+
+ # Identifiers, Literals and Comprehensions
+
+ def _visitDefault(self, node, *args, **kwargs):
+ return node
+ visitAssName = visitAssTuple = _visitDefault
+ visitConst = visitName = _visitDefault
+
+ def visitDict(self, node, *args, **kwargs):
+ node.items = [(self.visit(k, *args, **kwargs),
+ self.visit(v, *args, **kwargs)) for k, v in node.items]
+ return node
+
+ def visitGenExpr(self, node, *args, **kwargs):
+ node.code = self.visit(node.code, *args, **kwargs)
+ node.filename = '' # workaround for bug in pycodegen
+ return node
+
+ def visitGenExprFor(self, node, *args, **kwargs):
+ node.assign = self.visit(node.assign, *args, **kwargs)
+ node.iter = self.visit(node.iter, *args, **kwargs)
+ node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
+ return node
+
+ def visitGenExprIf(self, node, *args, **kwargs):
+ node.test = self.visit(node.test, *args, **kwargs)
+ return node
+
+ def visitGenExprInner(self, node, *args, **kwargs):
+ node.expr = self.visit(node.expr, *args, **kwargs)
+ node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
+ return node
+
+ def visitKeyword(self, node, *args, **kwargs):
+ node.expr = self.visit(node.expr, *args, **kwargs)
+ return node
+
+ def visitList(self, node, *args, **kwargs):
+ node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
+ return node
+
+ def visitListComp(self, node, *args, **kwargs):
+ node.expr = self.visit(node.expr, *args, **kwargs)
+ node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
+ return node
+
+ def visitListCompFor(self, node, *args, **kwargs):
+ node.assign = self.visit(node.assign, *args, **kwargs)
+ node.list = self.visit(node.list, *args, **kwargs)
+ node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
+ return node
+
+ def visitListCompIf(self, node, *args, **kwargs):
+ node.test = self.visit(node.test, *args, **kwargs)
+ return node
+
+ def visitSlice(self, node, *args, **kwargs):
+ node.expr = self.visit(node.expr, locals_=True, *args, **kwargs)
+ if node.lower is not None:
+ node.lower = self.visit(node.lower, *args, **kwargs)
+ if node.upper is not None:
+ node.upper = self.visit(node.upper, *args, **kwargs)
+ return node
+
+ def visitSliceobj(self, node, *args, **kwargs):
+ node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
+ return node
+
+ def visitTuple(self, node, *args, **kwargs):
+ node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
+ return node
+
+
+class ExpressionASTTransformer(ASTTransformer):
+ """Concrete AST transformer that implements the AST transformations needed
+ for template expressions.
+ """
+
+ def visitConst(self, node, locals_=False):
+ if isinstance(node.value, str):
+ return ast.Const(node.value.decode('utf-8'))
+ return node
+
+ def visitGenExprIf(self, node, *args, **kwargs):
+ node.test = self.visit(node.test, locals_=True)
+ return node
+
+ def visitGenExprInner(self, node, *args, **kwargs):
+ node.expr = self.visit(node.expr, locals_=True)
+ node.quals = [self.visit(x) for x in node.quals]
+ return node
+
+ def visitGetattr(self, node, locals_=False):
+ return ast.CallFunc(ast.Name('_lookup_attr'), [
+ ast.Name('data'), self.visit(node.expr, locals_=locals_),
+ ast.Const(node.attrname)
+ ])
+
+ def visitLambda(self, node, locals_=False):
+ node.code = self.visit(node.code, locals_=True)
+ node.filename = '' # workaround for bug in pycodegen
+ return node
+
+ def visitListComp(self, node, locals_=False):
+ node.expr = self.visit(node.expr, locals_=True)
+ node.quals = [self.visit(qual, locals_=True) for qual in node.quals]
+ return node
+
+ def visitName(self, node, locals_=False):
+ func_args = [ast.Name('data'), ast.Const(node.name)]
+ if locals_:
+ func_args.append(ast.CallFunc(ast.Name('locals'), []))
+ return ast.CallFunc(ast.Name('_lookup_name'), func_args)
+
+ def visitSubscript(self, node, locals_=False):
+ return ast.CallFunc(ast.Name('_lookup_item'), [
+ ast.Name('data'), self.visit(node.expr, locals_=locals_),
+ ast.Tuple([self.visit(sub, locals_=locals_) for sub in node.subs])
+ ])
diff --git a/genshi/template/loader.py b/genshi/template/loader.py
new file mode 100644
--- /dev/null
+++ b/genshi/template/loader.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+"""Template loading and caching."""
+
+import os
+try:
+ import threading
+except ImportError:
+ import dummy_threading as threading
+
+from genshi.template.core import TemplateError
+from genshi.template.markup import MarkupTemplate
+from genshi.util import LRUCache
+
+__all__ = ['TemplateLoader', 'TemplateNotFound']
+
+
+class TemplateNotFound(TemplateError):
+ """Exception raised when a specific template file could not be found."""
+
+ def __init__(self, name, search_path):
+ TemplateError.__init__(self, 'Template "%s" not found' % name)
+ self.search_path = search_path
+
+
+class TemplateLoader(object):
+ """Responsible for loading templates from files on the specified search
+ path.
+
+ >>> import tempfile
+ >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
+ >>> os.write(fd, '
$var
')
+ 11
+ >>> os.close(fd)
+
+ The template loader accepts a list of directory paths that are then used
+ when searching for template files, in the given order:
+
+ >>> loader = TemplateLoader([os.path.dirname(path)])
+
+ The `load()` method first checks the template cache whether the requested
+ template has already been loaded. If not, it attempts to locate the
+ template file, and returns the corresponding `Template` object:
+
+ >>> template = loader.load(os.path.basename(path))
+ >>> isinstance(template, MarkupTemplate)
+ True
+
+ Template instances are cached: requesting a template with the same name
+ results in the same instance being returned:
+
+ >>> loader.load(os.path.basename(path)) is template
+ True
+
+ >>> os.remove(path)
+ """
+ def __init__(self, search_path=None, auto_reload=False,
+ default_encoding=None, max_cache_size=25):
+ """Create the template laoder.
+
+ @param search_path: a list of absolute path names that should be
+ searched for template files, or a string containing a single
+ absolute path
+ @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
+ templates; defaults to UTF-8
+ @param max_cache_size: the maximum number of templates to keep in the
+ cache
+ """
+ self.search_path = search_path
+ if self.search_path is None:
+ self.search_path = []
+ elif isinstance(self.search_path, basestring):
+ self.search_path = [self.search_path]
+ self.auto_reload = auto_reload
+ self.default_encoding = default_encoding
+ self._cache = LRUCache(max_cache_size)
+ self._mtime = {}
+ self._lock = threading.Lock()
+
+ def load(self, filename, relative_to=None, cls=MarkupTemplate,
+ 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 not bypassed.
+
+ If requested template is not found, a `TemplateNotFound` exception is
+ raised. Otherwise, a `Template` object is returned that represents the
+ parsed template.
+
+ Template instances are cached to avoid having to parse the same
+ template file more than once. Thus, subsequent calls of this method
+ with the same template file name will return the same `Template`
+ object (unless the `auto_reload` option is enabled and the file was
+ changed since the last parse.)
+
+ If the `relative_to` parameter is provided, the `filename` is
+ interpreted as being relative to that path.
+
+ @param filename: the relative path of the template file to load
+ @param relative_to: the filename of the template from which the new
+ template is being loaded, or `None` if the template is being loaded
+ directly
+ @param cls: the class of the template object to instantiate
+ @param encoding: the encoding of the template to load; defaults to the
+ `default_encoding` of the loader instance
+ """
+ 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)
+
+ 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]:
+ return tmpl
+ except KeyError:
+ pass
+
+ search_path = self.search_path
+ isabs = False
+
+ if os.path.isabs(filename):
+ # Bypass the search path if the requested filename is absolute
+ search_path = [os.path.dirname(filename)]
+ isabs = True
+
+ elif relative_to and os.path.isabs(relative_to):
+ # Make sure that the directory containing the including
+ # template is on the search path
+ dirname = os.path.dirname(relative_to)
+ if dirname not in search_path:
+ search_path = 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)
+ try:
+ fileobj = open(filepath, 'U')
+ 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
+ # search path
+ filename = os.path.join(dirname, filename)
+ dirname = ''
+ tmpl = cls(fileobj, basedir=dirname, filename=filename,
+ loader=self, encoding=encoding)
+ finally:
+ fileobj.close()
+ self._cache[filename] = tmpl
+ self._mtime[filename] = os.path.getmtime(filepath)
+ return tmpl
+ except IOError:
+ continue
+
+ raise TemplateNotFound(filename, search_path)
+
+ finally:
+ self._lock.release()
diff --git a/genshi/template/markup.py b/genshi/template/markup.py
new file mode 100644
--- /dev/null
+++ b/genshi/template/markup.py
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+"""Markup templating engine."""
+
+from itertools import chain
+
+from genshi.core import Attrs, Namespace, Stream
+from genshi.core import START, END, START_NS, END_NS, TEXT, COMMENT
+from genshi.filters import IncludeFilter
+from genshi.input import XMLParser
+from genshi.template.core import BadDirectiveError, Template, _apply_directives
+from genshi.template.core import SUB
+from genshi.template.directives import *
+
+
+class MarkupTemplate(Template):
+ """Implementation of the template language for XML-based templates.
+
+ >>> tmpl = MarkupTemplate('''
+ ...
${item}
+ ...
''')
+ >>> print tmpl.generate(items=[1, 2, 3])
+
+
1
2
3
+
+ """
+ NAMESPACE = Namespace('http://genshi.edgewall.org/')
+
+ directives = [('def', DefDirective),
+ ('match', MatchDirective),
+ ('when', WhenDirective),
+ ('otherwise', OtherwiseDirective),
+ ('for', ForDirective),
+ ('if', IfDirective),
+ ('choose', ChooseDirective),
+ ('with', WithDirective),
+ ('replace', ReplaceDirective),
+ ('content', ContentDirective),
+ ('attrs', AttrsDirective),
+ ('strip', StripDirective)]
+
+ def __init__(self, source, basedir=None, filename=None, loader=None,
+ encoding=None):
+ """Initialize a template from either a string or a file-like object."""
+ Template.__init__(self, source, basedir=basedir, filename=filename,
+ loader=loader, encoding=encoding)
+
+ self.filters.append(self._match)
+ if loader:
+ self.filters.append(IncludeFilter(loader))
+
+ def _parse(self, encoding):
+ """Parse the template from an XML document."""
+ stream = [] # list of events of the "compiled" template
+ dirmap = {} # temporary mapping of directives to elements
+ ns_prefix = {}
+ depth = 0
+
+ for kind, data, pos in XMLParser(self.source, filename=self.filename,
+ encoding=encoding):
+
+ if kind is START_NS:
+ # Strip out the namespace declaration for template directives
+ prefix, uri = data
+ ns_prefix[prefix] = uri
+ if uri != self.NAMESPACE:
+ stream.append((kind, data, pos))
+
+ elif kind is END_NS:
+ uri = ns_prefix.pop(data, None)
+ if uri and uri != self.NAMESPACE:
+ stream.append((kind, data, pos))
+
+ elif kind is START:
+ # Record any directive attributes in start tags
+ tag, attrib = data
+ directives = []
+ strip = False
+
+ if tag in self.NAMESPACE:
+ cls = self._dir_by_name.get(tag.localname)
+ if cls is None:
+ raise BadDirectiveError(tag.localname, self.filepath,
+ pos[1])
+ value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '')
+ directives.append(cls(value, ns_prefix, self.filepath,
+ pos[1], pos[2]))
+ strip = True
+
+ new_attrib = []
+ for name, value in attrib:
+ if name in self.NAMESPACE:
+ cls = self._dir_by_name.get(name.localname)
+ if cls is None:
+ raise BadDirectiveError(name.localname,
+ self.filepath, pos[1])
+ directives.append(cls(value, ns_prefix, self.filepath,
+ pos[1], pos[2]))
+ else:
+ if value:
+ value = list(self._interpolate(value, self.basedir,
+ *pos))
+ if len(value) == 1 and value[0][0] is TEXT:
+ value = value[0][1]
+ else:
+ value = [(TEXT, u'', pos)]
+ new_attrib.append((name, value))
+
+ if directives:
+ index = self._dir_order.index
+ directives.sort(lambda a, b: cmp(index(a.__class__),
+ index(b.__class__)))
+ dirmap[(depth, tag)] = (directives, len(stream), strip)
+
+ stream.append((kind, (tag, Attrs(new_attrib)), pos))
+ depth += 1
+
+ elif kind is END:
+ depth -= 1
+ stream.append((kind, data, pos))
+
+ # If there have have directive attributes with the corresponding
+ # start tag, move the events inbetween into a "subprogram"
+ if (depth, data) in dirmap:
+ directives, start_offset, strip = dirmap.pop((depth, data))
+ substream = stream[start_offset:]
+ if strip:
+ substream = substream[1:-1]
+ stream[start_offset:] = [(SUB, (directives, substream),
+ pos)]
+
+ elif kind is TEXT:
+ for kind, data, pos in self._interpolate(data, self.basedir,
+ *pos):
+ stream.append((kind, data, pos))
+
+ elif kind is COMMENT:
+ if not data.lstrip().startswith('!'):
+ stream.append((kind, data, pos))
+
+ else:
+ stream.append((kind, data, pos))
+
+ return stream
+
+ def _match(self, stream, ctxt, match_templates=None):
+ """Internal stream filter that applies any defined match templates
+ to the stream.
+ """
+ if match_templates is None:
+ match_templates = ctxt._match_templates
+
+ tail = []
+ def _strip(stream):
+ depth = 1
+ while 1:
+ event = stream.next()
+ if event[0] is START:
+ depth += 1
+ elif event[0] is END:
+ depth -= 1
+ if depth > 0:
+ yield event
+ else:
+ tail[:] = [event]
+ break
+
+ for event in stream:
+
+ # We (currently) only care about start and end events for matching
+ # We might care about namespace events in the future, though
+ if not match_templates or (event[0] is not START and
+ event[0] is not END):
+ yield event
+ continue
+
+ for idx, (test, path, template, namespaces, directives) in \
+ enumerate(match_templates):
+
+ if test(event, namespaces, ctxt) is True:
+
+ # Let the remaining match templates know about the event so
+ # they get a chance to update their internal state
+ for test in [mt[0] for mt in match_templates[idx + 1:]]:
+ test(event, namespaces, ctxt, updateonly=True)
+
+ # Consume and store all events until an end event
+ # corresponding to this start event is encountered
+ content = chain([event], self._match(_strip(stream), ctxt),
+ tail)
+ for filter_ in self.filters[3:]:
+ content = filter_(content, ctxt)
+ content = list(content)
+
+ for test in [mt[0] for mt in match_templates]:
+ 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))
+
+ # Recursively process the output
+ template = _apply_directives(template, ctxt, directives)
+ for event in self._match(self._eval(self._flatten(template,
+ ctxt),
+ ctxt), ctxt,
+ match_templates[:idx] +
+ match_templates[idx + 1:]):
+ yield event
+
+ ctxt.pop()
+ break
+
+ else: # no matches
+ yield event
diff --git a/genshi/template/plugin.py b/genshi/template/plugin.py
new file mode 100644
--- /dev/null
+++ b/genshi/template/plugin.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# Copyright (C) 2006 Matthew Good
+# 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/.
+
+"""Basic support for the template engine plugin API used by TurboGears and
+CherryPy/Buffet.
+"""
+
+from pkg_resources import resource_filename
+
+from genshi.eval import Undefined
+from genshi.input import ET, HTML, XML
+from genshi.output import DocType
+from genshi.template.core import Context, Template
+from genshi.template.loader import TemplateLoader
+from genshi.template.markup import MarkupTemplate
+from genshi.template.text import TextTemplate
+
+
+class ConfigurationError(Exception):
+ """Exception raised when invalid plugin options are encountered."""
+
+
+class AbstractTemplateEnginePlugin(object):
+ """Implementation of the plugin API."""
+
+ template_class = None
+ extension = None
+
+ def __init__(self, extra_vars_func=None, options=None):
+ self.get_extra_vars = extra_vars_func
+ if options is None:
+ options = {}
+ self.options = options
+
+ self.default_encoding = options.get('genshi.default_encoding', 'utf-8')
+ auto_reload = options.get('genshi.auto_reload', '1').lower() \
+ in ('1', 'yes', 'true')
+ search_path = options.get('genshi.search_path', '').split(':')
+ try:
+ max_cache_size = int(options.get('genshi.max_cache_size', 25))
+ except ValueError:
+ raise ConfigurationError('Invalid value for max_cache_size: "%s"' %
+ max_cache_size)
+
+ self.loader = TemplateLoader(filter(None, search_path),
+ auto_reload=auto_reload,
+ max_cache_size=max_cache_size)
+
+ def load_template(self, templatename, template_string=None):
+ """Find a template specified in python 'dot' notation, or load one from
+ a string.
+ """
+ if template_string is not None:
+ return self.template_class(template_string)
+
+ divider = templatename.rfind('.')
+ if divider >= 0:
+ package = templatename[:divider]
+ basename = templatename[divider + 1:] + self.extension
+ templatename = resource_filename(package, basename)
+
+ return self.loader.load(templatename, cls=self.template_class)
+
+ def _get_render_options(self, format=None):
+ if format is None:
+ format = self.default_format
+ kwargs = {'method': format}
+ if self.default_encoding:
+ kwargs['encoding'] = self.default_encoding
+ return kwargs
+
+ def render(self, info, format=None, fragment=False, template=None):
+ """Render the template to a string using the provided info."""
+ kwargs = self._get_render_options(format=format)
+ return self.transform(info, template).render(**kwargs)
+
+ def transform(self, info, template):
+ """Render the output to an event stream."""
+ if not isinstance(template, Template):
+ template = self.load_template(template)
+ ctxt = Context(**info)
+
+ # Some functions for Kid compatibility
+ def defined(name):
+ return ctxt.get(name, Undefined) is not Undefined
+ ctxt['defined'] = defined
+ def value_of(name, default=None):
+ return ctxt.get(name, default)
+ ctxt['value_of'] = value_of
+
+ return template.generate(ctxt)
+
+
+class MarkupTemplateEnginePlugin(AbstractTemplateEnginePlugin):
+ """Implementation of the plugin API for markup templates."""
+
+ template_class = MarkupTemplate
+ extension = '.html'
+
+ doctypes = {'html': DocType.HTML, 'html-strict': DocType.HTML_STRICT,
+ 'html-transitional': DocType.HTML_TRANSITIONAL,
+ 'xhtml': DocType.XHTML, 'xhtml-strict': DocType.XHTML_STRICT,
+ 'xhtml-transitional': DocType.XHTML_TRANSITIONAL}
+
+ def __init__(self, extra_vars_func=None, options=None):
+ AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
+
+ doctype = options.get('genshi.default_doctype')
+ if doctype and doctype not in self.doctypes:
+ raise ConfigurationError('Unknown doctype "%s"' % doctype)
+ self.default_doctype = self.doctypes.get(doctype)
+
+ format = options.get('genshi.default_format', 'html')
+ if format not in ('html', 'xhtml', 'xml', 'text'):
+ raise ConfigurationError('Unknown output format "%s"' % format)
+ self.default_format = format
+
+ def _get_render_options(self, format=None):
+ kwargs = super(MarkupTemplateEnginePlugin,
+ self)._get_render_options(format)
+ if self.default_doctype:
+ kwargs['doctype'] = self.default_doctype
+ return kwargs
+
+ def transform(self, info, template):
+ """Render the output to an event stream."""
+ data = {'ET': ET, 'HTML': HTML, 'XML': XML}
+ if self.get_extra_vars:
+ data.update(self.get_extra_vars())
+ data.update(info)
+ return super(MarkupTemplateEnginePlugin, self).transform(data, template)
+
+
+class TextTemplateEnginePlugin(AbstractTemplateEnginePlugin):
+ """Implementation of the plugin API for text templates."""
+
+ template_class = TextTemplate
+ extension = '.txt'
+ default_format = 'text'
+
+ def transform(self, info, template):
+ """Render the output to an event stream."""
+ data = {}
+ if self.get_extra_vars:
+ data.update(self.get_extra_vars())
+ data.update(info)
+ return super(TextTemplateEnginePlugin, self).transform(data, template)
diff --git a/genshi/template/tests/__init__.py b/genshi/template/tests/__init__.py
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/__init__.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+import doctest
+import unittest
+
+
+def suite():
+ from genshi.template.tests import core, directives, eval, loader, markup, \
+ text
+ suite = unittest.TestSuite()
+ suite.addTest(core.suite())
+ suite.addTest(directives.suite())
+ suite.addTest(eval.suite())
+ suite.addTest(loader.suite())
+ suite.addTest(markup.suite())
+ suite.addTest(text.suite())
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/genshi/template/tests/core.py b/genshi/template/tests/core.py
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/core.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+import doctest
+import unittest
+
+from genshi.core import Stream
+from genshi.template.core import Template
+
+
+class TemplateTestCase(unittest.TestCase):
+ """Tests for basic template processing, expression evaluation and error
+ reporting.
+ """
+
+ def test_interpolate_string(self):
+ parts = list(Template._interpolate('bla'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Stream.TEXT, parts[0][0])
+ self.assertEqual('bla', parts[0][1])
+
+ def test_interpolate_simple(self):
+ parts = list(Template._interpolate('${bla}'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Template.EXPR, parts[0][0])
+ self.assertEqual('bla', parts[0][1].source)
+
+ def test_interpolate_escaped(self):
+ parts = list(Template._interpolate('$${bla}'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Stream.TEXT, parts[0][0])
+ self.assertEqual('${bla}', parts[0][1])
+
+ def test_interpolate_short(self):
+ parts = list(Template._interpolate('$bla'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Template.EXPR, parts[0][0])
+ self.assertEqual('bla', parts[0][1].source)
+
+ def test_interpolate_short_starting_with_underscore(self):
+ parts = list(Template._interpolate('$_bla'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Template.EXPR, parts[0][0])
+ self.assertEqual('_bla', parts[0][1].source)
+
+ def test_interpolate_short_containing_underscore(self):
+ parts = list(Template._interpolate('$foo_bar'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Template.EXPR, parts[0][0])
+ self.assertEqual('foo_bar', parts[0][1].source)
+
+ def test_interpolate_short_starting_with_dot(self):
+ parts = list(Template._interpolate('$.bla'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Stream.TEXT, parts[0][0])
+ self.assertEqual('$.bla', parts[0][1])
+
+ def test_interpolate_short_containing_dot(self):
+ parts = list(Template._interpolate('$foo.bar'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Template.EXPR, parts[0][0])
+ self.assertEqual('foo.bar', parts[0][1].source)
+
+ def test_interpolate_short_starting_with_digit(self):
+ parts = list(Template._interpolate('$0bla'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Stream.TEXT, parts[0][0])
+ self.assertEqual('$0bla', parts[0][1])
+
+ def test_interpolate_short_containing_digit(self):
+ parts = list(Template._interpolate('$foo0'))
+ self.assertEqual(1, len(parts))
+ self.assertEqual(Template.EXPR, parts[0][0])
+ self.assertEqual('foo0', parts[0][1].source)
+
+ def test_interpolate_mixed1(self):
+ parts = list(Template._interpolate('$foo bar $baz'))
+ self.assertEqual(3, len(parts))
+ self.assertEqual(Template.EXPR, parts[0][0])
+ self.assertEqual('foo', parts[0][1].source)
+ self.assertEqual(Stream.TEXT, parts[1][0])
+ self.assertEqual(' bar ', parts[1][1])
+ self.assertEqual(Template.EXPR, parts[2][0])
+ self.assertEqual('baz', parts[2][1].source)
+
+ def test_interpolate_mixed2(self):
+ parts = list(Template._interpolate('foo $bar baz'))
+ self.assertEqual(3, len(parts))
+ self.assertEqual(Stream.TEXT, parts[0][0])
+ self.assertEqual('foo ', parts[0][1])
+ self.assertEqual(Template.EXPR, parts[1][0])
+ self.assertEqual('bar', parts[1][1].source)
+ self.assertEqual(Stream.TEXT, parts[2][0])
+ self.assertEqual(' baz', parts[2][1])
+
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(Template.__module__))
+ suite.addTest(unittest.makeSuite(TemplateTestCase, 'test'))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/genshi/template/tests/directives.py b/genshi/template/tests/directives.py
new file mode 100644
--- /dev/null
+++ b/genshi/template/tests/directives.py
@@ -0,0 +1,910 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 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/.
+
+import doctest
+import sys
+import unittest
+
+from genshi.template import directives, MarkupTemplate, TextTemplate, \
+ TemplateRuntimeError
+
+
+class AttrsDirectiveTestCase(unittest.TestCase):
+ """Tests for the `py:attrs` template directive."""
+
+ def test_combined_with_loop(self):
+ """
+ Verify that the directive has access to the loop variables.
+ """
+ tmpl = MarkupTemplate("""
+
+ """)
+ items = [{'id': 1, 'class': 'foo'}, {'id': 2, 'class': 'bar'}]
+ self.assertEqual("""
+
+ """, str(tmpl.generate(items=items)))
+
+ def test_update_existing_attr(self):
+ """
+ Verify that an attribute value that evaluates to `None` removes an
+ existing attribute of that name.
+ """
+ tmpl = MarkupTemplate("""
+
+ """)
+ self.assertEqual("""
+
+ """, str(tmpl.generate()))
+
+ def test_remove_existing_attr(self):
+ """
+ Verify that an attribute value that evaluates to `None` removes an
+ existing attribute of that name.
+ """
+ tmpl = MarkupTemplate("""
+
+ """)
+ self.assertEqual("""
+
+ """, str(tmpl.generate()))
+
+
+class ChooseDirectiveTestCase(unittest.TestCase):
+ """Tests for the `py:choose` template directive and the complementary
+ directives `py:when` and `py:otherwise`."""
+
+ def test_multiple_true_whens(self):
+ """
+ Verify that, if multiple `py:when` bodies match, only the first is
+ output.
+ """
+ tmpl = MarkupTemplate("""
+ """, str(tmpl.generate()))
+
+ def test_when_with_strip(self):
+ """
+ Verify that a when directive with a strip directive actually strips of
+ the outer element.
+ """
+ tmpl = MarkupTemplate("""
+
+ foo
+
+ """)
+ self.assertEqual("""
+ foo
+ """, str(tmpl.generate()))
+
+ def test_when_outside_choose(self):
+ """
+ Verify that a `when` directive outside of a `choose` directive is
+ reported as an error.
+ """
+ tmpl = MarkupTemplate("""
+
+ """)
+ self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
+
+ def test_otherwise_outside_choose(self):
+ """
+ Verify that an `otherwise` directive outside of a `choose` directive is
+ reported as an error.
+ """
+ tmpl = MarkupTemplate("""
+
+ """)
+ self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
+
+ def test_when_without_test(self):
+ """
+ Verify that an `when` directive that doesn't have a `test` attribute
+ is reported as an error.
+ """
+ tmpl = MarkupTemplate("""
+
+ foo
+
+ """)
+ self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
+
+ def test_when_without_test_but_with_choose_value(self):
+ """
+ Verify that an `when` directive that doesn't have a `test` attribute
+ works as expected as long as the parent `choose` directive has a test
+ expression.
+ """
+ tmpl = MarkupTemplate("""
+
+ foo
+
+ """)
+ self.assertEqual("""
+ foo
+ """, str(tmpl.generate(foo='Yeah')))
+
+ def test_otherwise_without_test(self):
+ """
+ Verify that an `otherwise` directive can be used without a `test`
+ attribute.
+ """
+ tmpl = MarkupTemplate("""
+
+ foo
+
+ """)
+ self.assertEqual("""
+ foo
+ """, str(tmpl.generate()))
+
+ def test_as_element(self):
+ """
+ Verify that the directive can also be used as an element.
+ """
+ tmpl = MarkupTemplate("""
+
+ 1
+ 2
+ 3
+
+ """)
+ self.assertEqual("""
+ 1
+ """, str(tmpl.generate()))
+
+ def test_in_text_template(self):
+ """
+ Verify that the directive works as expected in a text template.
+ """
+ tmpl = TextTemplate("""#choose
+ #when 1 == 1
+ 1
+ #end
+ #when 2 == 2
+ 2
+ #end
+ #when 3 == 3
+ 3
+ #end
+ #end""")
+ self.assertEqual(""" 1\n""", str(tmpl.generate()))
+
+
+class DefDirectiveTestCase(unittest.TestCase):
+ """Tests for the `py:def` template directive."""
+
+ def test_function_with_strip(self):
+ """
+ Verify that a named template function with a strip directive actually
+ strips of the outer element.
+ """
+ tmpl = MarkupTemplate("""
+
+
+ """)
+ self.assertRaises(TypeError, list, tmpl.generate(badfunc=badfunc))
+
+ def test_def_in_matched(self):
+ tmpl = MarkupTemplate("""
+ ${select('*')}
+
+
+ ${maketitle(True)}
+
+ """)
+ self.assertEqual("""
+ True
+ """, str(tmpl.generate()))
+
+ def test_in_text_template(self):
+ """
+ Verify that the directive works as expected in a text template.
+ """
+ tmpl = TextTemplate("""
+ #def echo(greeting, name='world')
+ ${greeting}, ${name}!
+ #end
+ ${echo('Hi', name='you')}
+ """)
+ self.assertEqual(""" Hi, you!
+ """, str(tmpl.generate()))
+
+
+class ForDirectiveTestCase(unittest.TestCase):
+ """Tests for the `py:for` template directive."""
+
+ def test_loop_with_strip(self):
+ """
+ Verify that the combining the `py:for` directive with `py:strip` works
+ correctly.
+ """
+ tmpl = MarkupTemplate("""
+
+ ${item}
+
+ """)
+ self.assertEqual("""
+ 1
+ 2
+ 3
+ 4
+ 5
+ """, str(tmpl.generate(items=range(1, 6))))
+
+ def test_as_element(self):
+ """
+ Verify that the directive can also be used as an element.
+ """
+ tmpl = MarkupTemplate("""
+
+ ${item}
+
+ """)
+ self.assertEqual("""
+ 1
+ 2
+ 3
+ 4
+ 5
+ """, str(tmpl.generate(items=range(1, 6))))
+
+ def test_multi_assignment(self):
+ """
+ Verify that assignment to tuples works correctly.
+ """
+ tmpl = MarkupTemplate("""
+
+
key=$k, value=$v
+
+ """)
+ self.assertEqual("""
+
key=a, value=1
+
key=b, value=2
+ """, str(tmpl.generate(items=dict(a=1, b=2).items())))
+
+ def test_nested_assignment(self):
+ """
+ Verify that assignment to nested tuples works correctly.
+ """
+ tmpl = MarkupTemplate("""
+
+
$idx: key=$k, value=$v
+
+ """)
+ self.assertEqual("""
+
0: key=a, value=1
+
1: key=b, value=2
+ """, str(tmpl.generate(items=enumerate(dict(a=1, b=2).items()))))
+
+ def test_not_iterable(self):
+ """
+ Verify that assignment to nested tuples works correctly.
+ """
+ tmpl = MarkupTemplate("""
+
+ $item
+
+ """, filename='test.html')
+ try:
+ list(tmpl.generate(foo=12))
+ except TemplateRuntimeError, e:
+ self.assertEqual('test.html', e.filename)
+ if sys.version_info[:2] >= (2, 4):
+ self.assertEqual(2, e.lineno)
+
+
+class IfDirectiveTestCase(unittest.TestCase):
+ """Tests for the `py:if` template directive."""
+
+ def test_loop_with_strip(self):
+ """
+ Verify that the combining the `py:if` directive with `py:strip` works
+ correctly.
+ """
+ tmpl = MarkupTemplate("""
+ ${bar}
+ """)
+ self.assertEqual("""
+ Hello
+ """, str(tmpl.generate(foo=True, bar='Hello')))
+
+ def test_as_element(self):
+ """
+ Verify that the directive can also be used as an element.
+ """
+ tmpl = MarkupTemplate("""
+ ${bar}
+ """)
+ self.assertEqual("""
+ Hello
+ """, str(tmpl.generate(foo=True, bar='Hello')))
+
+
+class MatchDirectiveTestCase(unittest.TestCase):
+ """Tests for the `py:match` template directive."""
+
+ def test_with_strip(self):
+ """
+ Verify that a match template can produce the same kind of element that
+ it matched without entering an infinite recursion.
+ """
+ tmpl = MarkupTemplate("""
+
+
${select('text()')}
+
+ Hey Joe
+ """)
+ self.assertEqual("""
+
Hey Joe
+ """, str(tmpl.generate()))
+
+ def test_without_strip(self):
+ """
+ Verify that a match template can produce the same kind of element that
+ it matched without entering an infinite recursion.
+ """
+ tmpl = MarkupTemplate("""
+
+
${select('text()')}
+
+ Hey Joe
+ """)
+ self.assertEqual("""
+
+
Hey Joe
+
+ """, str(tmpl.generate()))
+
+ def test_as_element(self):
+ """
+ Verify that the directive can also be used as an element.
+ """
+ tmpl = MarkupTemplate("""
+
+
${select('text()')}
+
+ Hey Joe
+ """)
+ self.assertEqual("""
+
Hey Joe
+ """, str(tmpl.generate()))
+
+ def test_recursive_match_1(self):
+ """
+ Match directives are applied recursively, meaning that they are also
+ applied to any content they may have produced themselves:
+ """
+ tmpl = MarkupTemplate("""
+
+
+ ${select('*')}
+
+
+
+
+
+
+
+ """)
+ self.assertEqual("""
+
+
+
+
+
+
+
+
+
+
+ """, str(tmpl.generate()))
+
+ def test_recursive_match_2(self):
+ """
+ When two or more match templates match the same element and also
+ themselves output the element they match, avoiding recursion is even
+ more complex, but should work.
+ """
+ tmpl = MarkupTemplate("""
+
+
+ ${select('*')}
+
+
+ ${select('*')}
+
+
+
+
- """, str(tmpl.generate()))
-
- def test_when_with_strip(self):
- """
- Verify that a when directive with a strip directive actually strips of
- the outer element.
- """
- tmpl = MarkupTemplate("""
-
- foo
-
- """)
- self.assertEqual("""
- foo
- """, str(tmpl.generate()))
-
- def test_when_outside_choose(self):
- """
- Verify that a `when` directive outside of a `choose` directive is
- reported as an error.
- """
- tmpl = MarkupTemplate("""
-
- """)
- self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
-
- def test_otherwise_outside_choose(self):
- """
- Verify that an `otherwise` directive outside of a `choose` directive is
- reported as an error.
- """
- tmpl = MarkupTemplate("""
-
- """)
- self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
-
- def test_when_without_test(self):
- """
- Verify that an `when` directive that doesn't have a `test` attribute
- is reported as an error.
- """
- tmpl = MarkupTemplate("""
-
- foo
-
- """)
- self.assertRaises(TemplateRuntimeError, str, tmpl.generate())
-
- def test_when_without_test_but_with_choose_value(self):
- """
- Verify that an `when` directive that doesn't have a `test` attribute
- works as expected as long as the parent `choose` directive has a test
- expression.
- """
- tmpl = MarkupTemplate("""
-
- foo
-
- """)
- self.assertEqual("""
- foo
- """, str(tmpl.generate(foo='Yeah')))
-
- def test_otherwise_without_test(self):
- """
- Verify that an `otherwise` directive can be used without a `test`
- attribute.
- """
- tmpl = MarkupTemplate("""
-
- foo
-
- """)
- self.assertEqual("""
- foo
- """, str(tmpl.generate()))
-
- def test_as_element(self):
- """
- Verify that the directive can also be used as an element.
- """
- tmpl = MarkupTemplate("""
-
- 1
- 2
- 3
-
- """)
- self.assertEqual("""
- 1
- """, str(tmpl.generate()))
-
- def test_in_text_template(self):
- """
- Verify that the directive works as expected in a text template.
- """
- tmpl = TextTemplate("""#choose
- #when 1 == 1
- 1
- #end
- #when 2 == 2
- 2
- #end
- #when 3 == 3
- 3
- #end
- #end""")
- self.assertEqual(""" 1\n""", str(tmpl.generate()))
-
-
-class DefDirectiveTestCase(unittest.TestCase):
- """Tests for the `py:def` template directive."""
-
- def test_function_with_strip(self):
- """
- Verify that a named template function with a strip directive actually
- strips of the outer element.
- """
- tmpl = MarkupTemplate("""
-
-
- """)
- self.assertRaises(TypeError, list, tmpl.generate(badfunc=badfunc))
-
- def test_def_in_matched(self):
- tmpl = MarkupTemplate("""
- ${select('*')}
-
-
- ${maketitle(True)}
-
- """)
- self.assertEqual("""
- True
- """, str(tmpl.generate()))
-
- def test_in_text_template(self):
- """
- Verify that the directive works as expected in a text template.
- """
- tmpl = TextTemplate("""
- #def echo(greeting, name='world')
- ${greeting}, ${name}!
- #end
- ${echo('Hi', name='you')}
- """)
- self.assertEqual(""" Hi, you!
- """, str(tmpl.generate()))
-
-
-class ForDirectiveTestCase(unittest.TestCase):
- """Tests for the `py:for` template directive."""
-
- def test_loop_with_strip(self):
- """
- Verify that the combining the `py:for` directive with `py:strip` works
- correctly.
- """
- tmpl = MarkupTemplate("""
-
- ${item}
-
- """)
- self.assertEqual("""
- 1
- 2
- 3
- 4
- 5
- """, str(tmpl.generate(items=range(1, 6))))
-
- def test_as_element(self):
- """
- Verify that the directive can also be used as an element.
- """
- tmpl = MarkupTemplate("""
-
- ${item}
-
- """)
- self.assertEqual("""
- 1
- 2
- 3
- 4
- 5
- """, str(tmpl.generate(items=range(1, 6))))
-
- def test_multi_assignment(self):
- """
- Verify that assignment to tuples works correctly.
- """
- tmpl = MarkupTemplate("""
-
-
key=$k, value=$v
-
- """)
- self.assertEqual("""
-
key=a, value=1
-
key=b, value=2
- """, str(tmpl.generate(items=dict(a=1, b=2).items())))
-
- def test_nested_assignment(self):
- """
- Verify that assignment to nested tuples works correctly.
- """
- tmpl = MarkupTemplate("""
-
-
$idx: key=$k, value=$v
-
- """)
- self.assertEqual("""
-
0: key=a, value=1
-
1: key=b, value=2
- """, str(tmpl.generate(items=enumerate(dict(a=1, b=2).items()))))
-
- def test_not_iterable(self):
- """
- Verify that assignment to nested tuples works correctly.
- """
- tmpl = MarkupTemplate("""
-
- $item
-
- """, filename='test.html')
- try:
- list(tmpl.generate(foo=12))
- except TemplateRuntimeError, e:
- self.assertEqual('test.html', e.filename)
- if sys.version_info[:2] >= (2, 4):
- self.assertEqual(2, e.lineno)
-
-
-class IfDirectiveTestCase(unittest.TestCase):
- """Tests for the `py:if` template directive."""
-
- def test_loop_with_strip(self):
- """
- Verify that the combining the `py:if` directive with `py:strip` works
- correctly.
- """
- tmpl = MarkupTemplate("""
- ${bar}
- """)
- self.assertEqual("""
- Hello
- """, str(tmpl.generate(foo=True, bar='Hello')))
-
- def test_as_element(self):
- """
- Verify that the directive can also be used as an element.
- """
- tmpl = MarkupTemplate("""
- ${bar}
- """)
- self.assertEqual("""
- Hello
- """, str(tmpl.generate(foo=True, bar='Hello')))
-
-
-class MatchDirectiveTestCase(unittest.TestCase):
- """Tests for the `py:match` template directive."""
-
- def test_with_strip(self):
- """
- Verify that a match template can produce the same kind of element that
- it matched without entering an infinite recursion.
- """
- tmpl = MarkupTemplate("""
-
-
${select('text()')}
-
- Hey Joe
- """)
- self.assertEqual("""
-
Hey Joe
- """, str(tmpl.generate()))
-
- def test_without_strip(self):
- """
- Verify that a match template can produce the same kind of element that
- it matched without entering an infinite recursion.
- """
- tmpl = MarkupTemplate("""
-
-
${select('text()')}
-
- Hey Joe
- """)
- self.assertEqual("""
-
-
Hey Joe
-
- """, str(tmpl.generate()))
-
- def test_as_element(self):
- """
- Verify that the directive can also be used as an element.
- """
- tmpl = MarkupTemplate("""
-
-
${select('text()')}
-
- Hey Joe
- """)
- self.assertEqual("""
-
Hey Joe
- """, str(tmpl.generate()))
-
- def test_recursive_match_1(self):
- """
- Match directives are applied recursively, meaning that they are also
- applied to any content they may have produced themselves:
- """
- tmpl = MarkupTemplate("""
-
-
- ${select('*')}
-
-
-
-
-
-
-
- """)
- self.assertEqual("""
-
-
-
-
-
-
-
-
-
-
- """, str(tmpl.generate()))
-
- def test_recursive_match_2(self):
- """
- When two or more match templates match the same element and also
- themselves output the element they match, avoiding recursion is even
- more complex, but should work.
- """
- tmpl = MarkupTemplate("""
-
-
- ${select('*')}
-
-
- ${select('*')}
-
-
-
-