Mercurial > genshi > genshi-test
changeset 336:5f2c7782cd8a
Refactoring: `genshi.template` is now a package, it was getting way to crowded in that file.
author | cmlenz |
---|---|
date | Wed, 08 Nov 2006 15:50:15 +0000 |
parents | e14a0332cfdc |
children | fd1c77710fec 6c8b7a1fb50d |
files | ChangeLog UPGRADE.txt genshi/__init__.py genshi/eval.py genshi/plugin.py genshi/template.py genshi/template/__init__.py genshi/template/core.py genshi/template/directives.py genshi/template/eval.py genshi/template/loader.py genshi/template/markup.py genshi/template/plugin.py genshi/template/tests/__init__.py genshi/template/tests/core.py genshi/template/tests/directives.py genshi/template/tests/eval.py genshi/template/tests/loader.py genshi/template/tests/markup.py genshi/template/tests/text.py genshi/template/text.py genshi/tests/__init__.py genshi/tests/eval.py genshi/tests/template.py setup.py |
diffstat | 24 files changed, 4015 insertions(+), 3784 deletions(-) [+] |
line wrap: on
line diff
--- 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/
--- 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 ---------------------
--- 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('<html lang="en"><head><title>My document</title></head></html>') - -This results in a `Stream` object that can be used in a number of way. - ->>> doc.render(method='html', encoding='utf-8') -'<html lang="en"><head><title>My document</title></head></html>' - ->>> from genshi.input import HTML ->>> doc = HTML('<HTML lang=en><HEAD><TITLE>My document</HTML>') ->>> doc.render(method='html', encoding='utf-8') -'<html lang="en"><head><title>My document</title></head></html>' - ->>> title = doc.select('head/title') ->>> title.render(method='html', encoding='utf-8') -'<title>My document</title>' - - -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') -'<doc lang="en"><title>My document</title></doc>' """ from genshi.core import *
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 = '<string>' - 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, - '<Expression %s>' % (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 = '<string>' # 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 = '<string>' # 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 = '<string>' # 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]) - ])
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)
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='<string>', 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='<string>', 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='<string>', 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('''<ul xmlns:py="http://genshi.edgewall.org/"> - ... <li py:attrs="foo">Bar</li> - ... </ul>''') - >>> print tmpl.generate(foo={'class': 'collapse'}) - <ul> - <li class="collapse">Bar</li> - </ul> - >>> print tmpl.generate(foo=[('class', 'collapse')]) - <ul> - <li class="collapse">Bar</li> - </ul> - - If the value evaluates to `None` (or any other non-truth value), no - attributes are added: - - >>> print tmpl.generate(foo=None) - <ul> - <li>Bar</li> - </ul> - """ - __slots__ = [] - - def __call__(self, stream, 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('''<ul xmlns:py="http://genshi.edgewall.org/"> - ... <li py:content="bar">Hello</li> - ... </ul>''') - >>> print tmpl.generate(bar='Bye') - <ul> - <li>Bye</li> - </ul> - """ - __slots__ = [] - - 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('''<div xmlns:py="http://genshi.edgewall.org/"> - ... <p py:def="echo(greeting, name='world')" class="message"> - ... ${greeting}, ${name}! - ... </p> - ... ${echo('Hi', name='you')} - ... </div>''') - >>> print tmpl.generate(bar='Bye') - <div> - <p class="message"> - Hi, you! - </p> - </div> - - If a function does not require parameters, the parenthesis can be omitted - both when defining and when calling it: - - >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> - ... <p py:def="helloworld" class="message"> - ... Hello, world! - ... </p> - ... ${helloworld} - ... </div>''') - >>> print tmpl.generate(bar='Bye') - <div> - <p class="message"> - Hello, world! - </p> - </div> - """ - __slots__ = ['name', 'args', '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('''<ul xmlns:py="http://genshi.edgewall.org/"> - ... <li py:for="item in items">${item}</li> - ... </ul>''') - >>> print tmpl.generate(items=[1, 2, 3]) - <ul> - <li>1</li><li>2</li><li>3</li> - </ul> - """ - __slots__ = ['assign', 'filename'] - - 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('''<div xmlns:py="http://genshi.edgewall.org/"> - ... <b py:if="foo">${bar}</b> - ... </div>''') - >>> print tmpl.generate(foo=True, bar='Hello') - <div> - <b>Hello</b> - </div> - """ - __slots__ = [] - - ATTRIBUTE = 'test' - - def __call__(self, stream, ctxt, directives): - if self.expr.evaluate(ctxt): - return _apply_directives(stream, ctxt, directives) - return [] - - -class MatchDirective(Directive): - """Implementation of the `py:match` template directive. - - >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> - ... <span py:match="greeting"> - ... Hello ${select('@name')} - ... </span> - ... <greeting name="Dude" /> - ... </div>''') - >>> print tmpl.generate() - <div> - <span> - Hello Dude - </span> - </div> - """ - __slots__ = ['path', 'namespaces'] - - 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('''<div xmlns:py="http://genshi.edgewall.org/"> - ... <span py:replace="bar">Hello</span> - ... </div>''') - >>> print tmpl.generate(bar='Bye') - <div> - Bye - </div> - - This directive is equivalent to `py:content` combined with `py:strip`, - providing a less verbose way to achieve the same effect: - - >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> - ... <span py:content="bar" py:strip="">Hello</span> - ... </div>''') - >>> print tmpl.generate(bar='Bye') - <div> - Bye - </div> - """ - __slots__ = [] - - 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('''<div xmlns:py="http://genshi.edgewall.org/"> - ... <div py:strip="True"><b>foo</b></div> - ... </div>''') - >>> print tmpl.generate() - <div> - <b>foo</b> - </div> - - Leaving the attribute value empty is equivalent to a truth value. - - This directive is particulary interesting for named template functions or - match templates that do not generate a top-level element: - - >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> - ... <div py:def="echo(what)" py:strip=""> - ... <b>${what}</b> - ... </div> - ... ${echo('foo')} - ... </div>''') - >>> print tmpl.generate() - <div> - <b>foo</b> - </div> - """ - __slots__ = [] - - def __call__(self, stream, 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('''<div xmlns:py="http://genshi.edgewall.org/" - ... py:choose=""> - ... <span py:when="0 == 1">0</span> - ... <span py:when="1 == 1">1</span> - ... <span py:otherwise="">2</span> - ... </div>''') - >>> print tmpl.generate() - <div> - <span>1</span> - </div> - - If the `py:choose` directive contains an expression, the nested `py:when` - directives are tested for equality to the `py:choose` expression: - - >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" - ... py:choose="2"> - ... <span py:when="1">1</span> - ... <span py:when="2">2</span> - ... </div>''') - >>> print tmpl.generate() - <div> - <span>2</span> - </div> - - Behavior is undefined if a `py:choose` block contains content outside a - `py:when` or `py:otherwise` block. Behavior is also undefined if a - `py:otherwise` occurs before `py:when` blocks. - """ - __slots__ = ['matched', 'value'] - - 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('''<div xmlns:py="http://genshi.edgewall.org/"> - ... <span py:with="y=7; z=x+10">$x $y $z</span> - ... </div>''') - >>> print tmpl.generate(x=42) - <div> - <span>42 7 52</span> - </div> - """ - __slots__ = ['vars'] - - 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'(?<!\$)\$\{(.+?)\}', re.DOTALL) - _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z_][a-zA-Z0-9_\.]*)') - - def _interpolate(cls, text, basedir=None, filename=None, lineno=-1, - offset=0): - """Parse the given string and extract expressions. - - This method returns a list containing both literal text and `Expression` - objects. - - @param text: the text to parse - @param lineno: the line number at which the text was found (optional) - @param offset: the column number at which the text starts in the source - (optional) - """ - filepath = filename - if filepath and basedir: - filepath = os.path.join(basedir, filepath) - def _interpolate(text, patterns, lineno=lineno, offset=offset): - for idx, grp in enumerate(patterns.pop(0).split(text)): - if idx % 2: - try: - yield EXPR, Expression(grp.strip(), filepath, lineno), \ - (filename, lineno, offset) - except SyntaxError, err: - raise TemplateSyntaxError(err, filepath, lineno, - offset + (err.offset or 0)) - elif grp: - if patterns: - for result in _interpolate(grp, patterns[:]): - yield result - else: - yield TEXT, grp.replace('$$', '$'), \ - (filename, lineno, offset) - if '\n' in grp: - lines = grp.splitlines() - lineno += len(lines) - 1 - offset += len(lines[-1]) - else: - offset += len(grp) - return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE]) - _interpolate = classmethod(_interpolate) - - def generate(self, *args, **kwargs): - """Apply the template to the given context data. - - Any keyword arguments are made available to the template as context - data. - - Only one positional argument is accepted: if it is provided, it must be - an instance of the `Context` class, and keyword arguments are ignored. - This calling style is used for internal processing. - - @return: a markup event stream representing the result of applying - the template to the context data. - """ - if args: - assert len(args) == 1 - ctxt = args[0] - if ctxt is None: - ctxt = Context(**kwargs) - assert isinstance(ctxt, Context) - else: - ctxt = Context(**kwargs) - - stream = self.stream - for filter_ in self.filters: - stream = filter_(iter(stream), ctxt) - return Stream(stream) - - def _eval(self, stream, ctxt): - """Internal stream filter that evaluates any expressions in `START` and - `TEXT` events. - """ - filters = (self._flatten, self._eval) - - for kind, data, pos in stream: - - if kind is START and data[1]: - # Attributes may still contain expressions in start tags at - # this point, so do some evaluation - tag, attrib = data - new_attrib = [] - for name, substream in attrib: - if isinstance(substream, basestring): - value = substream - else: - values = [] - for subkind, subdata, subpos in self._eval(substream, - ctxt): - if subkind is TEXT: - values.append(subdata) - value = [x for x in values if x is not None] - if not value: - continue - new_attrib.append((name, u''.join(value))) - yield kind, (tag, Attrs(new_attrib)), pos - - elif kind is EXPR: - result = data.evaluate(ctxt) - if result is not None: - # First check for a string, otherwise the iterable test below - # succeeds, and the string will be chopped up into individual - # characters - if isinstance(result, basestring): - yield TEXT, result, pos - elif hasattr(result, '__iter__'): - substream = _ensure(result) - for filter_ in filters: - substream = filter_(substream, ctxt) - for event in substream: - yield event - else: - yield TEXT, unicode(result), pos - - else: - yield kind, data, pos - - def _flatten(self, stream, ctxt): - """Internal stream filter that expands `SUB` events in the stream.""" - for event in stream: - if event[0] is SUB: - # This event is a list of directives and a list of nested - # events to which those directives should be applied - directives, substream = event[1] - substream = _apply_directives(substream, ctxt, directives) - for event in self._flatten(substream, ctxt): - yield event - else: - yield event - - -EXPR = Template.EXPR -SUB = Template.SUB - - -class MarkupTemplate(Template): - """Implementation of the template language for XML-based templates. - - >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> - ... <li py:for="item in items">${item}</li> - ... </ul>''') - >>> print tmpl.generate(items=[1, 2, 3]) - <ul> - <li>1</li><li>2</li><li>3</li> - </ul> - """ - 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, - <BLANKLINE> - We have the following items for you: - * 1 - * 2 - * 3 - <BLANKLINE> - 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*(?<!\\)#((?:\w+|#).*)\n?', re.MULTILINE) - - def _parse(self, encoding): - """Parse the template from text input.""" - stream = [] # list of events of the "compiled" template - dirmap = {} # temporary mapping of directives to elements - depth = 0 - if not encoding: - encoding = 'utf-8' - - source = self.source.read().decode(encoding, 'replace') - offset = 0 - lineno = 1 - - for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)): - start, end = mo.span() - if start > 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, '<p>$var</p>') - 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
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='<string>', 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='<string>', 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='<string>', 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'(?<!\$)\$\{(.+?)\}', re.DOTALL) + _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z_][a-zA-Z0-9_\.]*)') + + def _interpolate(cls, text, basedir=None, filename=None, lineno=-1, + offset=0): + """Parse the given string and extract expressions. + + This method returns a list containing both literal text and `Expression` + objects. + + @param text: the text to parse + @param lineno: the line number at which the text was found (optional) + @param offset: the column number at which the text starts in the source + (optional) + """ + filepath = filename + if filepath and basedir: + filepath = os.path.join(basedir, filepath) + def _interpolate(text, patterns, lineno=lineno, offset=offset): + for idx, grp in enumerate(patterns.pop(0).split(text)): + if idx % 2: + try: + yield EXPR, Expression(grp.strip(), filepath, lineno), \ + (filename, lineno, offset) + except SyntaxError, err: + raise TemplateSyntaxError(err, filepath, lineno, + offset + (err.offset or 0)) + elif grp: + if patterns: + for result in _interpolate(grp, patterns[:]): + yield result + else: + yield TEXT, grp.replace('$$', '$'), \ + (filename, lineno, offset) + if '\n' in grp: + lines = grp.splitlines() + lineno += len(lines) - 1 + offset += len(lines[-1]) + else: + offset += len(grp) + return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE]) + _interpolate = classmethod(_interpolate) + + def generate(self, *args, **kwargs): + """Apply the template to the given context data. + + Any keyword arguments are made available to the template as context + data. + + Only one positional argument is accepted: if it is provided, it must be + an instance of the `Context` class, and keyword arguments are ignored. + This calling style is used for internal processing. + + @return: a markup event stream representing the result of applying + the template to the context data. + """ + if args: + assert len(args) == 1 + ctxt = args[0] + if ctxt is None: + ctxt = Context(**kwargs) + assert isinstance(ctxt, Context) + else: + ctxt = Context(**kwargs) + + stream = self.stream + for filter_ in self.filters: + stream = filter_(iter(stream), ctxt) + return Stream(stream) + + def _eval(self, stream, ctxt): + """Internal stream filter that evaluates any expressions in `START` and + `TEXT` events. + """ + filters = (self._flatten, self._eval) + + for kind, data, pos in stream: + + if kind is START and data[1]: + # Attributes may still contain expressions in start tags at + # this point, so do some evaluation + tag, attrib = data + new_attrib = [] + for name, substream in attrib: + if isinstance(substream, basestring): + value = substream + else: + values = [] + for subkind, subdata, subpos in self._eval(substream, + ctxt): + if subkind is TEXT: + values.append(subdata) + value = [x for x in values if x is not None] + if not value: + continue + new_attrib.append((name, u''.join(value))) + yield kind, (tag, Attrs(new_attrib)), pos + + elif kind is EXPR: + result = data.evaluate(ctxt) + if result is not None: + # First check for a string, otherwise the iterable test below + # succeeds, and the string will be chopped up into individual + # characters + if isinstance(result, basestring): + yield TEXT, result, pos + elif hasattr(result, '__iter__'): + substream = _ensure(result) + for filter_ in filters: + substream = filter_(substream, ctxt) + for event in substream: + yield event + else: + yield TEXT, unicode(result), pos + + else: + yield kind, data, pos + + def _flatten(self, stream, ctxt): + """Internal stream filter that expands `SUB` events in the stream.""" + for event in stream: + if event[0] is SUB: + # This event is a list of directives and a list of nested + # events to which those directives should be applied + directives, substream = event[1] + substream = _apply_directives(substream, ctxt, directives) + for event in self._flatten(substream, ctxt): + yield event + else: + yield event + + +EXPR = Template.EXPR +SUB = Template.SUB
new file mode 100644 --- /dev/null +++ b/genshi/template/directives.py @@ -0,0 +1,600 @@ +# -*- 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/. + +"""Implementation of the various template directives.""" + +import compiler + +from genshi.core import Attrs, Stream +from genshi.path import Path +from genshi.template.core import EXPR, Directive, TemplateRuntimeError, \ + TemplateSyntaxError, _apply_directives +from genshi.template.eval import Expression, _parse + +__all__ = ['AttrsDirective', 'ChooseDirective', 'ContentDirective', + 'DefDirective', 'ForDirective', 'IfDirective', 'MatchDirective', + 'OtherwiseDirective', 'ReplaceDirective', 'StripDirective', + 'WhenDirective', 'WithDirective'] + + +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: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:attrs="foo">Bar</li> + ... </ul>''') + >>> print tmpl.generate(foo={'class': 'collapse'}) + <ul> + <li class="collapse">Bar</li> + </ul> + >>> print tmpl.generate(foo=[('class', 'collapse')]) + <ul> + <li class="collapse">Bar</li> + </ul> + + If the value evaluates to `None` (or any other non-truth value), no + attributes are added: + + >>> print tmpl.generate(foo=None) + <ul> + <li>Bar</li> + </ul> + """ + __slots__ = [] + + def __call__(self, stream, 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('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:content="bar">Hello</li> + ... </ul>''') + >>> print tmpl.generate(bar='Bye') + <ul> + <li>Bye</li> + </ul> + """ + __slots__ = [] + + 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('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <p py:def="echo(greeting, name='world')" class="message"> + ... ${greeting}, ${name}! + ... </p> + ... ${echo('Hi', name='you')} + ... </div>''') + >>> print tmpl.generate(bar='Bye') + <div> + <p class="message"> + Hi, you! + </p> + </div> + + If a function does not require parameters, the parenthesis can be omitted + both when defining and when calling it: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <p py:def="helloworld" class="message"> + ... Hello, world! + ... </p> + ... ${helloworld} + ... </div>''') + >>> print tmpl.generate(bar='Bye') + <div> + <p class="message"> + Hello, world! + </p> + </div> + """ + __slots__ = ['name', 'args', '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('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:for="item in items">${item}</li> + ... </ul>''') + >>> print tmpl.generate(items=[1, 2, 3]) + <ul> + <li>1</li><li>2</li><li>3</li> + </ul> + """ + __slots__ = ['assign', 'filename'] + + 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('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <b py:if="foo">${bar}</b> + ... </div>''') + >>> print tmpl.generate(foo=True, bar='Hello') + <div> + <b>Hello</b> + </div> + """ + __slots__ = [] + + ATTRIBUTE = 'test' + + def __call__(self, stream, ctxt, directives): + if self.expr.evaluate(ctxt): + return _apply_directives(stream, ctxt, directives) + return [] + + +class MatchDirective(Directive): + """Implementation of the `py:match` template directive. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:match="greeting"> + ... Hello ${select('@name')} + ... </span> + ... <greeting name="Dude" /> + ... </div>''') + >>> print tmpl.generate() + <div> + <span> + Hello Dude + </span> + </div> + """ + __slots__ = ['path', 'namespaces'] + + 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('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:replace="bar">Hello</span> + ... </div>''') + >>> print tmpl.generate(bar='Bye') + <div> + Bye + </div> + + This directive is equivalent to `py:content` combined with `py:strip`, + providing a less verbose way to achieve the same effect: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:content="bar" py:strip="">Hello</span> + ... </div>''') + >>> print tmpl.generate(bar='Bye') + <div> + Bye + </div> + """ + __slots__ = [] + + 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('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <div py:strip="True"><b>foo</b></div> + ... </div>''') + >>> print tmpl.generate() + <div> + <b>foo</b> + </div> + + Leaving the attribute value empty is equivalent to a truth value. + + This directive is particulary interesting for named template functions or + match templates that do not generate a top-level element: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <div py:def="echo(what)" py:strip=""> + ... <b>${what}</b> + ... </div> + ... ${echo('foo')} + ... </div>''') + >>> print tmpl.generate() + <div> + <b>foo</b> + </div> + """ + __slots__ = [] + + def __call__(self, stream, 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('''<div xmlns:py="http://genshi.edgewall.org/" + ... py:choose=""> + ... <span py:when="0 == 1">0</span> + ... <span py:when="1 == 1">1</span> + ... <span py:otherwise="">2</span> + ... </div>''') + >>> print tmpl.generate() + <div> + <span>1</span> + </div> + + If the `py:choose` directive contains an expression, the nested `py:when` + directives are tested for equality to the `py:choose` expression: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" + ... py:choose="2"> + ... <span py:when="1">1</span> + ... <span py:when="2">2</span> + ... </div>''') + >>> print tmpl.generate() + <div> + <span>2</span> + </div> + + Behavior is undefined if a `py:choose` block contains content outside a + `py:when` or `py:otherwise` block. Behavior is also undefined if a + `py:otherwise` occurs before `py:when` blocks. + """ + __slots__ = ['matched', 'value'] + + 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('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:with="y=7; z=x+10">$x $y $z</span> + ... </div>''') + >>> print tmpl.generate(x=42) + <div> + <span>42 7 52</span> + </div> + """ + __slots__ = ['vars'] + + 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__)
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 = '<string>' + 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, + '<Expression %s>' % (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 = '<string>' # 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 = '<string>' # 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 = '<string>' # 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]) + ])
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, '<p>$var</p>') + 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()
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('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:for="item in items">${item}</li> + ... </ul>''') + >>> print tmpl.generate(items=[1, 2, 3]) + <ul> + <li>1</li><li>2</li><li>3</li> + </ul> + """ + 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
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)
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')
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')
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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <elem py:for="item in items" py:attrs="item"/> + </doc>""") + items = [{'id': 1, 'class': 'foo'}, {'id': 2, 'class': 'bar'}] + self.assertEqual("""<doc> + <elem id="1" class="foo"/><elem id="2" class="bar"/> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <elem class="foo" py:attrs="{'class': 'bar'}"/> + </doc>""") + self.assertEqual("""<doc> + <elem class="bar"/> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <elem class="foo" py:attrs="{'class': None}"/> + </doc>""") + self.assertEqual("""<doc> + <elem/> + </doc>""", 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("""<div xmlns:py="http://genshi.edgewall.org/" py:choose=""> + <span py:when="1 == 1">1</span> + <span py:when="2 == 2">2</span> + <span py:when="3 == 3">3</span> + </div>""") + self.assertEqual("""<div> + <span>1</span> + </div>""", str(tmpl.generate())) + + def test_otherwise(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/" py:choose=""> + <span py:when="False">hidden</span> + <span py:otherwise="">hello</span> + </div>""") + self.assertEqual("""<div> + <span>hello</span> + </div>""", str(tmpl.generate())) + + def test_nesting(self): + """ + Verify that `py:choose` blocks can be nested: + """ + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:choose="1"> + <div py:when="1" py:choose="3"> + <span py:when="2">2</span> + <span py:when="3">3</span> + </div> + </div> + </doc>""") + self.assertEqual("""<doc> + <div> + <div> + <span>3</span> + </div> + </div> + </doc>""", str(tmpl.generate())) + + def test_complex_nesting(self): + """ + Verify more complex nesting. + """ + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:choose="1"> + <div py:when="1" py:choose=""> + <span py:when="2">OK</span> + <span py:when="1">FAIL</span> + </div> + </div> + </doc>""") + self.assertEqual("""<doc> + <div> + <div> + <span>OK</span> + </div> + </div> + </doc>""", str(tmpl.generate())) + + def test_complex_nesting_otherwise(self): + """ + Verify more complex nesting using otherwise. + """ + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:choose="1"> + <div py:when="1" py:choose="2"> + <span py:when="1">FAIL</span> + <span py:otherwise="">OK</span> + </div> + </div> + </doc>""") + self.assertEqual("""<doc> + <div> + <div> + <span>OK</span> + </div> + </div> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:choose="" py:strip=""> + <span py:otherwise="">foo</span> + </div> + </doc>""") + self.assertEqual("""<doc> + <span>foo</span> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:when="xy" /> + </doc>""") + 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:otherwise="" /> + </doc>""") + 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:choose="" py:strip=""> + <py:when>foo</py:when> + </div> + </doc>""") + 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:choose="foo" py:strip=""> + <py:when>foo</py:when> + </div> + </doc>""") + self.assertEqual("""<doc> + foo + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:choose="" py:strip=""> + <py:otherwise>foo</py:otherwise> + </div> + </doc>""") + self.assertEqual("""<doc> + foo + </doc>""", str(tmpl.generate())) + + def test_as_element(self): + """ + Verify that the directive can also be used as an element. + """ + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:choose> + <py:when test="1 == 1">1</py:when> + <py:when test="2 == 2">2</py:when> + <py:when test="3 == 3">3</py:when> + </py:choose> + </doc>""") + self.assertEqual("""<doc> + 1 + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:def="echo(what)" py:strip=""> + <b>${what}</b> + </div> + ${echo('foo')} + </doc>""") + self.assertEqual("""<doc> + <b>foo</b> + </doc>""", str(tmpl.generate())) + + def test_exec_in_replace(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <p py:def="echo(greeting, name='world')" class="message"> + ${greeting}, ${name}! + </p> + <div py:replace="echo('hello')"></div> + </div>""") + self.assertEqual("""<div> + <p class="message"> + hello, world! + </p> + </div>""", str(tmpl.generate())) + + def test_as_element(self): + """ + Verify that the directive can also be used as an element. + """ + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:def function="echo(what)"> + <b>${what}</b> + </py:def> + ${echo('foo')} + </doc>""") + self.assertEqual("""<doc> + <b>foo</b> + </doc>""", str(tmpl.generate())) + + def test_nested_defs(self): + """ + Verify that a template function defined inside a conditional block can + be called from outside that block. + """ + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:if test="semantic"> + <strong py:def="echo(what)">${what}</strong> + </py:if> + <py:if test="not semantic"> + <b py:def="echo(what)">${what}</b> + </py:if> + ${echo('foo')} + </doc>""") + self.assertEqual("""<doc> + <strong>foo</strong> + </doc>""", str(tmpl.generate(semantic=True))) + + def test_function_with_default_arg(self): + """ + Verify that keyword arguments work with `py:def` directives. + """ + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <b py:def="echo(what, bold=False)" py:strip="not bold">${what}</b> + ${echo('foo')} + </doc>""") + self.assertEqual("""<doc> + foo + </doc>""", str(tmpl.generate())) + + def test_invocation_in_attribute(self): + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:def function="echo(what)">${what or 'something'}</py:def> + <p class="${echo('foo')}">bar</p> + </doc>""") + self.assertEqual("""<doc> + <p class="foo">bar</p> + </doc>""", str(tmpl.generate())) + + def test_invocation_in_attribute_none(self): + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:def function="echo()">${None}</py:def> + <p class="${echo()}">bar</p> + </doc>""") + self.assertEqual("""<doc> + <p>bar</p> + </doc>""", str(tmpl.generate())) + + def test_function_raising_typeerror(self): + def badfunc(): + raise TypeError + tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> + <div py:def="dobadfunc()"> + ${badfunc()} + </div> + <div py:content="dobadfunc()"/> + </html>""") + self.assertRaises(TypeError, list, tmpl.generate(badfunc=badfunc)) + + def test_def_in_matched(self): + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <head py:match="head">${select('*')}</head> + <head> + <py:def function="maketitle(test)"><b py:replace="test" /></py:def> + <title>${maketitle(True)}</title> + </head> + </doc>""") + self.assertEqual("""<doc> + <head><title>True</title></head> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:for="item in items" py:strip=""> + <b>${item}</b> + </div> + </doc>""") + self.assertEqual("""<doc> + <b>1</b> + <b>2</b> + <b>3</b> + <b>4</b> + <b>5</b> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:for each="item in items"> + <b>${item}</b> + </py:for> + </doc>""") + self.assertEqual("""<doc> + <b>1</b> + <b>2</b> + <b>3</b> + <b>4</b> + <b>5</b> + </doc>""", str(tmpl.generate(items=range(1, 6)))) + + def test_multi_assignment(self): + """ + Verify that assignment to tuples works correctly. + """ + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:for each="k, v in items"> + <p>key=$k, value=$v</p> + </py:for> + </doc>""") + self.assertEqual("""<doc> + <p>key=a, value=1</p> + <p>key=b, value=2</p> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:for each="idx, (k, v) in items"> + <p>$idx: key=$k, value=$v</p> + </py:for> + </doc>""") + self.assertEqual("""<doc> + <p>0: key=a, value=1</p> + <p>1: key=b, value=2</p> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:for each="item in foo"> + $item + </py:for> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <b py:if="foo" py:strip="">${bar}</b> + </doc>""") + self.assertEqual("""<doc> + Hello + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:if test="foo">${bar}</py:if> + </doc>""") + self.assertEqual("""<doc> + Hello + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <elem py:match="elem" py:strip=""> + <div class="elem">${select('text()')}</div> + </elem> + <elem>Hey Joe</elem> + </doc>""") + self.assertEqual("""<doc> + <div class="elem">Hey Joe</div> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <elem py:match="elem"> + <div class="elem">${select('text()')}</div> + </elem> + <elem>Hey Joe</elem> + </doc>""") + self.assertEqual("""<doc> + <elem> + <div class="elem">Hey Joe</div> + </elem> + </doc>""", str(tmpl.generate())) + + def test_as_element(self): + """ + Verify that the directive can also be used as an element. + """ + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:match path="elem"> + <div class="elem">${select('text()')}</div> + </py:match> + <elem>Hey Joe</elem> + </doc>""") + self.assertEqual("""<doc> + <div class="elem">Hey Joe</div> + </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> + <elem py:match="elem"> + <div class="elem"> + ${select('*')} + </div> + </elem> + <elem> + <subelem> + <elem/> + </subelem> + </elem> + </doc>""") + self.assertEqual("""<doc> + <elem> + <div class="elem"> + <subelem> + <elem> + <div class="elem"> + </div> + </elem> + </subelem> + </div> + </elem> + </doc>""", 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("""<html xmlns:py="http://genshi.edgewall.org/"> + <body py:match="body"> + <div id="header"/> + ${select('*')} + </body> + <body py:match="body"> + ${select('*')} + <div id="footer"/> + </body> + <body> + <h1>Foo</h1> + </body> + </html>""") + self.assertEqual("""<html> + <body> + <div id="header"/><h1>Foo</h1> + <div id="footer"/> + </body> + </html>""", str(tmpl.generate())) + + def test_select_all_attrs(self): + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:match="elem" py:attrs="select('@*')"> + ${select('text()')} + </div> + <elem id="joe">Hey Joe</elem> + </doc>""") + self.assertEqual("""<doc> + <div id="joe"> + Hey Joe + </div> + </doc>""", str(tmpl.generate())) + + def test_select_all_attrs_empty(self): + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:match="elem" py:attrs="select('@*')"> + ${select('text()')} + </div> + <elem>Hey Joe</elem> + </doc>""") + self.assertEqual("""<doc> + <div> + Hey Joe + </div> + </doc>""", str(tmpl.generate())) + + def test_select_all_attrs_in_body(self): + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <div py:match="elem"> + Hey ${select('text()')} ${select('@*')} + </div> + <elem title="Cool">Joe</elem> + </doc>""") + self.assertEqual("""<doc> + <div> + Hey Joe Cool + </div> + </doc>""", str(tmpl.generate())) + + def test_def_in_match(self): + tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> + <py:def function="maketitle(test)"><b py:replace="test" /></py:def> + <head py:match="head">${select('*')}</head> + <head><title>${maketitle(True)}</title></head> + </doc>""") + self.assertEqual("""<doc> + <head><title>True</title></head> + </doc>""", str(tmpl.generate())) + + def test_match_with_xpath_variable(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <span py:match="*[name()=$tagname]"> + Hello ${select('@name')} + </span> + <greeting name="Dude"/> + </div>""") + self.assertEqual("""<div> + <span> + Hello Dude + </span> + </div>""", str(tmpl.generate(tagname='greeting'))) + self.assertEqual("""<div> + <greeting name="Dude"/> + </div>""", str(tmpl.generate(tagname='sayhello'))) + + def test_content_directive_in_match(self): + tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> + <div py:match="foo">I said <q py:content="select('text()')">something</q>.</div> + <foo>bar</foo> + </html>""") + self.assertEqual("""<html> + <div>I said <q>bar</q>.</div> + </html>""", str(tmpl.generate())) + + def test_cascaded_matches(self): + tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> + <body py:match="body">${select('*')}</body> + <head py:match="head">${select('title')}</head> + <body py:match="body">${select('*')}<hr /></body> + <head><title>Welcome to Markup</title></head> + <body><h2>Are you ready to mark up?</h2></body> + </html>""") + self.assertEqual("""<html> + <head><title>Welcome to Markup</title></head> + <body><h2>Are you ready to mark up?</h2><hr/></body> + </html>""", str(tmpl.generate())) + + def test_multiple_matches(self): + tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> + <input py:match="form//input" py:attrs="select('@*')" + value="${values[str(select('@name'))]}" /> + <form><p py:for="field in fields"> + <label>${field.capitalize()}</label> + <input type="text" name="${field}" /> + </p></form> + </html>""") + fields = ['hello_%s' % i for i in range(5)] + values = dict([('hello_%s' % i, i) for i in range(5)]) + self.assertEqual("""<html> + <form><p> + <label>Hello_0</label> + <input value="0" type="text" name="hello_0"/> + </p><p> + <label>Hello_1</label> + <input value="1" type="text" name="hello_1"/> + </p><p> + <label>Hello_2</label> + <input value="2" type="text" name="hello_2"/> + </p><p> + <label>Hello_3</label> + <input value="3" type="text" name="hello_3"/> + </p><p> + <label>Hello_4</label> + <input value="4" type="text" name="hello_4"/> + </p></form> + </html>""", str(tmpl.generate(fields=fields, values=values))) + + def test_namespace_context(self): + tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" + xmlns:x="http://www.example.org/"> + <div py:match="x:foo">Foo</div> + <foo xmlns="http://www.example.org/"/> + </html>""") + # FIXME: there should be a way to strip out unwanted/unused namespaces, + # such as the "x" in this example + self.assertEqual("""<html xmlns:x="http://www.example.org/"> + <div>Foo</div> + </html>""", str(tmpl.generate())) + + def test_match_with_position_predicate(self): + tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> + <p py:match="body/p[1]" class="first">${select('*|text()')}</p> + <body> + <p>Foo</p> + <p>Bar</p> + </body> + </html>""") + self.assertEqual("""<html> + <body> + <p class="first">Foo</p> + <p>Bar</p> + </body> + </html>""", str(tmpl.generate())) + + def test_match_with_closure(self): + tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> + <p py:match="body//p" class="para">${select('*|text()')}</p> + <body> + <p>Foo</p> + <div><p>Bar</p></div> + </body> + </html>""") + self.assertEqual("""<html> + <body> + <p class="para">Foo</p> + <div><p class="para">Bar</p></div> + </body> + </html>""", str(tmpl.generate())) + + def test_match_without_closure(self): + tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> + <p py:match="body/p" class="para">${select('*|text()')}</p> + <body> + <p>Foo</p> + <div><p>Bar</p></div> + </body> + </html>""") + self.assertEqual("""<html> + <body> + <p class="para">Foo</p> + <div><p>Bar</p></div> + </body> + </html>""", str(tmpl.generate())) + + # FIXME + #def test_match_after_step(self): + # tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + # <span py:match="div/greeting"> + # Hello ${select('@name')} + # </span> + # <greeting name="Dude" /> + # </div>""") + # self.assertEqual("""<div> + # <span> + # Hello Dude + # </span> + # </div>""", str(tmpl.generate())) + + +class StripDirectiveTestCase(unittest.TestCase): + """Tests for the `py:strip` template directive.""" + + def test_strip_false(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <div py:strip="False"><b>foo</b></div> + </div>""") + self.assertEqual("""<div> + <div><b>foo</b></div> + </div>""", str(tmpl.generate())) + + def test_strip_empty(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <div py:strip=""><b>foo</b></div> + </div>""") + self.assertEqual("""<div> + <b>foo</b> + </div>""", str(tmpl.generate())) + + +class WithDirectiveTestCase(unittest.TestCase): + """Tests for the `py:with` template directive.""" + + def test_shadowing(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + ${x} + <span py:with="x = x * 2" py:replace="x"/> + ${x} + </div>""") + self.assertEqual("""<div> + 42 + 84 + 42 + </div>""", str(tmpl.generate(x=42))) + + def test_as_element(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <py:with vars="x = x * 2">${x}</py:with> + </div>""") + self.assertEqual("""<div> + 84 + </div>""", str(tmpl.generate(x=42))) + + def test_multiple_vars_same_name(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <py:with vars=" + foo = 'bar'; + foo = foo.replace('r', 'z') + "> + $foo + </py:with> + </div>""") + self.assertEqual("""<div> + baz + </div>""", str(tmpl.generate(x=42))) + + def test_multiple_vars_single_assignment(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <py:with vars="x = y = z = 1">${x} ${y} ${z}</py:with> + </div>""") + self.assertEqual("""<div> + 1 1 1 + </div>""", str(tmpl.generate(x=42))) + + def test_nested_vars_single_assignment(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <py:with vars="x, (y, z) = (1, (2, 3))">${x} ${y} ${z}</py:with> + </div>""") + self.assertEqual("""<div> + 1 2 3 + </div>""", str(tmpl.generate(x=42))) + + def test_multiple_vars_trailing_semicolon(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <py:with vars="x = x * 2; y = x / 2;">${x} ${y}</py:with> + </div>""") + self.assertEqual("""<div> + 84 42 + </div>""", str(tmpl.generate(x=42))) + + def test_semicolon_escape(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <py:with vars="x = 'here is a semicolon: ;'; y = 'here are two semicolons: ;;' ;"> + ${x} + ${y} + </py:with> + </div>""") + self.assertEqual("""<div> + here is a semicolon: ; + here are two semicolons: ;; + </div>""", str(tmpl.generate())) + + def test_unicode_expr(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <span py:with="weeks=(u'一', u'二', u'三', u'四', u'五', u'六', u'日')"> + $weeks + </span> + </div>""") + self.assertEqual("""<div> + <span> + 一二三四五六日 + </span> + </div>""", str(tmpl.generate())) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(directives)) + suite.addTest(unittest.makeSuite(AttrsDirectiveTestCase, 'test')) + suite.addTest(unittest.makeSuite(ChooseDirectiveTestCase, 'test')) + suite.addTest(unittest.makeSuite(DefDirectiveTestCase, 'test')) + suite.addTest(unittest.makeSuite(ForDirectiveTestCase, 'test')) + suite.addTest(unittest.makeSuite(IfDirectiveTestCase, 'test')) + suite.addTest(unittest.makeSuite(MatchDirectiveTestCase, 'test')) + suite.addTest(unittest.makeSuite(StripDirectiveTestCase, 'test')) + suite.addTest(unittest.makeSuite(WithDirectiveTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/genshi/template/tests/eval.py @@ -0,0 +1,389 @@ +# -*- 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.eval import Expression, Undefined + + +class ExpressionTestCase(unittest.TestCase): + + def test_name_lookup(self): + self.assertEqual('bar', Expression('foo').evaluate({'foo': 'bar'})) + self.assertEqual(id, Expression('id').evaluate({}, nocall=True)) + self.assertEqual('bar', Expression('id').evaluate({'id': 'bar'})) + self.assertEqual(None, Expression('id').evaluate({'id': None}, + nocall=True)) + + def test_str_literal(self): + self.assertEqual('foo', Expression('"foo"').evaluate({})) + self.assertEqual('foo', Expression('"""foo"""').evaluate({})) + self.assertEqual('foo', Expression("'foo'").evaluate({})) + self.assertEqual('foo', Expression("'''foo'''").evaluate({})) + self.assertEqual('foo', Expression("u'foo'").evaluate({})) + self.assertEqual('foo', Expression("r'foo'").evaluate({})) + + def test_str_literal_non_ascii(self): + expr = Expression(u"u'\xfe'") + self.assertEqual(u'þ', expr.evaluate({})) + expr = Expression("u'\xfe'") + self.assertEqual(u'þ', expr.evaluate({})) + expr = Expression("'\xc3\xbe'") + self.assertEqual(u'þ', expr.evaluate({})) + + def test_num_literal(self): + self.assertEqual(42, Expression("42").evaluate({})) + self.assertEqual(42L, Expression("42L").evaluate({})) + self.assertEqual(.42, Expression(".42").evaluate({})) + self.assertEqual(07, Expression("07").evaluate({})) + self.assertEqual(0xF2, Expression("0xF2").evaluate({})) + self.assertEqual(0XF2, Expression("0XF2").evaluate({})) + + def test_dict_literal(self): + self.assertEqual({}, Expression("{}").evaluate({})) + self.assertEqual({'key': True}, + Expression("{'key': value}").evaluate({'value': True})) + + def test_list_literal(self): + self.assertEqual([], Expression("[]").evaluate({})) + self.assertEqual([1, 2, 3], Expression("[1, 2, 3]").evaluate({})) + self.assertEqual([True], + Expression("[value]").evaluate({'value': True})) + + def test_tuple_literal(self): + self.assertEqual((), Expression("()").evaluate({})) + self.assertEqual((1, 2, 3), Expression("(1, 2, 3)").evaluate({})) + self.assertEqual((True,), + Expression("(value,)").evaluate({'value': True})) + + def test_unaryop_pos(self): + self.assertEqual(1, Expression("+1").evaluate({})) + self.assertEqual(1, Expression("+x").evaluate({'x': 1})) + + def test_unaryop_neg(self): + self.assertEqual(-1, Expression("-1").evaluate({})) + self.assertEqual(-1, Expression("-x").evaluate({'x': 1})) + + def test_unaryop_not(self): + self.assertEqual(False, Expression("not True").evaluate({})) + self.assertEqual(False, Expression("not x").evaluate({'x': True})) + + def test_unaryop_inv(self): + self.assertEqual(-2, Expression("~1").evaluate({})) + self.assertEqual(-2, Expression("~x").evaluate({'x': 1})) + + def test_binop_add(self): + self.assertEqual(3, Expression("2 + 1").evaluate({})) + self.assertEqual(3, Expression("x + y").evaluate({'x': 2, 'y': 1})) + + def test_binop_sub(self): + self.assertEqual(1, Expression("2 - 1").evaluate({})) + self.assertEqual(1, Expression("x - y").evaluate({'x': 1, 'y': 1})) + + def test_binop_sub(self): + self.assertEqual(1, Expression("2 - 1").evaluate({})) + self.assertEqual(1, Expression("x - y").evaluate({'x': 2, 'y': 1})) + + def test_binop_mul(self): + self.assertEqual(4, Expression("2 * 2").evaluate({})) + self.assertEqual(4, Expression("x * y").evaluate({'x': 2, 'y': 2})) + + def test_binop_pow(self): + self.assertEqual(4, Expression("2 ** 2").evaluate({})) + self.assertEqual(4, Expression("x ** y").evaluate({'x': 2, 'y': 2})) + + def test_binop_div(self): + self.assertEqual(2, Expression("4 / 2").evaluate({})) + self.assertEqual(2, Expression("x / y").evaluate({'x': 4, 'y': 2})) + + def test_binop_floordiv(self): + self.assertEqual(1, Expression("3 // 2").evaluate({})) + self.assertEqual(1, Expression("x // y").evaluate({'x': 3, 'y': 2})) + + def test_binop_mod(self): + self.assertEqual(1, Expression("3 % 2").evaluate({})) + self.assertEqual(1, Expression("x % y").evaluate({'x': 3, 'y': 2})) + + def test_binop_and(self): + self.assertEqual(0, Expression("1 & 0").evaluate({})) + self.assertEqual(0, Expression("x & y").evaluate({'x': 1, 'y': 0})) + + def test_binop_or(self): + self.assertEqual(1, Expression("1 | 0").evaluate({})) + self.assertEqual(1, Expression("x | y").evaluate({'x': 1, 'y': 0})) + + def test_binop_contains(self): + self.assertEqual(True, Expression("1 in (1, 2, 3)").evaluate({})) + self.assertEqual(True, Expression("x in y").evaluate({'x': 1, + 'y': (1, 2, 3)})) + + def test_binop_not_contains(self): + self.assertEqual(True, Expression("4 not in (1, 2, 3)").evaluate({})) + self.assertEqual(True, Expression("x not in y").evaluate({'x': 4, + 'y': (1, 2, 3)})) + + def test_binop_is(self): + self.assertEqual(True, Expression("1 is 1").evaluate({})) + self.assertEqual(True, Expression("x is y").evaluate({'x': 1, 'y': 1})) + self.assertEqual(False, Expression("1 is 2").evaluate({})) + self.assertEqual(False, Expression("x is y").evaluate({'x': 1, 'y': 2})) + + def test_binop_is_not(self): + self.assertEqual(True, Expression("1 is not 2").evaluate({})) + self.assertEqual(True, Expression("x is not y").evaluate({'x': 1, + 'y': 2})) + self.assertEqual(False, Expression("1 is not 1").evaluate({})) + self.assertEqual(False, Expression("x is not y").evaluate({'x': 1, + 'y': 1})) + + def test_boolop_and(self): + self.assertEqual(False, Expression("True and False").evaluate({})) + self.assertEqual(False, Expression("x and y").evaluate({'x': True, + 'y': False})) + + def test_boolop_or(self): + self.assertEqual(True, Expression("True or False").evaluate({})) + self.assertEqual(True, Expression("x or y").evaluate({'x': True, + 'y': False})) + + def test_compare_eq(self): + self.assertEqual(True, Expression("1 == 1").evaluate({})) + self.assertEqual(True, Expression("x == y").evaluate({'x': 1, 'y': 1})) + + def test_compare_ne(self): + self.assertEqual(False, Expression("1 != 1").evaluate({})) + self.assertEqual(False, Expression("x != y").evaluate({'x': 1, 'y': 1})) + self.assertEqual(False, Expression("1 <> 1").evaluate({})) + self.assertEqual(False, Expression("x <> y").evaluate({'x': 1, 'y': 1})) + + def test_compare_lt(self): + self.assertEqual(True, Expression("1 < 2").evaluate({})) + self.assertEqual(True, Expression("x < y").evaluate({'x': 1, 'y': 2})) + + def test_compare_le(self): + self.assertEqual(True, Expression("1 <= 1").evaluate({})) + self.assertEqual(True, Expression("x <= y").evaluate({'x': 1, 'y': 1})) + + def test_compare_gt(self): + self.assertEqual(True, Expression("2 > 1").evaluate({})) + self.assertEqual(True, Expression("x > y").evaluate({'x': 2, 'y': 1})) + + def test_compare_ge(self): + self.assertEqual(True, Expression("1 >= 1").evaluate({})) + self.assertEqual(True, Expression("x >= y").evaluate({'x': 1, 'y': 1})) + + def test_compare_multi(self): + self.assertEqual(True, Expression("1 != 3 == 3").evaluate({})) + self.assertEqual(True, Expression("x != y == y").evaluate({'x': 1, + 'y': 3})) + + def test_call_function(self): + self.assertEqual(42, Expression("foo()").evaluate({'foo': lambda: 42})) + data = {'foo': 'bar'} + self.assertEqual('BAR', Expression("foo.upper()").evaluate(data)) + data = {'foo': {'bar': range(42)}} + self.assertEqual(42, Expression("len(foo.bar)").evaluate(data)) + + def test_call_keywords(self): + self.assertEqual(42, Expression("foo(x=bar)").evaluate({'foo': lambda x: x, + 'bar': 42})) + + def test_call_star_args(self): + self.assertEqual(42, Expression("foo(*bar)").evaluate({'foo': lambda x: x, + 'bar': [42]})) + + def test_call_dstar_args(self): + def foo(x): + return x + self.assertEqual(42, Expression("foo(**bar)").evaluate({'foo': foo, + 'bar': {"x": 42}})) + + def test_call_function_without_params(self): + self.assertEqual(42, Expression("foo").evaluate({'foo': lambda: 42})) + data = {'foo': 'bar'} + self.assertEqual('BAR', Expression("foo.upper").evaluate(data)) + data = {'foo': {'bar': range(42)}} + + def test_lambda(self): + # Define a custom `sorted` function cause the builtin isn't available + # on Python 2.3 + def sorted(items, compfunc): + items.sort(compfunc) + return items + data = {'items': [{'name': 'b', 'value': 0}, {'name': 'a', 'value': 1}], + 'sorted': sorted} + expr = Expression("sorted(items, lambda a, b: cmp(a.name, b.name))") + self.assertEqual([{'name': 'a', 'value': 1}, {'name': 'b', 'value': 0}], + expr.evaluate(data)) + + def test_list_comprehension(self): + expr = Expression("[n for n in numbers if n < 2]") + self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)})) + + expr = Expression("[(i, n + 1) for i, n in enumerate(numbers)]") + self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], + expr.evaluate({'numbers': range(5)})) + + expr = Expression("[offset + n for n in numbers]") + self.assertEqual([2, 3, 4, 5, 6], + expr.evaluate({'numbers': range(5), 'offset': 2})) + + def test_list_comprehension_with_getattr(self): + items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] + expr = Expression("[i.name for i in items if i.value > 1]") + self.assertEqual(['b'], expr.evaluate({'items': items})) + + def test_list_comprehension_with_getitem(self): + items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] + expr = Expression("[i['name'] for i in items if i['value'] > 1]") + self.assertEqual(['b'], expr.evaluate({'items': items})) + + if sys.version_info >= (2, 4): + # Generator expressions only supported in Python 2.4 and up + + def test_generator_expression(self): + expr = Expression("list(n for n in numbers if n < 2)") + self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)})) + + expr = Expression("list((i, n + 1) for i, n in enumerate(numbers))") + self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], + expr.evaluate({'numbers': range(5)})) + + expr = Expression("list(offset + n for n in numbers)") + self.assertEqual([2, 3, 4, 5, 6], + expr.evaluate({'numbers': range(5), 'offset': 2})) + + def test_generator_expression_with_getattr(self): + items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] + expr = Expression("list(i.name for i in items if i.value > 1)") + self.assertEqual(['b'], expr.evaluate({'items': items})) + + def test_generator_expression_with_getitem(self): + items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] + expr = Expression("list(i['name'] for i in items if i['value'] > 1)") + self.assertEqual(['b'], expr.evaluate({'items': items})) + + def test_slice(self): + expr = Expression("numbers[0:2]") + self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)})) + + def test_slice_with_vars(self): + expr = Expression("numbers[start:end]") + self.assertEqual([0, 1], expr.evaluate({'numbers': range(5), 'start': 0, + 'end': 2})) + + def test_slice_copy(self): + expr = Expression("numbers[:]") + self.assertEqual([0, 1, 2, 3, 4], expr.evaluate({'numbers': range(5)})) + + def test_slice_stride(self): + expr = Expression("numbers[::stride]") + self.assertEqual([0, 2, 4], expr.evaluate({'numbers': range(5), + 'stride': 2})) + + def test_slice_negative_start(self): + expr = Expression("numbers[-1:]") + self.assertEqual([4], expr.evaluate({'numbers': range(5)})) + + def test_slice_negative_end(self): + expr = Expression("numbers[:-1]") + self.assertEqual([0, 1, 2, 3], expr.evaluate({'numbers': range(5)})) + + def test_error_access_undefined(self): + expr = Expression("nothing", filename='index.html', lineno=50) + self.assertEqual(Undefined, type(expr.evaluate({}))) + + def test_error_call_undefined(self): + expr = Expression("nothing()", filename='index.html', lineno=50) + try: + expr.evaluate({}) + self.fail('Expected NameError') + except NameError, e: + exc_type, exc_value, exc_traceback = sys.exc_info() + frame = exc_traceback.tb_next + frames = [] + while frame.tb_next: + frame = frame.tb_next + frames.append(frame) + self.assertEqual('Variable "nothing" is not defined', str(e)) + self.assertEqual('<Expression "nothing()">', + frames[-3].tb_frame.f_code.co_name) + self.assertEqual('index.html', + frames[-3].tb_frame.f_code.co_filename) + self.assertEqual(50, frames[-3].tb_lineno) + + def test_error_getattr_undefined(self): + expr = Expression("nothing.nil", filename='index.html', lineno=50) + try: + expr.evaluate({}) + self.fail('Expected NameError') + except NameError, e: + exc_type, exc_value, exc_traceback = sys.exc_info() + frame = exc_traceback.tb_next + frames = [] + while frame.tb_next: + frame = frame.tb_next + frames.append(frame) + self.assertEqual('Variable "nothing" is not defined', str(e)) + self.assertEqual('<Expression "nothing.nil">', + frames[-3].tb_frame.f_code.co_name) + self.assertEqual('index.html', + frames[-3].tb_frame.f_code.co_filename) + self.assertEqual(50, frames[-3].tb_lineno) + + def test_error_getitem_undefined(self): + expr = Expression("nothing[0]", filename='index.html', lineno=50) + try: + expr.evaluate({}) + self.fail('Expected NameError') + except NameError, e: + exc_type, exc_value, exc_traceback = sys.exc_info() + frame = exc_traceback.tb_next + frames = [] + while frame.tb_next: + frame = frame.tb_next + frames.append(frame) + self.assertEqual('Variable "nothing" is not defined', str(e)) + self.assertEqual('<Expression "nothing[0]">', + frames[-3].tb_frame.f_code.co_name) + self.assertEqual('index.html', + frames[-3].tb_frame.f_code.co_filename) + self.assertEqual(50, frames[-3].tb_lineno) + + def test_error_getattr_nested_undefined(self): + expr = Expression("nothing.nil", filename='index.html', lineno=50) + val = expr.evaluate({'nothing': object()}) + assert isinstance(val, Undefined) + self.assertEqual("nil", val._name) + + def test_error_getitem_nested_undefined_string(self): + expr = Expression("nothing['bla']", filename='index.html', lineno=50) + val = expr.evaluate({'nothing': object()}) + assert isinstance(val, Undefined) + self.assertEqual("bla", val._name) + + def test_error_getitem_nested_undefined_int(self): + expr = Expression("nothing[0]", filename='index.html', lineno=50) + self.assertRaises(TypeError, expr.evaluate, {'nothing': object()}) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(Expression.__module__)) + suite.addTest(unittest.makeSuite(ExpressionTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/genshi/template/tests/loader.py @@ -0,0 +1,200 @@ +# -*- 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 os +import shutil +import tempfile +import unittest + +from genshi.template.loader import TemplateLoader +from genshi.template.markup import MarkupTemplate + + +class TemplateLoaderTestCase(unittest.TestCase): + """Tests for the template loader.""" + + def setUp(self): + self.dirname = tempfile.mkdtemp(suffix='markup_test') + + def tearDown(self): + shutil.rmtree(self.dirname) + + def test_search_path_empty(self): + loader = TemplateLoader() + self.assertEqual([], loader.search_path) + + def test_search_path_as_string(self): + loader = TemplateLoader(self.dirname) + self.assertEqual([self.dirname], loader.search_path) + + def test_relative_include_samedir(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""<div>Included</div>""") + finally: + file1.close() + + file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w') + try: + file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> + <xi:include href="tmpl1.html" /> + </html>""") + finally: + file2.close() + + loader = TemplateLoader([self.dirname]) + tmpl = loader.load('tmpl2.html') + self.assertEqual("""<html> + <div>Included</div> + </html>""", tmpl.generate().render()) + + def test_relative_include_subdir(self): + os.mkdir(os.path.join(self.dirname, 'sub')) + file1 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w') + try: + file1.write("""<div>Included</div>""") + finally: + file1.close() + + file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w') + try: + file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> + <xi:include href="sub/tmpl1.html" /> + </html>""") + finally: + file2.close() + + loader = TemplateLoader([self.dirname]) + tmpl = loader.load('tmpl2.html') + self.assertEqual("""<html> + <div>Included</div> + </html>""", tmpl.generate().render()) + + def test_relative_include_parentdir(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""<div>Included</div>""") + finally: + file1.close() + + os.mkdir(os.path.join(self.dirname, 'sub')) + file2 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w') + try: + file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> + <xi:include href="../tmpl1.html" /> + </html>""") + finally: + file2.close() + + loader = TemplateLoader([self.dirname]) + tmpl = loader.load('sub/tmpl2.html') + self.assertEqual("""<html> + <div>Included</div> + </html>""", tmpl.generate().render()) + + def test_relative_include_without_search_path(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""<div>Included</div>""") + finally: + file1.close() + + file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w') + try: + file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> + <xi:include href="tmpl1.html" /> + </html>""") + finally: + file2.close() + + loader = TemplateLoader() + tmpl = loader.load(os.path.join(self.dirname, 'tmpl2.html')) + self.assertEqual("""<html> + <div>Included</div> + </html>""", tmpl.generate().render()) + + def test_relative_include_without_search_path_nested(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""<div>Included</div>""") + finally: + file1.close() + + file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w') + try: + file2.write("""<div xmlns:xi="http://www.w3.org/2001/XInclude"> + <xi:include href="tmpl1.html" /> + </div>""") + finally: + file2.close() + + file3 = open(os.path.join(self.dirname, 'tmpl3.html'), 'w') + try: + file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> + <xi:include href="tmpl2.html" /> + </html>""") + finally: + file3.close() + + loader = TemplateLoader() + tmpl = loader.load(os.path.join(self.dirname, 'tmpl3.html')) + self.assertEqual("""<html> + <div> + <div>Included</div> + </div> + </html>""", tmpl.generate().render()) + + def test_relative_include_from_inmemory_template(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""<div>Included</div>""") + finally: + file1.close() + + loader = TemplateLoader([self.dirname]) + tmpl2 = MarkupTemplate("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> + <xi:include href="../tmpl1.html" /> + </html>""", filename='subdir/tmpl2.html', loader=loader) + + self.assertEqual("""<html> + <div>Included</div> + </html>""", tmpl2.generate().render()) + + def test_load_with_default_encoding(self): + f = open(os.path.join(self.dirname, 'tmpl.html'), 'w') + try: + f.write(u'<div>\xf6</div>'.encode('iso-8859-1')) + finally: + f.close() + loader = TemplateLoader([self.dirname], default_encoding='iso-8859-1') + loader.load('tmpl.html') + + def test_load_with_explicit_encoding(self): + f = open(os.path.join(self.dirname, 'tmpl.html'), 'w') + try: + f.write(u'<div>\xf6</div>'.encode('iso-8859-1')) + finally: + f.close() + loader = TemplateLoader([self.dirname], default_encoding='utf-8') + loader.load('tmpl.html', encoding='iso-8859-1') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(TemplateLoader.__module__)) + suite.addTest(unittest.makeSuite(TemplateLoaderTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/genshi/template/tests/markup.py @@ -0,0 +1,190 @@ +# -*- 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.core import Markup +from genshi.template.core import BadDirectiveError, TemplateSyntaxError +from genshi.template.markup import MarkupTemplate + + +class MarkupTemplateTestCase(unittest.TestCase): + """Tests for markup template processing.""" + + def test_interpolate_mixed3(self): + tmpl = MarkupTemplate('<root> ${var} $var</root>') + self.assertEqual('<root> 42 42</root>', str(tmpl.generate(var=42))) + + def test_interpolate_leading_trailing_space(self): + tmpl = MarkupTemplate('<root>${ foo }</root>') + self.assertEqual('<root>bar</root>', str(tmpl.generate(foo='bar'))) + + def test_interpolate_multiline(self): + tmpl = MarkupTemplate("""<root>${dict( + bar = 'baz' + )[foo]}</root>""") + self.assertEqual('<root>baz</root>', str(tmpl.generate(foo='bar'))) + + def test_interpolate_non_string_attrs(self): + tmpl = MarkupTemplate('<root attr="${1}"/>') + self.assertEqual('<root attr="1"/>', str(tmpl.generate())) + + def test_interpolate_list_result(self): + tmpl = MarkupTemplate('<root>$foo</root>') + self.assertEqual('<root>buzz</root>', str(tmpl.generate(foo=('buzz',)))) + + def test_empty_attr(self): + tmpl = MarkupTemplate('<root attr=""/>') + self.assertEqual('<root attr=""/>', str(tmpl.generate())) + + def test_bad_directive_error(self): + xml = '<p xmlns:py="http://genshi.edgewall.org/" py:do="nothing" />' + try: + tmpl = MarkupTemplate(xml, filename='test.html') + except BadDirectiveError, e: + self.assertEqual('test.html', e.filename) + if sys.version_info[:2] >= (2, 4): + self.assertEqual(1, e.lineno) + + def test_directive_value_syntax_error(self): + xml = """<p xmlns:py="http://genshi.edgewall.org/" py:if="bar'" />""" + try: + tmpl = MarkupTemplate(xml, filename='test.html') + self.fail('Expected SyntaxError') + except TemplateSyntaxError, e: + self.assertEqual('test.html', e.filename) + if sys.version_info[:2] >= (2, 4): + self.assertEqual(1, e.lineno) + + def test_expression_syntax_error(self): + xml = """<p> + Foo <em>${bar"}</em> + </p>""" + try: + tmpl = MarkupTemplate(xml, filename='test.html') + self.fail('Expected SyntaxError') + except TemplateSyntaxError, e: + self.assertEqual('test.html', e.filename) + if sys.version_info[:2] >= (2, 4): + self.assertEqual(2, e.lineno) + + def test_expression_syntax_error_multi_line(self): + xml = """<p><em></em> + + ${bar"} + + </p>""" + try: + tmpl = MarkupTemplate(xml, filename='test.html') + self.fail('Expected SyntaxError') + except TemplateSyntaxError, e: + self.assertEqual('test.html', e.filename) + if sys.version_info[:2] >= (2, 4): + self.assertEqual(3, e.lineno) + + def test_markup_noescape(self): + """ + Verify that outputting context data that is a `Markup` instance is not + escaped. + """ + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + $myvar + </div>""") + self.assertEqual("""<div> + <b>foo</b> + </div>""", str(tmpl.generate(myvar=Markup('<b>foo</b>')))) + + def test_text_noescape_quotes(self): + """ + Verify that outputting context data in text nodes doesn't escape quotes. + """ + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + $myvar + </div>""") + self.assertEqual("""<div> + "foo" + </div>""", str(tmpl.generate(myvar='"foo"'))) + + def test_attr_escape_quotes(self): + """ + Verify that outputting context data in attribtes escapes quotes. + """ + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <elem class="$myvar"/> + </div>""") + self.assertEqual("""<div> + <elem class=""foo""/> + </div>""", str(tmpl.generate(myvar='"foo"'))) + + def test_directive_element(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <py:if test="myvar">bar</py:if> + </div>""") + self.assertEqual("""<div> + bar + </div>""", str(tmpl.generate(myvar='"foo"'))) + + def test_normal_comment(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <!-- foo bar --> + </div>""") + self.assertEqual("""<div> + <!-- foo bar --> + </div>""", str(tmpl.generate())) + + def test_template_comment(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <!-- !foo --> + <!--!bar--> + </div>""") + self.assertEqual("""<div> + </div>""", str(tmpl.generate())) + + def test_parse_with_same_namespace_nested(self): + tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> + <span xmlns:py="http://genshi.edgewall.org/"> + </span> + </div>""") + self.assertEqual("""<div> + <span> + </span> + </div>""", str(tmpl.generate())) + + def test_latin1_encoded_with_xmldecl(self): + tmpl = MarkupTemplate(u"""<?xml version="1.0" encoding="iso-8859-1" ?> + <div xmlns:py="http://genshi.edgewall.org/"> + \xf6 + </div>""".encode('iso-8859-1'), encoding='iso-8859-1') + self.assertEqual(u"""<div> + \xf6 + </div>""", unicode(tmpl.generate())) + + def test_latin1_encoded_explicit_encoding(self): + tmpl = MarkupTemplate(u"""<div xmlns:py="http://genshi.edgewall.org/"> + \xf6 + </div>""".encode('iso-8859-1'), encoding='iso-8859-1') + self.assertEqual(u"""<div> + \xf6 + </div>""", unicode(tmpl.generate())) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(MarkupTemplate.__module__)) + suite.addTest(unittest.makeSuite(MarkupTemplateTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/genshi/template/tests/text.py @@ -0,0 +1,69 @@ +# -*- 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.template.text import TextTemplate + + +class TextTemplateTestCase(unittest.TestCase): + """Tests for text template processing.""" + + def test_escaping(self): + tmpl = TextTemplate('\\#escaped') + self.assertEqual('#escaped', str(tmpl.generate())) + + def test_comment(self): + tmpl = TextTemplate('## a comment') + self.assertEqual('', str(tmpl.generate())) + + def test_comment_escaping(self): + tmpl = TextTemplate('\\## escaped comment') + self.assertEqual('## escaped comment', str(tmpl.generate())) + + def test_end_with_args(self): + tmpl = TextTemplate(""" + #if foo + bar + #end 'if foo'""") + self.assertEqual('', str(tmpl.generate())) + + def test_latin1_encoded(self): + text = u'$foo\xf6$bar'.encode('iso-8859-1') + tmpl = TextTemplate(text, encoding='iso-8859-1') + self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y'))) + + # FIXME + #def test_empty_lines(self): + # tmpl = TextTemplate("""Your items: + # + # #for item in items + # * ${item} + # + # #end""") + # self.assertEqual("""Your items: + # * 0 + # * 1 + # * 2 + # """, tmpl.generate(items=range(3)).render('text')) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(TextTemplate.__module__)) + suite.addTest(unittest.makeSuite(TextTemplateTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite')
new file mode 100644 --- /dev/null +++ b/genshi/template/text.py @@ -0,0 +1,107 @@ +# -*- 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/. + +"""Plain text templating engine.""" + +import re + +from genshi.template.core import BadDirectiveError, Template, SUB +from genshi.template.directives import * + + +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, + <BLANKLINE> + We have the following items for you: + * 1 + * 2 + * 3 + <BLANKLINE> + 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*(?<!\\)#((?:\w+|#).*)\n?', re.MULTILINE) + + def _parse(self, encoding): + """Parse the template from text input.""" + stream = [] # list of events of the "compiled" template + dirmap = {} # temporary mapping of directives to elements + depth = 0 + if not encoding: + encoding = 'utf-8' + + source = self.source.read().decode(encoding, 'replace') + offset = 0 + lineno = 1 + + for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)): + start, end = mo.span() + if start > 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
--- a/genshi/tests/__init__.py +++ b/genshi/tests/__init__.py @@ -11,18 +11,17 @@ # 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(): import genshi - from genshi.tests import builder, core, eval, filters, input, output, \ - path, template, util + from genshi.tests import builder, core, filters, input, output, path, \ + util + from genshi.template import tests as template + suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(genshi)) suite.addTest(builder.suite()) suite.addTest(core.suite()) - suite.addTest(eval.suite()) suite.addTest(filters.suite()) suite.addTest(input.suite()) suite.addTest(output.suite())
deleted file mode 100644 --- a/genshi/tests/eval.py +++ /dev/null @@ -1,389 +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/. - -import doctest -import sys -import unittest - -from genshi.eval import Expression, Undefined - - -class ExpressionTestCase(unittest.TestCase): - - def test_name_lookup(self): - self.assertEqual('bar', Expression('foo').evaluate({'foo': 'bar'})) - self.assertEqual(id, Expression('id').evaluate({}, nocall=True)) - self.assertEqual('bar', Expression('id').evaluate({'id': 'bar'})) - self.assertEqual(None, Expression('id').evaluate({'id': None}, - nocall=True)) - - def test_str_literal(self): - self.assertEqual('foo', Expression('"foo"').evaluate({})) - self.assertEqual('foo', Expression('"""foo"""').evaluate({})) - self.assertEqual('foo', Expression("'foo'").evaluate({})) - self.assertEqual('foo', Expression("'''foo'''").evaluate({})) - self.assertEqual('foo', Expression("u'foo'").evaluate({})) - self.assertEqual('foo', Expression("r'foo'").evaluate({})) - - def test_str_literal_non_ascii(self): - expr = Expression(u"u'\xfe'") - self.assertEqual(u'þ', expr.evaluate({})) - expr = Expression("u'\xfe'") - self.assertEqual(u'þ', expr.evaluate({})) - expr = Expression("'\xc3\xbe'") - self.assertEqual(u'þ', expr.evaluate({})) - - def test_num_literal(self): - self.assertEqual(42, Expression("42").evaluate({})) - self.assertEqual(42L, Expression("42L").evaluate({})) - self.assertEqual(.42, Expression(".42").evaluate({})) - self.assertEqual(07, Expression("07").evaluate({})) - self.assertEqual(0xF2, Expression("0xF2").evaluate({})) - self.assertEqual(0XF2, Expression("0XF2").evaluate({})) - - def test_dict_literal(self): - self.assertEqual({}, Expression("{}").evaluate({})) - self.assertEqual({'key': True}, - Expression("{'key': value}").evaluate({'value': True})) - - def test_list_literal(self): - self.assertEqual([], Expression("[]").evaluate({})) - self.assertEqual([1, 2, 3], Expression("[1, 2, 3]").evaluate({})) - self.assertEqual([True], - Expression("[value]").evaluate({'value': True})) - - def test_tuple_literal(self): - self.assertEqual((), Expression("()").evaluate({})) - self.assertEqual((1, 2, 3), Expression("(1, 2, 3)").evaluate({})) - self.assertEqual((True,), - Expression("(value,)").evaluate({'value': True})) - - def test_unaryop_pos(self): - self.assertEqual(1, Expression("+1").evaluate({})) - self.assertEqual(1, Expression("+x").evaluate({'x': 1})) - - def test_unaryop_neg(self): - self.assertEqual(-1, Expression("-1").evaluate({})) - self.assertEqual(-1, Expression("-x").evaluate({'x': 1})) - - def test_unaryop_not(self): - self.assertEqual(False, Expression("not True").evaluate({})) - self.assertEqual(False, Expression("not x").evaluate({'x': True})) - - def test_unaryop_inv(self): - self.assertEqual(-2, Expression("~1").evaluate({})) - self.assertEqual(-2, Expression("~x").evaluate({'x': 1})) - - def test_binop_add(self): - self.assertEqual(3, Expression("2 + 1").evaluate({})) - self.assertEqual(3, Expression("x + y").evaluate({'x': 2, 'y': 1})) - - def test_binop_sub(self): - self.assertEqual(1, Expression("2 - 1").evaluate({})) - self.assertEqual(1, Expression("x - y").evaluate({'x': 1, 'y': 1})) - - def test_binop_sub(self): - self.assertEqual(1, Expression("2 - 1").evaluate({})) - self.assertEqual(1, Expression("x - y").evaluate({'x': 2, 'y': 1})) - - def test_binop_mul(self): - self.assertEqual(4, Expression("2 * 2").evaluate({})) - self.assertEqual(4, Expression("x * y").evaluate({'x': 2, 'y': 2})) - - def test_binop_pow(self): - self.assertEqual(4, Expression("2 ** 2").evaluate({})) - self.assertEqual(4, Expression("x ** y").evaluate({'x': 2, 'y': 2})) - - def test_binop_div(self): - self.assertEqual(2, Expression("4 / 2").evaluate({})) - self.assertEqual(2, Expression("x / y").evaluate({'x': 4, 'y': 2})) - - def test_binop_floordiv(self): - self.assertEqual(1, Expression("3 // 2").evaluate({})) - self.assertEqual(1, Expression("x // y").evaluate({'x': 3, 'y': 2})) - - def test_binop_mod(self): - self.assertEqual(1, Expression("3 % 2").evaluate({})) - self.assertEqual(1, Expression("x % y").evaluate({'x': 3, 'y': 2})) - - def test_binop_and(self): - self.assertEqual(0, Expression("1 & 0").evaluate({})) - self.assertEqual(0, Expression("x & y").evaluate({'x': 1, 'y': 0})) - - def test_binop_or(self): - self.assertEqual(1, Expression("1 | 0").evaluate({})) - self.assertEqual(1, Expression("x | y").evaluate({'x': 1, 'y': 0})) - - def test_binop_contains(self): - self.assertEqual(True, Expression("1 in (1, 2, 3)").evaluate({})) - self.assertEqual(True, Expression("x in y").evaluate({'x': 1, - 'y': (1, 2, 3)})) - - def test_binop_not_contains(self): - self.assertEqual(True, Expression("4 not in (1, 2, 3)").evaluate({})) - self.assertEqual(True, Expression("x not in y").evaluate({'x': 4, - 'y': (1, 2, 3)})) - - def test_binop_is(self): - self.assertEqual(True, Expression("1 is 1").evaluate({})) - self.assertEqual(True, Expression("x is y").evaluate({'x': 1, 'y': 1})) - self.assertEqual(False, Expression("1 is 2").evaluate({})) - self.assertEqual(False, Expression("x is y").evaluate({'x': 1, 'y': 2})) - - def test_binop_is_not(self): - self.assertEqual(True, Expression("1 is not 2").evaluate({})) - self.assertEqual(True, Expression("x is not y").evaluate({'x': 1, - 'y': 2})) - self.assertEqual(False, Expression("1 is not 1").evaluate({})) - self.assertEqual(False, Expression("x is not y").evaluate({'x': 1, - 'y': 1})) - - def test_boolop_and(self): - self.assertEqual(False, Expression("True and False").evaluate({})) - self.assertEqual(False, Expression("x and y").evaluate({'x': True, - 'y': False})) - - def test_boolop_or(self): - self.assertEqual(True, Expression("True or False").evaluate({})) - self.assertEqual(True, Expression("x or y").evaluate({'x': True, - 'y': False})) - - def test_compare_eq(self): - self.assertEqual(True, Expression("1 == 1").evaluate({})) - self.assertEqual(True, Expression("x == y").evaluate({'x': 1, 'y': 1})) - - def test_compare_ne(self): - self.assertEqual(False, Expression("1 != 1").evaluate({})) - self.assertEqual(False, Expression("x != y").evaluate({'x': 1, 'y': 1})) - self.assertEqual(False, Expression("1 <> 1").evaluate({})) - self.assertEqual(False, Expression("x <> y").evaluate({'x': 1, 'y': 1})) - - def test_compare_lt(self): - self.assertEqual(True, Expression("1 < 2").evaluate({})) - self.assertEqual(True, Expression("x < y").evaluate({'x': 1, 'y': 2})) - - def test_compare_le(self): - self.assertEqual(True, Expression("1 <= 1").evaluate({})) - self.assertEqual(True, Expression("x <= y").evaluate({'x': 1, 'y': 1})) - - def test_compare_gt(self): - self.assertEqual(True, Expression("2 > 1").evaluate({})) - self.assertEqual(True, Expression("x > y").evaluate({'x': 2, 'y': 1})) - - def test_compare_ge(self): - self.assertEqual(True, Expression("1 >= 1").evaluate({})) - self.assertEqual(True, Expression("x >= y").evaluate({'x': 1, 'y': 1})) - - def test_compare_multi(self): - self.assertEqual(True, Expression("1 != 3 == 3").evaluate({})) - self.assertEqual(True, Expression("x != y == y").evaluate({'x': 1, - 'y': 3})) - - def test_call_function(self): - self.assertEqual(42, Expression("foo()").evaluate({'foo': lambda: 42})) - data = {'foo': 'bar'} - self.assertEqual('BAR', Expression("foo.upper()").evaluate(data)) - data = {'foo': {'bar': range(42)}} - self.assertEqual(42, Expression("len(foo.bar)").evaluate(data)) - - def test_call_keywords(self): - self.assertEqual(42, Expression("foo(x=bar)").evaluate({'foo': lambda x: x, - 'bar': 42})) - - def test_call_star_args(self): - self.assertEqual(42, Expression("foo(*bar)").evaluate({'foo': lambda x: x, - 'bar': [42]})) - - def test_call_dstar_args(self): - def foo(x): - return x - self.assertEqual(42, Expression("foo(**bar)").evaluate({'foo': foo, - 'bar': {"x": 42}})) - - def test_call_function_without_params(self): - self.assertEqual(42, Expression("foo").evaluate({'foo': lambda: 42})) - data = {'foo': 'bar'} - self.assertEqual('BAR', Expression("foo.upper").evaluate(data)) - data = {'foo': {'bar': range(42)}} - - def test_lambda(self): - # Define a custom `sorted` function cause the builtin isn't available - # on Python 2.3 - def sorted(items, compfunc): - items.sort(compfunc) - return items - data = {'items': [{'name': 'b', 'value': 0}, {'name': 'a', 'value': 1}], - 'sorted': sorted} - expr = Expression("sorted(items, lambda a, b: cmp(a.name, b.name))") - self.assertEqual([{'name': 'a', 'value': 1}, {'name': 'b', 'value': 0}], - expr.evaluate(data)) - - def test_list_comprehension(self): - expr = Expression("[n for n in numbers if n < 2]") - self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)})) - - expr = Expression("[(i, n + 1) for i, n in enumerate(numbers)]") - self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], - expr.evaluate({'numbers': range(5)})) - - expr = Expression("[offset + n for n in numbers]") - self.assertEqual([2, 3, 4, 5, 6], - expr.evaluate({'numbers': range(5), 'offset': 2})) - - def test_list_comprehension_with_getattr(self): - items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] - expr = Expression("[i.name for i in items if i.value > 1]") - self.assertEqual(['b'], expr.evaluate({'items': items})) - - def test_list_comprehension_with_getitem(self): - items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] - expr = Expression("[i['name'] for i in items if i['value'] > 1]") - self.assertEqual(['b'], expr.evaluate({'items': items})) - - if sys.version_info >= (2, 4): - # Generator expressions only supported in Python 2.4 and up - - def test_generator_expression(self): - expr = Expression("list(n for n in numbers if n < 2)") - self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)})) - - expr = Expression("list((i, n + 1) for i, n in enumerate(numbers))") - self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], - expr.evaluate({'numbers': range(5)})) - - expr = Expression("list(offset + n for n in numbers)") - self.assertEqual([2, 3, 4, 5, 6], - expr.evaluate({'numbers': range(5), 'offset': 2})) - - def test_generator_expression_with_getattr(self): - items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] - expr = Expression("list(i.name for i in items if i.value > 1)") - self.assertEqual(['b'], expr.evaluate({'items': items})) - - def test_generator_expression_with_getitem(self): - items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] - expr = Expression("list(i['name'] for i in items if i['value'] > 1)") - self.assertEqual(['b'], expr.evaluate({'items': items})) - - def test_slice(self): - expr = Expression("numbers[0:2]") - self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)})) - - def test_slice_with_vars(self): - expr = Expression("numbers[start:end]") - self.assertEqual([0, 1], expr.evaluate({'numbers': range(5), 'start': 0, - 'end': 2})) - - def test_slice_copy(self): - expr = Expression("numbers[:]") - self.assertEqual([0, 1, 2, 3, 4], expr.evaluate({'numbers': range(5)})) - - def test_slice_stride(self): - expr = Expression("numbers[::stride]") - self.assertEqual([0, 2, 4], expr.evaluate({'numbers': range(5), - 'stride': 2})) - - def test_slice_negative_start(self): - expr = Expression("numbers[-1:]") - self.assertEqual([4], expr.evaluate({'numbers': range(5)})) - - def test_slice_negative_end(self): - expr = Expression("numbers[:-1]") - self.assertEqual([0, 1, 2, 3], expr.evaluate({'numbers': range(5)})) - - def test_error_access_undefined(self): - expr = Expression("nothing", filename='index.html', lineno=50) - self.assertEqual(Undefined, type(expr.evaluate({}))) - - def test_error_call_undefined(self): - expr = Expression("nothing()", filename='index.html', lineno=50) - try: - expr.evaluate({}) - self.fail('Expected NameError') - except NameError, e: - exc_type, exc_value, exc_traceback = sys.exc_info() - frame = exc_traceback.tb_next - frames = [] - while frame.tb_next: - frame = frame.tb_next - frames.append(frame) - self.assertEqual('Variable "nothing" is not defined', str(e)) - self.assertEqual('<Expression "nothing()">', - frames[-3].tb_frame.f_code.co_name) - self.assertEqual('index.html', - frames[-3].tb_frame.f_code.co_filename) - self.assertEqual(50, frames[-3].tb_lineno) - - def test_error_getattr_undefined(self): - expr = Expression("nothing.nil", filename='index.html', lineno=50) - try: - expr.evaluate({}) - self.fail('Expected NameError') - except NameError, e: - exc_type, exc_value, exc_traceback = sys.exc_info() - frame = exc_traceback.tb_next - frames = [] - while frame.tb_next: - frame = frame.tb_next - frames.append(frame) - self.assertEqual('Variable "nothing" is not defined', str(e)) - self.assertEqual('<Expression "nothing.nil">', - frames[-3].tb_frame.f_code.co_name) - self.assertEqual('index.html', - frames[-3].tb_frame.f_code.co_filename) - self.assertEqual(50, frames[-3].tb_lineno) - - def test_error_getitem_undefined(self): - expr = Expression("nothing[0]", filename='index.html', lineno=50) - try: - expr.evaluate({}) - self.fail('Expected NameError') - except NameError, e: - exc_type, exc_value, exc_traceback = sys.exc_info() - frame = exc_traceback.tb_next - frames = [] - while frame.tb_next: - frame = frame.tb_next - frames.append(frame) - self.assertEqual('Variable "nothing" is not defined', str(e)) - self.assertEqual('<Expression "nothing[0]">', - frames[-3].tb_frame.f_code.co_name) - self.assertEqual('index.html', - frames[-3].tb_frame.f_code.co_filename) - self.assertEqual(50, frames[-3].tb_lineno) - - def test_error_getattr_nested_undefined(self): - expr = Expression("nothing.nil", filename='index.html', lineno=50) - val = expr.evaluate({'nothing': object()}) - assert isinstance(val, Undefined) - self.assertEqual("nil", val._name) - - def test_error_getitem_nested_undefined_string(self): - expr = Expression("nothing['bla']", filename='index.html', lineno=50) - val = expr.evaluate({'nothing': object()}) - assert isinstance(val, Undefined) - self.assertEqual("bla", val._name) - - def test_error_getitem_nested_undefined_int(self): - expr = Expression("nothing[0]", filename='index.html', lineno=50) - self.assertRaises(TypeError, expr.evaluate, {'nothing': object()}) - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ExpressionTestCase, 'test')) - suite.addTest(doctest.DocTestSuite(Expression.__module__)) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite')
deleted file mode 100644 --- a/genshi/tests/template.py +++ /dev/null @@ -1,1377 +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/. - -import doctest -import os -import unittest -import shutil -import sys -import tempfile - -from genshi import template -from genshi.core import Markup, Stream -from genshi.template import BadDirectiveError, MarkupTemplate, Template, \ - TemplateLoader, TemplateRuntimeError, \ - TemplateSyntaxError, TextTemplate - - -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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <elem py:for="item in items" py:attrs="item"/> - </doc>""") - items = [{'id': 1, 'class': 'foo'}, {'id': 2, 'class': 'bar'}] - self.assertEqual("""<doc> - <elem id="1" class="foo"/><elem id="2" class="bar"/> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <elem class="foo" py:attrs="{'class': 'bar'}"/> - </doc>""") - self.assertEqual("""<doc> - <elem class="bar"/> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <elem class="foo" py:attrs="{'class': None}"/> - </doc>""") - self.assertEqual("""<doc> - <elem/> - </doc>""", 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("""<div xmlns:py="http://genshi.edgewall.org/" py:choose=""> - <span py:when="1 == 1">1</span> - <span py:when="2 == 2">2</span> - <span py:when="3 == 3">3</span> - </div>""") - self.assertEqual("""<div> - <span>1</span> - </div>""", str(tmpl.generate())) - - def test_otherwise(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/" py:choose=""> - <span py:when="False">hidden</span> - <span py:otherwise="">hello</span> - </div>""") - self.assertEqual("""<div> - <span>hello</span> - </div>""", str(tmpl.generate())) - - def test_nesting(self): - """ - Verify that `py:choose` blocks can be nested: - """ - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:choose="1"> - <div py:when="1" py:choose="3"> - <span py:when="2">2</span> - <span py:when="3">3</span> - </div> - </div> - </doc>""") - self.assertEqual("""<doc> - <div> - <div> - <span>3</span> - </div> - </div> - </doc>""", str(tmpl.generate())) - - def test_complex_nesting(self): - """ - Verify more complex nesting. - """ - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:choose="1"> - <div py:when="1" py:choose=""> - <span py:when="2">OK</span> - <span py:when="1">FAIL</span> - </div> - </div> - </doc>""") - self.assertEqual("""<doc> - <div> - <div> - <span>OK</span> - </div> - </div> - </doc>""", str(tmpl.generate())) - - def test_complex_nesting_otherwise(self): - """ - Verify more complex nesting using otherwise. - """ - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:choose="1"> - <div py:when="1" py:choose="2"> - <span py:when="1">FAIL</span> - <span py:otherwise="">OK</span> - </div> - </div> - </doc>""") - self.assertEqual("""<doc> - <div> - <div> - <span>OK</span> - </div> - </div> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:choose="" py:strip=""> - <span py:otherwise="">foo</span> - </div> - </doc>""") - self.assertEqual("""<doc> - <span>foo</span> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:when="xy" /> - </doc>""") - 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:otherwise="" /> - </doc>""") - 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:choose="" py:strip=""> - <py:when>foo</py:when> - </div> - </doc>""") - 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:choose="foo" py:strip=""> - <py:when>foo</py:when> - </div> - </doc>""") - self.assertEqual("""<doc> - foo - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:choose="" py:strip=""> - <py:otherwise>foo</py:otherwise> - </div> - </doc>""") - self.assertEqual("""<doc> - foo - </doc>""", str(tmpl.generate())) - - def test_as_element(self): - """ - Verify that the directive can also be used as an element. - """ - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:choose> - <py:when test="1 == 1">1</py:when> - <py:when test="2 == 2">2</py:when> - <py:when test="3 == 3">3</py:when> - </py:choose> - </doc>""") - self.assertEqual("""<doc> - 1 - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:def="echo(what)" py:strip=""> - <b>${what}</b> - </div> - ${echo('foo')} - </doc>""") - self.assertEqual("""<doc> - <b>foo</b> - </doc>""", str(tmpl.generate())) - - def test_exec_in_replace(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <p py:def="echo(greeting, name='world')" class="message"> - ${greeting}, ${name}! - </p> - <div py:replace="echo('hello')"></div> - </div>""") - self.assertEqual("""<div> - <p class="message"> - hello, world! - </p> - </div>""", str(tmpl.generate())) - - def test_as_element(self): - """ - Verify that the directive can also be used as an element. - """ - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:def function="echo(what)"> - <b>${what}</b> - </py:def> - ${echo('foo')} - </doc>""") - self.assertEqual("""<doc> - <b>foo</b> - </doc>""", str(tmpl.generate())) - - def test_nested_defs(self): - """ - Verify that a template function defined inside a conditional block can - be called from outside that block. - """ - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:if test="semantic"> - <strong py:def="echo(what)">${what}</strong> - </py:if> - <py:if test="not semantic"> - <b py:def="echo(what)">${what}</b> - </py:if> - ${echo('foo')} - </doc>""") - self.assertEqual("""<doc> - <strong>foo</strong> - </doc>""", str(tmpl.generate(semantic=True))) - - def test_function_with_default_arg(self): - """ - Verify that keyword arguments work with `py:def` directives. - """ - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <b py:def="echo(what, bold=False)" py:strip="not bold">${what}</b> - ${echo('foo')} - </doc>""") - self.assertEqual("""<doc> - foo - </doc>""", str(tmpl.generate())) - - def test_invocation_in_attribute(self): - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:def function="echo(what)">${what or 'something'}</py:def> - <p class="${echo('foo')}">bar</p> - </doc>""") - self.assertEqual("""<doc> - <p class="foo">bar</p> - </doc>""", str(tmpl.generate())) - - def test_invocation_in_attribute_none(self): - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:def function="echo()">${None}</py:def> - <p class="${echo()}">bar</p> - </doc>""") - self.assertEqual("""<doc> - <p>bar</p> - </doc>""", str(tmpl.generate())) - - def test_function_raising_typeerror(self): - def badfunc(): - raise TypeError - tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> - <div py:def="dobadfunc()"> - ${badfunc()} - </div> - <div py:content="dobadfunc()"/> - </html>""") - self.assertRaises(TypeError, list, tmpl.generate(badfunc=badfunc)) - - def test_def_in_matched(self): - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <head py:match="head">${select('*')}</head> - <head> - <py:def function="maketitle(test)"><b py:replace="test" /></py:def> - <title>${maketitle(True)}</title> - </head> - </doc>""") - self.assertEqual("""<doc> - <head><title>True</title></head> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:for="item in items" py:strip=""> - <b>${item}</b> - </div> - </doc>""") - self.assertEqual("""<doc> - <b>1</b> - <b>2</b> - <b>3</b> - <b>4</b> - <b>5</b> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:for each="item in items"> - <b>${item}</b> - </py:for> - </doc>""") - self.assertEqual("""<doc> - <b>1</b> - <b>2</b> - <b>3</b> - <b>4</b> - <b>5</b> - </doc>""", str(tmpl.generate(items=range(1, 6)))) - - def test_multi_assignment(self): - """ - Verify that assignment to tuples works correctly. - """ - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:for each="k, v in items"> - <p>key=$k, value=$v</p> - </py:for> - </doc>""") - self.assertEqual("""<doc> - <p>key=a, value=1</p> - <p>key=b, value=2</p> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:for each="idx, (k, v) in items"> - <p>$idx: key=$k, value=$v</p> - </py:for> - </doc>""") - self.assertEqual("""<doc> - <p>0: key=a, value=1</p> - <p>1: key=b, value=2</p> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:for each="item in foo"> - $item - </py:for> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <b py:if="foo" py:strip="">${bar}</b> - </doc>""") - self.assertEqual("""<doc> - Hello - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:if test="foo">${bar}</py:if> - </doc>""") - self.assertEqual("""<doc> - Hello - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <elem py:match="elem" py:strip=""> - <div class="elem">${select('text()')}</div> - </elem> - <elem>Hey Joe</elem> - </doc>""") - self.assertEqual("""<doc> - <div class="elem">Hey Joe</div> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <elem py:match="elem"> - <div class="elem">${select('text()')}</div> - </elem> - <elem>Hey Joe</elem> - </doc>""") - self.assertEqual("""<doc> - <elem> - <div class="elem">Hey Joe</div> - </elem> - </doc>""", str(tmpl.generate())) - - def test_as_element(self): - """ - Verify that the directive can also be used as an element. - """ - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:match path="elem"> - <div class="elem">${select('text()')}</div> - </py:match> - <elem>Hey Joe</elem> - </doc>""") - self.assertEqual("""<doc> - <div class="elem">Hey Joe</div> - </doc>""", 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("""<doc xmlns:py="http://genshi.edgewall.org/"> - <elem py:match="elem"> - <div class="elem"> - ${select('*')} - </div> - </elem> - <elem> - <subelem> - <elem/> - </subelem> - </elem> - </doc>""") - self.assertEqual("""<doc> - <elem> - <div class="elem"> - <subelem> - <elem> - <div class="elem"> - </div> - </elem> - </subelem> - </div> - </elem> - </doc>""", 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("""<html xmlns:py="http://genshi.edgewall.org/"> - <body py:match="body"> - <div id="header"/> - ${select('*')} - </body> - <body py:match="body"> - ${select('*')} - <div id="footer"/> - </body> - <body> - <h1>Foo</h1> - </body> - </html>""") - self.assertEqual("""<html> - <body> - <div id="header"/><h1>Foo</h1> - <div id="footer"/> - </body> - </html>""", str(tmpl.generate())) - - def test_select_all_attrs(self): - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:match="elem" py:attrs="select('@*')"> - ${select('text()')} - </div> - <elem id="joe">Hey Joe</elem> - </doc>""") - self.assertEqual("""<doc> - <div id="joe"> - Hey Joe - </div> - </doc>""", str(tmpl.generate())) - - def test_select_all_attrs_empty(self): - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:match="elem" py:attrs="select('@*')"> - ${select('text()')} - </div> - <elem>Hey Joe</elem> - </doc>""") - self.assertEqual("""<doc> - <div> - Hey Joe - </div> - </doc>""", str(tmpl.generate())) - - def test_select_all_attrs_in_body(self): - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <div py:match="elem"> - Hey ${select('text()')} ${select('@*')} - </div> - <elem title="Cool">Joe</elem> - </doc>""") - self.assertEqual("""<doc> - <div> - Hey Joe Cool - </div> - </doc>""", str(tmpl.generate())) - - def test_def_in_match(self): - tmpl = MarkupTemplate("""<doc xmlns:py="http://genshi.edgewall.org/"> - <py:def function="maketitle(test)"><b py:replace="test" /></py:def> - <head py:match="head">${select('*')}</head> - <head><title>${maketitle(True)}</title></head> - </doc>""") - self.assertEqual("""<doc> - <head><title>True</title></head> - </doc>""", str(tmpl.generate())) - - def test_match_with_xpath_variable(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <span py:match="*[name()=$tagname]"> - Hello ${select('@name')} - </span> - <greeting name="Dude"/> - </div>""") - self.assertEqual("""<div> - <span> - Hello Dude - </span> - </div>""", str(tmpl.generate(tagname='greeting'))) - self.assertEqual("""<div> - <greeting name="Dude"/> - </div>""", str(tmpl.generate(tagname='sayhello'))) - - def test_content_directive_in_match(self): - tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> - <div py:match="foo">I said <q py:content="select('text()')">something</q>.</div> - <foo>bar</foo> - </html>""") - self.assertEqual("""<html> - <div>I said <q>bar</q>.</div> - </html>""", str(tmpl.generate())) - - def test_cascaded_matches(self): - tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> - <body py:match="body">${select('*')}</body> - <head py:match="head">${select('title')}</head> - <body py:match="body">${select('*')}<hr /></body> - <head><title>Welcome to Markup</title></head> - <body><h2>Are you ready to mark up?</h2></body> - </html>""") - self.assertEqual("""<html> - <head><title>Welcome to Markup</title></head> - <body><h2>Are you ready to mark up?</h2><hr/></body> - </html>""", str(tmpl.generate())) - - def test_multiple_matches(self): - tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> - <input py:match="form//input" py:attrs="select('@*')" - value="${values[str(select('@name'))]}" /> - <form><p py:for="field in fields"> - <label>${field.capitalize()}</label> - <input type="text" name="${field}" /> - </p></form> - </html>""") - fields = ['hello_%s' % i for i in range(5)] - values = dict([('hello_%s' % i, i) for i in range(5)]) - self.assertEqual("""<html> - <form><p> - <label>Hello_0</label> - <input value="0" type="text" name="hello_0"/> - </p><p> - <label>Hello_1</label> - <input value="1" type="text" name="hello_1"/> - </p><p> - <label>Hello_2</label> - <input value="2" type="text" name="hello_2"/> - </p><p> - <label>Hello_3</label> - <input value="3" type="text" name="hello_3"/> - </p><p> - <label>Hello_4</label> - <input value="4" type="text" name="hello_4"/> - </p></form> - </html>""", str(tmpl.generate(fields=fields, values=values))) - - def test_namespace_context(self): - tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/" - xmlns:x="http://www.example.org/"> - <div py:match="x:foo">Foo</div> - <foo xmlns="http://www.example.org/"/> - </html>""") - # FIXME: there should be a way to strip out unwanted/unused namespaces, - # such as the "x" in this example - self.assertEqual("""<html xmlns:x="http://www.example.org/"> - <div>Foo</div> - </html>""", str(tmpl.generate())) - - def test_match_with_position_predicate(self): - tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> - <p py:match="body/p[1]" class="first">${select('*|text()')}</p> - <body> - <p>Foo</p> - <p>Bar</p> - </body> - </html>""") - self.assertEqual("""<html> - <body> - <p class="first">Foo</p> - <p>Bar</p> - </body> - </html>""", str(tmpl.generate())) - - def test_match_with_closure(self): - tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> - <p py:match="body//p" class="para">${select('*|text()')}</p> - <body> - <p>Foo</p> - <div><p>Bar</p></div> - </body> - </html>""") - self.assertEqual("""<html> - <body> - <p class="para">Foo</p> - <div><p class="para">Bar</p></div> - </body> - </html>""", str(tmpl.generate())) - - def test_match_without_closure(self): - tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"> - <p py:match="body/p" class="para">${select('*|text()')}</p> - <body> - <p>Foo</p> - <div><p>Bar</p></div> - </body> - </html>""") - self.assertEqual("""<html> - <body> - <p class="para">Foo</p> - <div><p>Bar</p></div> - </body> - </html>""", str(tmpl.generate())) - - # FIXME - #def test_match_after_step(self): - # tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - # <span py:match="div/greeting"> - # Hello ${select('@name')} - # </span> - # <greeting name="Dude" /> - # </div>""") - # self.assertEqual("""<div> - # <span> - # Hello Dude - # </span> - # </div>""", str(tmpl.generate())) - - -class StripDirectiveTestCase(unittest.TestCase): - """Tests for the `py:strip` template directive.""" - - def test_strip_false(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <div py:strip="False"><b>foo</b></div> - </div>""") - self.assertEqual("""<div> - <div><b>foo</b></div> - </div>""", str(tmpl.generate())) - - def test_strip_empty(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <div py:strip=""><b>foo</b></div> - </div>""") - self.assertEqual("""<div> - <b>foo</b> - </div>""", str(tmpl.generate())) - - -class WithDirectiveTestCase(unittest.TestCase): - """Tests for the `py:with` template directive.""" - - def test_shadowing(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - ${x} - <span py:with="x = x * 2" py:replace="x"/> - ${x} - </div>""") - self.assertEqual("""<div> - 42 - 84 - 42 - </div>""", str(tmpl.generate(x=42))) - - def test_as_element(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <py:with vars="x = x * 2">${x}</py:with> - </div>""") - self.assertEqual("""<div> - 84 - </div>""", str(tmpl.generate(x=42))) - - def test_multiple_vars_same_name(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <py:with vars=" - foo = 'bar'; - foo = foo.replace('r', 'z') - "> - $foo - </py:with> - </div>""") - self.assertEqual("""<div> - baz - </div>""", str(tmpl.generate(x=42))) - - def test_multiple_vars_single_assignment(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <py:with vars="x = y = z = 1">${x} ${y} ${z}</py:with> - </div>""") - self.assertEqual("""<div> - 1 1 1 - </div>""", str(tmpl.generate(x=42))) - - def test_nested_vars_single_assignment(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <py:with vars="x, (y, z) = (1, (2, 3))">${x} ${y} ${z}</py:with> - </div>""") - self.assertEqual("""<div> - 1 2 3 - </div>""", str(tmpl.generate(x=42))) - - def test_multiple_vars_trailing_semicolon(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <py:with vars="x = x * 2; y = x / 2;">${x} ${y}</py:with> - </div>""") - self.assertEqual("""<div> - 84 42 - </div>""", str(tmpl.generate(x=42))) - - def test_semicolon_escape(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <py:with vars="x = 'here is a semicolon: ;'; y = 'here are two semicolons: ;;' ;"> - ${x} - ${y} - </py:with> - </div>""") - self.assertEqual("""<div> - here is a semicolon: ; - here are two semicolons: ;; - </div>""", str(tmpl.generate())) - - def test_unicode_expr(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <span py:with="weeks=(u'一', u'二', u'三', u'四', u'五', u'六', u'日')"> - $weeks - </span> - </div>""") - self.assertEqual("""<div> - <span> - 一二三四五六日 - </span> - </div>""", str(tmpl.generate())) - - -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]) - - -class MarkupTemplateTestCase(unittest.TestCase): - """Tests for markup template processing.""" - - def test_interpolate_mixed3(self): - tmpl = MarkupTemplate('<root> ${var} $var</root>') - self.assertEqual('<root> 42 42</root>', str(tmpl.generate(var=42))) - - def test_interpolate_leading_trailing_space(self): - tmpl = MarkupTemplate('<root>${ foo }</root>') - self.assertEqual('<root>bar</root>', str(tmpl.generate(foo='bar'))) - - def test_interpolate_multiline(self): - tmpl = MarkupTemplate("""<root>${dict( - bar = 'baz' - )[foo]}</root>""") - self.assertEqual('<root>baz</root>', str(tmpl.generate(foo='bar'))) - - def test_interpolate_non_string_attrs(self): - tmpl = MarkupTemplate('<root attr="${1}"/>') - self.assertEqual('<root attr="1"/>', str(tmpl.generate())) - - def test_interpolate_list_result(self): - tmpl = MarkupTemplate('<root>$foo</root>') - self.assertEqual('<root>buzz</root>', str(tmpl.generate(foo=('buzz',)))) - - def test_empty_attr(self): - tmpl = MarkupTemplate('<root attr=""/>') - self.assertEqual('<root attr=""/>', str(tmpl.generate())) - - def test_bad_directive_error(self): - xml = '<p xmlns:py="http://genshi.edgewall.org/" py:do="nothing" />' - try: - tmpl = MarkupTemplate(xml, filename='test.html') - except BadDirectiveError, e: - self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): - self.assertEqual(1, e.lineno) - - def test_directive_value_syntax_error(self): - xml = """<p xmlns:py="http://genshi.edgewall.org/" py:if="bar'" />""" - try: - tmpl = MarkupTemplate(xml, filename='test.html') - self.fail('Expected SyntaxError') - except TemplateSyntaxError, e: - self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): - self.assertEqual(1, e.lineno) - - def test_expression_syntax_error(self): - xml = """<p> - Foo <em>${bar"}</em> - </p>""" - try: - tmpl = MarkupTemplate(xml, filename='test.html') - self.fail('Expected SyntaxError') - except TemplateSyntaxError, e: - self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): - self.assertEqual(2, e.lineno) - - def test_expression_syntax_error_multi_line(self): - xml = """<p><em></em> - - ${bar"} - - </p>""" - try: - tmpl = MarkupTemplate(xml, filename='test.html') - self.fail('Expected SyntaxError') - except TemplateSyntaxError, e: - self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): - self.assertEqual(3, e.lineno) - - def test_markup_noescape(self): - """ - Verify that outputting context data that is a `Markup` instance is not - escaped. - """ - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - $myvar - </div>""") - self.assertEqual("""<div> - <b>foo</b> - </div>""", str(tmpl.generate(myvar=Markup('<b>foo</b>')))) - - def test_text_noescape_quotes(self): - """ - Verify that outputting context data in text nodes doesn't escape quotes. - """ - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - $myvar - </div>""") - self.assertEqual("""<div> - "foo" - </div>""", str(tmpl.generate(myvar='"foo"'))) - - def test_attr_escape_quotes(self): - """ - Verify that outputting context data in attribtes escapes quotes. - """ - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <elem class="$myvar"/> - </div>""") - self.assertEqual("""<div> - <elem class=""foo""/> - </div>""", str(tmpl.generate(myvar='"foo"'))) - - def test_directive_element(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <py:if test="myvar">bar</py:if> - </div>""") - self.assertEqual("""<div> - bar - </div>""", str(tmpl.generate(myvar='"foo"'))) - - def test_normal_comment(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <!-- foo bar --> - </div>""") - self.assertEqual("""<div> - <!-- foo bar --> - </div>""", str(tmpl.generate())) - - def test_template_comment(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <!-- !foo --> - <!--!bar--> - </div>""") - self.assertEqual("""<div> - </div>""", str(tmpl.generate())) - - def test_parse_with_same_namespace_nested(self): - tmpl = MarkupTemplate("""<div xmlns:py="http://genshi.edgewall.org/"> - <span xmlns:py="http://genshi.edgewall.org/"> - </span> - </div>""") - self.assertEqual("""<div> - <span> - </span> - </div>""", str(tmpl.generate())) - - def test_latin1_encoded_with_xmldecl(self): - tmpl = MarkupTemplate(u"""<?xml version="1.0" encoding="iso-8859-1" ?> - <div xmlns:py="http://genshi.edgewall.org/"> - \xf6 - </div>""".encode('iso-8859-1'), encoding='iso-8859-1') - self.assertEqual(u"""<div> - \xf6 - </div>""", unicode(tmpl.generate())) - - def test_latin1_encoded_explicit_encoding(self): - tmpl = MarkupTemplate(u"""<div xmlns:py="http://genshi.edgewall.org/"> - \xf6 - </div>""".encode('iso-8859-1'), encoding='iso-8859-1') - self.assertEqual(u"""<div> - \xf6 - </div>""", unicode(tmpl.generate())) - - -class TextTemplateTestCase(unittest.TestCase): - """Tests for text template processing.""" - - def test_escaping(self): - tmpl = TextTemplate('\\#escaped') - self.assertEqual('#escaped', str(tmpl.generate())) - - def test_comment(self): - tmpl = TextTemplate('## a comment') - self.assertEqual('', str(tmpl.generate())) - - def test_comment_escaping(self): - tmpl = TextTemplate('\\## escaped comment') - self.assertEqual('## escaped comment', str(tmpl.generate())) - - def test_end_with_args(self): - tmpl = TextTemplate(""" - #if foo - bar - #end 'if foo'""") - self.assertEqual('', str(tmpl.generate())) - - def test_latin1_encoded(self): - text = u'$foo\xf6$bar'.encode('iso-8859-1') - tmpl = TextTemplate(text, encoding='iso-8859-1') - self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y'))) - - # FIXME - #def test_empty_lines(self): - # tmpl = TextTemplate("""Your items: - # - # #for item in items - # * ${item} - # - # #end""") - # self.assertEqual("""Your items: - # * 0 - # * 1 - # * 2 - # """, tmpl.generate(items=range(3)).render('text')) - - -class TemplateLoaderTestCase(unittest.TestCase): - """Tests for the template loader.""" - - def setUp(self): - self.dirname = tempfile.mkdtemp(suffix='markup_test') - - def tearDown(self): - shutil.rmtree(self.dirname) - - def test_search_path_empty(self): - loader = TemplateLoader() - self.assertEqual([], loader.search_path) - - def test_search_path_as_string(self): - loader = TemplateLoader(self.dirname) - self.assertEqual([self.dirname], loader.search_path) - - def test_relative_include_samedir(self): - file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') - try: - file1.write("""<div>Included</div>""") - finally: - file1.close() - - file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w') - try: - file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> - <xi:include href="tmpl1.html" /> - </html>""") - finally: - file2.close() - - loader = TemplateLoader([self.dirname]) - tmpl = loader.load('tmpl2.html') - self.assertEqual("""<html> - <div>Included</div> - </html>""", tmpl.generate().render()) - - def test_relative_include_subdir(self): - os.mkdir(os.path.join(self.dirname, 'sub')) - file1 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w') - try: - file1.write("""<div>Included</div>""") - finally: - file1.close() - - file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w') - try: - file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> - <xi:include href="sub/tmpl1.html" /> - </html>""") - finally: - file2.close() - - loader = TemplateLoader([self.dirname]) - tmpl = loader.load('tmpl2.html') - self.assertEqual("""<html> - <div>Included</div> - </html>""", tmpl.generate().render()) - - def test_relative_include_parentdir(self): - file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') - try: - file1.write("""<div>Included</div>""") - finally: - file1.close() - - os.mkdir(os.path.join(self.dirname, 'sub')) - file2 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w') - try: - file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> - <xi:include href="../tmpl1.html" /> - </html>""") - finally: - file2.close() - - loader = TemplateLoader([self.dirname]) - tmpl = loader.load('sub/tmpl2.html') - self.assertEqual("""<html> - <div>Included</div> - </html>""", tmpl.generate().render()) - - def test_relative_include_without_search_path(self): - file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') - try: - file1.write("""<div>Included</div>""") - finally: - file1.close() - - file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w') - try: - file2.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> - <xi:include href="tmpl1.html" /> - </html>""") - finally: - file2.close() - - loader = TemplateLoader() - tmpl = loader.load(os.path.join(self.dirname, 'tmpl2.html')) - self.assertEqual("""<html> - <div>Included</div> - </html>""", tmpl.generate().render()) - - def test_relative_include_without_search_path_nested(self): - file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') - try: - file1.write("""<div>Included</div>""") - finally: - file1.close() - - file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w') - try: - file2.write("""<div xmlns:xi="http://www.w3.org/2001/XInclude"> - <xi:include href="tmpl1.html" /> - </div>""") - finally: - file2.close() - - file3 = open(os.path.join(self.dirname, 'tmpl3.html'), 'w') - try: - file3.write("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> - <xi:include href="tmpl2.html" /> - </html>""") - finally: - file3.close() - - loader = TemplateLoader() - tmpl = loader.load(os.path.join(self.dirname, 'tmpl3.html')) - self.assertEqual("""<html> - <div> - <div>Included</div> - </div> - </html>""", tmpl.generate().render()) - - def test_relative_include_from_inmemory_template(self): - file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') - try: - file1.write("""<div>Included</div>""") - finally: - file1.close() - - loader = TemplateLoader([self.dirname]) - tmpl2 = MarkupTemplate("""<html xmlns:xi="http://www.w3.org/2001/XInclude"> - <xi:include href="../tmpl1.html" /> - </html>""", filename='subdir/tmpl2.html', loader=loader) - - self.assertEqual("""<html> - <div>Included</div> - </html>""", tmpl2.generate().render()) - - def test_load_with_default_encoding(self): - f = open(os.path.join(self.dirname, 'tmpl.html'), 'w') - try: - f.write(u'<div>\xf6</div>'.encode('iso-8859-1')) - finally: - f.close() - loader = TemplateLoader([self.dirname], default_encoding='iso-8859-1') - loader.load('tmpl.html') - - def test_load_with_explicit_encoding(self): - f = open(os.path.join(self.dirname, 'tmpl.html'), 'w') - try: - f.write(u'<div>\xf6</div>'.encode('iso-8859-1')) - finally: - f.close() - loader = TemplateLoader([self.dirname], default_encoding='utf-8') - loader.load('tmpl.html', encoding='iso-8859-1') - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(template)) - suite.addTest(unittest.makeSuite(AttrsDirectiveTestCase, 'test')) - suite.addTest(unittest.makeSuite(ChooseDirectiveTestCase, 'test')) - suite.addTest(unittest.makeSuite(DefDirectiveTestCase, 'test')) - suite.addTest(unittest.makeSuite(ForDirectiveTestCase, 'test')) - suite.addTest(unittest.makeSuite(IfDirectiveTestCase, 'test')) - suite.addTest(unittest.makeSuite(MatchDirectiveTestCase, 'test')) - suite.addTest(unittest.makeSuite(StripDirectiveTestCase, 'test')) - suite.addTest(unittest.makeSuite(WithDirectiveTestCase, 'test')) - suite.addTest(unittest.makeSuite(TemplateTestCase, 'test')) - suite.addTest(unittest.makeSuite(MarkupTemplateTestCase, 'test')) - suite.addTest(unittest.makeSuite(TextTemplateTestCase, 'test')) - suite.addTest(unittest.makeSuite(TemplateLoaderTestCase, 'test')) - return suite - -if __name__ == '__main__': - unittest.main(defaultTest='suite')
--- a/setup.py +++ b/setup.py @@ -46,14 +46,14 @@ 'Topic :: Text Processing :: Markup :: XML' ], keywords = ['python.templating.engines'], - packages = ['genshi'], + packages = ['genshi', 'genshi.template'], test_suite = 'genshi.tests.suite', extras_require = {'plugin': ['setuptools>=0.6a2']}, entry_points = """ [python.templating.engines] - genshi = genshi.plugin:MarkupTemplateEnginePlugin[plugin] - genshi-markup = genshi.plugin:MarkupTemplateEnginePlugin[plugin] - genshi-text = genshi.plugin:TextTemplateEnginePlugin[plugin] + genshi = genshi.template.plugin:MarkupTemplateEnginePlugin[plugin] + genshi-markup = genshi.template.plugin:MarkupTemplateEnginePlugin[plugin] + genshi-text = genshi.template.plugin:TextTemplateEnginePlugin[plugin] """, )