cmlenz@336: # -*- coding: utf-8 -*- cmlenz@336: # cmlenz@407: # Copyright (C) 2006-2007 Edgewall Software cmlenz@336: # All rights reserved. cmlenz@336: # cmlenz@336: # This software is licensed as described in the file COPYING, which cmlenz@336: # you should have received as part of this distribution. The terms cmlenz@336: # are also available at http://genshi.edgewall.org/wiki/License. cmlenz@336: # cmlenz@336: # This software consists of voluntary contributions made by many cmlenz@336: # individuals. For the exact contribution history, see the revision cmlenz@336: # history and logs, available at http://genshi.edgewall.org/log/. cmlenz@336: cmlenz@427: """Basic templating functionality.""" cmlenz@427: cmlenz@336: try: cmlenz@336: from collections import deque cmlenz@336: except ImportError: cmlenz@336: class deque(list): cmlenz@336: def appendleft(self, x): self.insert(0, x) cmlenz@336: def popleft(self): return self.pop(0) cmlenz@336: import os cmlenz@336: from StringIO import StringIO cmlenz@609: import sys cmlenz@336: cmlenz@636: from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure cmlenz@434: from genshi.input import ParseError aflett@711: from genshi.template.match import MatchSet cmlenz@336: cmlenz@336: __all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError', cmlenz@336: 'TemplateSyntaxError', 'BadDirectiveError'] cmlenz@425: __docformat__ = 'restructuredtext en' cmlenz@336: cmlenz@609: if sys.version_info < (2, 4): cmlenz@609: _ctxt2dict = lambda ctxt: ctxt.frames[0] cmlenz@609: else: cmlenz@609: _ctxt2dict = lambda ctxt: ctxt cmlenz@609: cmlenz@336: cmlenz@336: class TemplateError(Exception): cmlenz@336: """Base exception class for errors related to template processing.""" cmlenz@336: cmlenz@610: def __init__(self, message, filename=None, lineno=-1, offset=-1): cmlenz@438: """Create the exception. cmlenz@435: cmlenz@435: :param message: the error message cmlenz@435: :param filename: the filename of the template cmlenz@435: :param lineno: the number of line in the template at which the error cmlenz@435: occurred cmlenz@435: :param offset: the column number at which the error occurred cmlenz@435: """ cmlenz@610: if filename is None: cmlenz@610: filename = '' cmlenz@438: self.msg = message #: the error message string cmlenz@407: if filename != '' or lineno >= 0: cmlenz@407: message = '%s (%s, line %d)' % (self.msg, filename, lineno) cmlenz@438: Exception.__init__(self, message) cmlenz@438: self.filename = filename #: the name of the template file cmlenz@438: self.lineno = lineno #: the number of the line containing the error cmlenz@438: self.offset = offset #: the offset on the line cmlenz@336: cmlenz@336: cmlenz@336: class TemplateSyntaxError(TemplateError): cmlenz@336: """Exception raised when an expression in a template causes a Python syntax cmlenz@438: error, or the template is not well-formed. cmlenz@438: """ cmlenz@336: cmlenz@610: def __init__(self, message, filename=None, lineno=-1, offset=-1): cmlenz@435: """Create the exception cmlenz@435: cmlenz@435: :param message: the error message cmlenz@435: :param filename: the filename of the template cmlenz@435: :param lineno: the number of line in the template at which the error cmlenz@435: occurred cmlenz@435: :param offset: the column number at which the error occurred cmlenz@435: """ cmlenz@336: if isinstance(message, SyntaxError) and message.lineno is not None: cmlenz@336: message = str(message).replace(' (line %d)' % message.lineno, '') cmlenz@438: TemplateError.__init__(self, message, filename, lineno) cmlenz@336: cmlenz@336: cmlenz@336: class BadDirectiveError(TemplateSyntaxError): cmlenz@336: """Exception raised when an unknown directive is encountered when parsing cmlenz@336: a template. cmlenz@336: cmlenz@336: An unknown directive is any attribute using the namespace for directives, cmlenz@336: with a local name that doesn't match any registered directive. cmlenz@336: """ cmlenz@336: cmlenz@610: def __init__(self, name, filename=None, lineno=-1): cmlenz@435: """Create the exception cmlenz@435: cmlenz@435: :param name: the name of the directive cmlenz@435: :param filename: the filename of the template cmlenz@435: :param lineno: the number of line in the template at which the error cmlenz@435: occurred cmlenz@435: """ cmlenz@438: TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name, cmlenz@438: filename, lineno) cmlenz@438: cmlenz@438: cmlenz@438: class TemplateRuntimeError(TemplateError): cmlenz@438: """Exception raised when an the evaluation of a Python expression in a cmlenz@438: template causes an error. cmlenz@438: """ cmlenz@336: cmlenz@336: cmlenz@336: class Context(object): cmlenz@336: """Container for template input data. cmlenz@336: cmlenz@336: A context provides a stack of scopes (represented by dictionaries). cmlenz@336: cmlenz@336: Template directives such as loops can push a new scope on the stack with cmlenz@336: data that should only be available inside the loop. When the loop cmlenz@336: terminates, that scope can get popped off the stack again. cmlenz@336: cmlenz@336: >>> ctxt = Context(one='foo', other=1) cmlenz@336: >>> ctxt.get('one') cmlenz@336: 'foo' cmlenz@336: >>> ctxt.get('other') cmlenz@336: 1 cmlenz@336: >>> ctxt.push(dict(one='frost')) cmlenz@336: >>> ctxt.get('one') cmlenz@336: 'frost' cmlenz@336: >>> ctxt.get('other') cmlenz@336: 1 cmlenz@336: >>> ctxt.pop() cmlenz@336: {'one': 'frost'} cmlenz@336: >>> ctxt.get('one') cmlenz@336: 'foo' cmlenz@336: """ cmlenz@336: cmlenz@336: def __init__(self, **data): cmlenz@435: """Initialize the template context with the given keyword arguments as cmlenz@435: data. cmlenz@435: """ cmlenz@336: self.frames = deque([data]) cmlenz@336: self.pop = self.frames.popleft cmlenz@336: self.push = self.frames.appendleft aflett@711: self._match_set = MatchSet() cmlenz@553: self._choice_stack = [] cmlenz@336: cmlenz@442: # Helper functions for use in expressions cmlenz@442: def defined(name): cmlenz@442: """Return whether a variable with the specified name exists in the cmlenz@442: expression scope.""" cmlenz@442: return name in self cmlenz@442: def value_of(name, default=None): cmlenz@442: """If a variable of the specified name is defined, return its value. cmlenz@442: Otherwise, return the provided default value, or ``None``.""" cmlenz@442: return self.get(name, default) cmlenz@442: data.setdefault('defined', defined) cmlenz@442: data.setdefault('value_of', value_of) cmlenz@442: cmlenz@336: def __repr__(self): cmlenz@336: return repr(list(self.frames)) cmlenz@336: cmlenz@405: def __contains__(self, key): cmlenz@435: """Return whether a variable exists in any of the scopes. cmlenz@435: cmlenz@435: :param key: the name of the variable cmlenz@435: """ cmlenz@405: return self._find(key)[1] is not None cmlenz@564: has_key = __contains__ cmlenz@405: cmlenz@405: def __delitem__(self, key): cmlenz@435: """Remove a variable from all scopes. cmlenz@435: cmlenz@435: :param key: the name of the variable cmlenz@435: """ cmlenz@405: for frame in self.frames: cmlenz@405: if key in frame: cmlenz@405: del frame[key] cmlenz@405: cmlenz@405: def __getitem__(self, key): cmlenz@405: """Get a variables's value, starting at the current scope and going cmlenz@405: upward. cmlenz@405: cmlenz@435: :param key: the name of the variable cmlenz@435: :return: the variable value cmlenz@435: :raises KeyError: if the requested variable wasn't found in any scope cmlenz@405: """ cmlenz@405: value, frame = self._find(key) cmlenz@405: if frame is None: cmlenz@405: raise KeyError(key) cmlenz@405: return value cmlenz@405: cmlenz@420: def __len__(self): cmlenz@435: """Return the number of distinctly named variables in the context. cmlenz@435: cmlenz@435: :return: the number of variables in the context cmlenz@435: """ cmlenz@420: return len(self.items()) cmlenz@420: cmlenz@336: def __setitem__(self, key, value): cmlenz@435: """Set a variable in the current scope. cmlenz@435: cmlenz@435: :param key: the name of the variable cmlenz@435: :param value: the variable value cmlenz@435: """ cmlenz@336: self.frames[0][key] = value cmlenz@336: cmlenz@336: def _find(self, key, default=None): cmlenz@336: """Retrieve a given variable's value and the frame it was found in. cmlenz@336: cmlenz@435: Intended primarily for internal use by directives. cmlenz@435: cmlenz@435: :param key: the name of the variable cmlenz@435: :param default: the default value to return when the variable is not cmlenz@435: found cmlenz@336: """ cmlenz@336: for frame in self.frames: cmlenz@336: if key in frame: cmlenz@336: return frame[key], frame cmlenz@336: return default, None cmlenz@336: cmlenz@336: def get(self, key, default=None): cmlenz@336: """Get a variable's value, starting at the current scope and going cmlenz@336: upward. cmlenz@435: cmlenz@435: :param key: the name of the variable cmlenz@435: :param default: the default value to return when the variable is not cmlenz@435: found cmlenz@336: """ cmlenz@336: for frame in self.frames: cmlenz@336: if key in frame: cmlenz@336: return frame[key] cmlenz@336: return default cmlenz@405: cmlenz@405: def keys(self): cmlenz@435: """Return the name of all variables in the context. cmlenz@435: cmlenz@435: :return: a list of variable names cmlenz@435: """ cmlenz@405: keys = [] cmlenz@405: for frame in self.frames: cmlenz@405: keys += [key for key in frame if key not in keys] cmlenz@405: return keys cmlenz@405: cmlenz@405: def items(self): cmlenz@435: """Return a list of ``(name, value)`` tuples for all variables in the cmlenz@435: context. cmlenz@435: cmlenz@435: :return: a list of variables cmlenz@435: """ cmlenz@405: return [(key, self.get(key)) for key in self.keys()] cmlenz@336: cmlenz@336: def push(self, data): cmlenz@435: """Push a new scope on the stack. cmlenz@435: cmlenz@435: :param data: the data dictionary to push on the context stack. cmlenz@435: """ cmlenz@336: cmlenz@336: def pop(self): cmlenz@336: """Pop the top-most scope from the stack.""" cmlenz@336: cmlenz@336: aflett@703: def _apply_directives(stream, directives, ctxt, **vars): cmlenz@435: """Apply the given directives to the stream. cmlenz@435: cmlenz@435: :param stream: the stream the directives should be applied to aflett@703: :param directives: the list of directives to apply cmlenz@435: :param ctxt: the `Context` aflett@703: :param vars: additional variables that should be available when Python aflett@703: code is executed cmlenz@435: :return: the stream with the given directives applied cmlenz@435: """ cmlenz@336: if directives: aflett@703: stream = directives[0](iter(stream), directives[1:], ctxt, **vars) cmlenz@336: return stream cmlenz@336: aflett@703: def _eval_expr(expr, ctxt, **vars): aflett@703: """Evaluate the given `Expression` object. aflett@703: aflett@703: :param expr: the expression to evaluate aflett@703: :param ctxt: the `Context` aflett@703: :param vars: additional variables that should be available to the aflett@703: expression aflett@703: :return: the result of the evaluation aflett@703: """ aflett@703: if vars: aflett@703: ctxt.push(vars) aflett@703: retval = expr.evaluate(ctxt) aflett@703: if vars: aflett@703: ctxt.pop() aflett@703: return retval aflett@703: aflett@703: def _exec_suite(suite, ctxt, **vars): aflett@703: """Execute the given `Suite` object. aflett@703: aflett@703: :param suite: the code suite to execute aflett@703: :param ctxt: the `Context` aflett@703: :param vars: additional variables that should be available to the aflett@703: code aflett@703: """ aflett@703: if vars: aflett@703: ctxt.push(vars) aflett@703: ctxt.push({}) aflett@703: suite.execute(_ctxt2dict(ctxt)) aflett@703: if vars: aflett@703: top = ctxt.pop() aflett@703: ctxt.pop() aflett@703: ctxt.frames[0].update(top) aflett@703: cmlenz@336: cmlenz@336: class TemplateMeta(type): cmlenz@336: """Meta class for templates.""" cmlenz@336: cmlenz@336: def __new__(cls, name, bases, d): cmlenz@336: if 'directives' in d: cmlenz@336: d['_dir_by_name'] = dict(d['directives']) cmlenz@336: d['_dir_order'] = [directive[1] for directive in d['directives']] cmlenz@336: cmlenz@336: return type.__new__(cls, name, bases, d) cmlenz@336: cmlenz@336: cmlenz@336: class Template(object): cmlenz@336: """Abstract template base class. cmlenz@336: cmlenz@336: This class implements most of the template processing model, but does not cmlenz@336: specify the syntax of templates. cmlenz@336: """ cmlenz@336: __metaclass__ = TemplateMeta cmlenz@336: cmlenz@609: EXEC = StreamEventKind('EXEC') cmlenz@609: """Stream event kind representing a Python code suite to execute.""" cmlenz@609: cmlenz@427: EXPR = StreamEventKind('EXPR') cmlenz@427: """Stream event kind representing a Python expression.""" cmlenz@427: cmlenz@475: INCLUDE = StreamEventKind('INCLUDE') cmlenz@475: """Stream event kind representing the inclusion of another template.""" cmlenz@475: cmlenz@427: SUB = StreamEventKind('SUB') cmlenz@427: """Stream event kind representing a nested stream to which one or more cmlenz@427: directives should be applied. cmlenz@427: """ cmlenz@336: cmlenz@605: serializer = None cmlenz@636: _number_conv = unicode # function used to convert numbers to event data cmlenz@605: cmlenz@336: def __init__(self, source, basedir=None, filename=None, loader=None, cmlenz@606: encoding=None, lookup='strict', allow_exec=True): cmlenz@427: """Initialize a template from either a string, a file-like object, or cmlenz@427: an already parsed markup stream. cmlenz@427: cmlenz@427: :param source: a string, file-like object, or markup stream to read the cmlenz@427: template from cmlenz@427: :param basedir: the base directory containing the template file; when cmlenz@427: loaded from a `TemplateLoader`, this will be the cmlenz@427: directory on the template search path in which the cmlenz@427: template was found cmlenz@427: :param filename: the name of the template file, relative to the given cmlenz@427: base directory cmlenz@492: :param loader: the `TemplateLoader` to use for loading included cmlenz@492: templates cmlenz@427: :param encoding: the encoding of the `source` cmlenz@606: :param lookup: the variable lookup mechanism; either "strict" (the cmlenz@606: default), "lenient", or a custom lookup class cmlenz@545: :param allow_exec: whether Python code blocks in templates should be cmlenz@545: allowed cmlenz@545: cmlenz@545: :note: Changed in 0.5: Added the `allow_exec` argument cmlenz@427: """ cmlenz@336: self.basedir = basedir cmlenz@336: self.filename = filename cmlenz@336: if basedir and filename: cmlenz@336: self.filepath = os.path.join(basedir, filename) cmlenz@336: else: cmlenz@336: self.filepath = filename cmlenz@363: self.loader = loader cmlenz@442: self.lookup = lookup cmlenz@545: self.allow_exec = allow_exec cmlenz@336: cmlenz@610: self.filters = [self._flatten, self._eval, self._exec] cmlenz@610: if loader: cmlenz@610: self.filters.append(self._include) cmlenz@610: cmlenz@374: if isinstance(source, basestring): cmlenz@374: source = StringIO(source) cmlenz@374: else: cmlenz@374: source = source cmlenz@434: try: cmlenz@434: self.stream = list(self._prepare(self._parse(source, encoding))) cmlenz@434: except ParseError, e: cmlenz@434: raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset) cmlenz@336: cmlenz@336: def __repr__(self): cmlenz@336: return '<%s "%s">' % (self.__class__.__name__, self.filename) cmlenz@336: cmlenz@374: def _parse(self, source, encoding): cmlenz@336: """Parse the template. cmlenz@336: cmlenz@336: The parsing stage parses the template and constructs a list of cmlenz@336: directives that will be executed in the render stage. The input is cmlenz@336: split up into literal output (text that does not depend on the context cmlenz@336: data) and directives or expressions. cmlenz@427: cmlenz@427: :param source: a file-like object containing the XML source of the cmlenz@427: template, or an XML event stream cmlenz@427: :param encoding: the encoding of the `source` cmlenz@336: """ cmlenz@336: raise NotImplementedError cmlenz@336: cmlenz@351: def _prepare(self, stream): cmlenz@427: """Call the `attach` method of every directive found in the template. cmlenz@427: cmlenz@427: :param stream: the event stream of the template cmlenz@427: """ cmlenz@548: from genshi.template.loader import TemplateNotFound cmlenz@548: cmlenz@351: for kind, data, pos in stream: cmlenz@351: if kind is SUB: cmlenz@362: directives = [] cmlenz@362: substream = data[1] cmlenz@362: for cls, value, namespaces, pos in data[0]: cmlenz@362: directive, substream = cls.attach(self, substream, value, cmlenz@362: namespaces, pos) cmlenz@362: if directive: cmlenz@362: directives.append(directive) cmlenz@351: substream = self._prepare(substream) cmlenz@351: if directives: cmlenz@351: yield kind, (directives, list(substream)), pos cmlenz@351: else: cmlenz@351: for event in substream: cmlenz@351: yield event cmlenz@351: else: cmlenz@475: if kind is INCLUDE: cmlenz@610: href, cls, fallback = data cmlenz@548: if isinstance(href, basestring) and \ cmlenz@548: not getattr(self.loader, 'auto_reload', True): cmlenz@548: # If the path to the included template is static, and cmlenz@548: # auto-reloading is disabled on the template loader, cmlenz@548: # the template is inlined into the stream cmlenz@548: try: cmlenz@548: tmpl = self.loader.load(href, relative_to=pos[0], cmlenz@610: cls=cls or self.__class__) cmlenz@548: for event in tmpl.stream: cmlenz@548: yield event cmlenz@548: except TemplateNotFound: cmlenz@548: if fallback is None: cmlenz@548: raise cmlenz@548: for event in self._prepare(fallback): cmlenz@548: yield event cmlenz@548: continue cmlenz@590: elif fallback: cmlenz@548: # Otherwise the include is performed at run time cmlenz@639: data = href, cls, list(self._prepare(fallback)) cmlenz@548: cmlenz@351: yield kind, data, pos cmlenz@351: cmlenz@336: def generate(self, *args, **kwargs): cmlenz@336: """Apply the template to the given context data. cmlenz@336: cmlenz@336: Any keyword arguments are made available to the template as context cmlenz@336: data. cmlenz@336: cmlenz@336: Only one positional argument is accepted: if it is provided, it must be cmlenz@336: an instance of the `Context` class, and keyword arguments are ignored. cmlenz@336: This calling style is used for internal processing. cmlenz@336: cmlenz@427: :return: a markup event stream representing the result of applying cmlenz@427: the template to the context data. cmlenz@336: """ aflett@703: vars = {} cmlenz@336: if args: cmlenz@336: assert len(args) == 1 cmlenz@336: ctxt = args[0] cmlenz@336: if ctxt is None: cmlenz@336: ctxt = Context(**kwargs) aflett@703: else: aflett@703: vars = kwargs cmlenz@336: assert isinstance(ctxt, Context) cmlenz@336: else: cmlenz@336: ctxt = Context(**kwargs) cmlenz@336: cmlenz@336: stream = self.stream cmlenz@336: for filter_ in self.filters: aflett@703: stream = filter_(iter(stream), ctxt, **vars) cmlenz@605: return Stream(stream, self.serializer) cmlenz@336: aflett@703: def _eval(self, stream, ctxt, **vars): cmlenz@336: """Internal stream filter that evaluates any expressions in `START` and cmlenz@336: `TEXT` events. cmlenz@336: """ cmlenz@336: filters = (self._flatten, self._eval) cmlenz@636: number_conv = self._number_conv cmlenz@336: cmlenz@336: for kind, data, pos in stream: cmlenz@336: cmlenz@336: if kind is START and data[1]: cmlenz@336: # Attributes may still contain expressions in start tags at cmlenz@336: # this point, so do some evaluation cmlenz@345: tag, attrs = data cmlenz@345: new_attrs = [] cmlenz@345: for name, substream in attrs: cmlenz@336: if isinstance(substream, basestring): cmlenz@336: value = substream cmlenz@336: else: cmlenz@336: values = [] cmlenz@336: for subkind, subdata, subpos in self._eval(substream, aflett@703: ctxt, aflett@703: **vars): cmlenz@336: if subkind is TEXT: cmlenz@336: values.append(subdata) cmlenz@336: value = [x for x in values if x is not None] cmlenz@336: if not value: cmlenz@336: continue cmlenz@345: new_attrs.append((name, u''.join(value))) cmlenz@345: yield kind, (tag, Attrs(new_attrs)), pos cmlenz@336: cmlenz@336: elif kind is EXPR: aflett@703: result = _eval_expr(data, ctxt, **vars) cmlenz@336: if result is not None: cmlenz@635: # First check for a string, otherwise the iterable test cmlenz@635: # below succeeds, and the string will be chopped up into cmlenz@635: # individual characters cmlenz@336: if isinstance(result, basestring): cmlenz@336: yield TEXT, result, pos cmlenz@635: elif isinstance(result, (int, float, long)): cmlenz@636: yield TEXT, number_conv(result), pos cmlenz@336: elif hasattr(result, '__iter__'): cmlenz@336: substream = _ensure(result) cmlenz@336: for filter_ in filters: aflett@703: substream = filter_(substream, ctxt, **vars) cmlenz@336: for event in substream: cmlenz@336: yield event cmlenz@336: else: cmlenz@336: yield TEXT, unicode(result), pos cmlenz@336: cmlenz@336: else: cmlenz@336: yield kind, data, pos cmlenz@336: aflett@703: def _exec(self, stream, ctxt, **vars): cmlenz@609: """Internal stream filter that executes Python code blocks.""" cmlenz@609: for event in stream: cmlenz@609: if event[0] is EXEC: aflett@703: _exec_suite(event[1], ctxt, **vars) cmlenz@609: else: cmlenz@609: yield event cmlenz@609: aflett@703: def _flatten(self, stream, ctxt, **vars): cmlenz@336: """Internal stream filter that expands `SUB` events in the stream.""" cmlenz@336: for event in stream: cmlenz@336: if event[0] is SUB: cmlenz@336: # This event is a list of directives and a list of nested cmlenz@336: # events to which those directives should be applied cmlenz@336: directives, substream = event[1] aflett@703: substream = _apply_directives(substream, directives, ctxt, aflett@703: **vars) aflett@703: for event in self._flatten(substream, ctxt, **vars): cmlenz@336: yield event cmlenz@336: else: cmlenz@336: yield event cmlenz@336: aflett@703: def _include(self, stream, ctxt, **vars): cmlenz@475: """Internal stream filter that performs inclusion of external cmlenz@475: template files. cmlenz@475: """ cmlenz@475: from genshi.template.loader import TemplateNotFound cmlenz@475: cmlenz@475: for event in stream: cmlenz@475: if event[0] is INCLUDE: cmlenz@610: href, cls, fallback = event[1] cmlenz@475: if not isinstance(href, basestring): cmlenz@475: parts = [] aflett@703: for subkind, subdata, subpos in self._eval(href, ctxt, aflett@703: **vars): cmlenz@475: if subkind is TEXT: cmlenz@475: parts.append(subdata) cmlenz@475: href = u''.join([x for x in parts if x is not None]) cmlenz@475: try: cmlenz@475: tmpl = self.loader.load(href, relative_to=event[2][0], cmlenz@610: cls=cls or self.__class__) aflett@703: for event in tmpl.generate(ctxt, **vars): cmlenz@475: yield event cmlenz@475: except TemplateNotFound: cmlenz@475: if fallback is None: cmlenz@475: raise cmlenz@475: for filter_ in self.filters: aflett@703: fallback = filter_(iter(fallback), ctxt, **vars) cmlenz@475: for event in fallback: cmlenz@475: yield event cmlenz@475: else: cmlenz@475: yield event cmlenz@475: cmlenz@336: cmlenz@609: EXEC = Template.EXEC cmlenz@336: EXPR = Template.EXPR cmlenz@475: INCLUDE = Template.INCLUDE cmlenz@336: SUB = Template.SUB