Edgewall Software

source: branches/stable/0.4.x/genshi/template/base.py

Last change on this file was 597, checked in by cmlenz, 16 years ago

Ported [594:596] to 0.4.x branch.

  • Property svn:eol-style set to native
File size: 16.0 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2007 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
14"""Basic templating functionality."""
15
16try:
17    from collections import deque
18except ImportError:
19    class deque(list):
20        def appendleft(self, x): self.insert(0, x)
21        def popleft(self): return self.pop(0)
22import os
23from StringIO import StringIO
24
25from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
26from genshi.input import ParseError
27
28__all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError',
29           'TemplateSyntaxError', 'BadDirectiveError']
30__docformat__ = 'restructuredtext en'
31
32
33class TemplateError(Exception):
34    """Base exception class for errors related to template processing."""
35
36    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
37        """Create the exception.
38       
39        :param message: the error message
40        :param filename: the filename of the template
41        :param lineno: the number of line in the template at which the error
42                       occurred
43        :param offset: the column number at which the error occurred
44        """
45        self.msg = message #: the error message string
46        if filename != '<string>' or lineno >= 0:
47            message = '%s (%s, line %d)' % (self.msg, filename, lineno)
48        Exception.__init__(self, message)
49        self.filename = filename #: the name of the template file
50        self.lineno = lineno #: the number of the line containing the error
51        self.offset = offset #: the offset on the line
52
53
54class TemplateSyntaxError(TemplateError):
55    """Exception raised when an expression in a template causes a Python syntax
56    error, or the template is not well-formed.
57    """
58
59    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
60        """Create the exception
61       
62        :param message: the error message
63        :param filename: the filename of the template
64        :param lineno: the number of line in the template at which the error
65                       occurred
66        :param offset: the column number at which the error occurred
67        """
68        if isinstance(message, SyntaxError) and message.lineno is not None:
69            message = str(message).replace(' (line %d)' % message.lineno, '')
70        TemplateError.__init__(self, message, filename, lineno)
71
72
73class BadDirectiveError(TemplateSyntaxError):
74    """Exception raised when an unknown directive is encountered when parsing
75    a template.
76   
77    An unknown directive is any attribute using the namespace for directives,
78    with a local name that doesn't match any registered directive.
79    """
80
81    def __init__(self, name, filename='<string>', lineno=-1):
82        """Create the exception
83       
84        :param name: the name of the directive
85        :param filename: the filename of the template
86        :param lineno: the number of line in the template at which the error
87                       occurred
88        """
89        TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name,
90                                     filename, lineno)
91
92
93class TemplateRuntimeError(TemplateError):
94    """Exception raised when an the evaluation of a Python expression in a
95    template causes an error.
96    """
97
98
99class Context(object):
100    """Container for template input data.
101   
102    A context provides a stack of scopes (represented by dictionaries).
103   
104    Template directives such as loops can push a new scope on the stack with
105    data that should only be available inside the loop. When the loop
106    terminates, that scope can get popped off the stack again.
107   
108    >>> ctxt = Context(one='foo', other=1)
109    >>> ctxt.get('one')
110    'foo'
111    >>> ctxt.get('other')
112    1
113    >>> ctxt.push(dict(one='frost'))
114    >>> ctxt.get('one')
115    'frost'
116    >>> ctxt.get('other')
117    1
118    >>> ctxt.pop()
119    {'one': 'frost'}
120    >>> ctxt.get('one')
121    'foo'
122    """
123
124    def __init__(self, **data):
125        """Initialize the template context with the given keyword arguments as
126        data.
127        """
128        self.frames = deque([data])
129        self.pop = self.frames.popleft
130        self.push = self.frames.appendleft
131        self._match_templates = []
132
133        # Helper functions for use in expressions
134        def defined(name):
135            """Return whether a variable with the specified name exists in the
136            expression scope."""
137            return name in self
138        def value_of(name, default=None):
139            """If a variable of the specified name is defined, return its value.
140            Otherwise, return the provided default value, or ``None``."""
141            return self.get(name, default)
142        data.setdefault('defined', defined)
143        data.setdefault('value_of', value_of)
144
145    def __repr__(self):
146        return repr(list(self.frames))
147
148    def __contains__(self, key):
149        """Return whether a variable exists in any of the scopes.
150       
151        :param key: the name of the variable
152        """
153        return self._find(key)[1] is not None
154
155    def __delitem__(self, key):
156        """Remove a variable from all scopes.
157       
158        :param key: the name of the variable
159        """
160        for frame in self.frames:
161            if key in frame:
162                del frame[key]
163
164    def __getitem__(self, key):
165        """Get a variables's value, starting at the current scope and going
166        upward.
167       
168        :param key: the name of the variable
169        :return: the variable value
170        :raises KeyError: if the requested variable wasn't found in any scope
171        """
172        value, frame = self._find(key)
173        if frame is None:
174            raise KeyError(key)
175        return value
176
177    def __len__(self):
178        """Return the number of distinctly named variables in the context.
179       
180        :return: the number of variables in the context
181        """
182        return len(self.items())
183
184    def __setitem__(self, key, value):
185        """Set a variable in the current scope.
186       
187        :param key: the name of the variable
188        :param value: the variable value
189        """
190        self.frames[0][key] = value
191
192    def _find(self, key, default=None):
193        """Retrieve a given variable's value and the frame it was found in.
194
195        Intended primarily for internal use by directives.
196       
197        :param key: the name of the variable
198        :param default: the default value to return when the variable is not
199                        found
200        """
201        for frame in self.frames:
202            if key in frame:
203                return frame[key], frame
204        return default, None
205
206    def get(self, key, default=None):
207        """Get a variable's value, starting at the current scope and going
208        upward.
209       
210        :param key: the name of the variable
211        :param default: the default value to return when the variable is not
212                        found
213        """
214        for frame in self.frames:
215            if key in frame:
216                return frame[key]
217        return default
218
219    def keys(self):
220        """Return the name of all variables in the context.
221       
222        :return: a list of variable names
223        """
224        keys = []
225        for frame in self.frames:
226            keys += [key for key in frame if key not in keys]
227        return keys
228
229    def items(self):
230        """Return a list of ``(name, value)`` tuples for all variables in the
231        context.
232       
233        :return: a list of variables
234        """
235        return [(key, self.get(key)) for key in self.keys()]
236
237    def push(self, data):
238        """Push a new scope on the stack.
239       
240        :param data: the data dictionary to push on the context stack.
241        """
242
243    def pop(self):
244        """Pop the top-most scope from the stack."""
245
246
247def _apply_directives(stream, ctxt, directives):
248    """Apply the given directives to the stream.
249   
250    :param stream: the stream the directives should be applied to
251    :param ctxt: the `Context`
252    :param directives: the list of directives to apply
253    :return: the stream with the given directives applied
254    """
255    if directives:
256        stream = directives[0](iter(stream), ctxt, directives[1:])
257    return stream
258
259
260class TemplateMeta(type):
261    """Meta class for templates."""
262
263    def __new__(cls, name, bases, d):
264        if 'directives' in d:
265            d['_dir_by_name'] = dict(d['directives'])
266            d['_dir_order'] = [directive[1] for directive in d['directives']]
267
268        return type.__new__(cls, name, bases, d)
269
270
271class Template(object):
272    """Abstract template base class.
273   
274    This class implements most of the template processing model, but does not
275    specify the syntax of templates.
276    """
277    __metaclass__ = TemplateMeta
278
279    EXPR = StreamEventKind('EXPR')
280    """Stream event kind representing a Python expression."""
281
282    SUB = StreamEventKind('SUB')
283    """Stream event kind representing a nested stream to which one or more
284    directives should be applied.
285    """
286
287    def __init__(self, source, basedir=None, filename=None, loader=None,
288                 encoding=None, lookup='lenient'):
289        """Initialize a template from either a string, a file-like object, or
290        an already parsed markup stream.
291       
292        :param source: a string, file-like object, or markup stream to read the
293                       template from
294        :param basedir: the base directory containing the template file; when
295                        loaded from a `TemplateLoader`, this will be the
296                        directory on the template search path in which the
297                        template was found
298        :param filename: the name of the template file, relative to the given
299                         base directory
300        :param loader: the `TemplateLoader` to use for loading included
301                       templates
302        :param encoding: the encoding of the `source`
303        :param lookup: the variable lookup mechanism; either "lenient" (the
304                       default), "strict", or a custom lookup class
305        """
306        self.basedir = basedir
307        self.filename = filename
308        if basedir and filename:
309            self.filepath = os.path.join(basedir, filename)
310        else:
311            self.filepath = filename
312        self.loader = loader
313        self.lookup = lookup
314
315        if isinstance(source, basestring):
316            source = StringIO(source)
317        else:
318            source = source
319        try:
320            self.stream = list(self._prepare(self._parse(source, encoding)))
321        except ParseError, e:
322            raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
323        self.filters = [self._flatten, self._eval]
324
325    def __repr__(self):
326        return '<%s "%s">' % (self.__class__.__name__, self.filename)
327
328    def _parse(self, source, encoding):
329        """Parse the template.
330       
331        The parsing stage parses the template and constructs a list of
332        directives that will be executed in the render stage. The input is
333        split up into literal output (text that does not depend on the context
334        data) and directives or expressions.
335       
336        :param source: a file-like object containing the XML source of the
337                       template, or an XML event stream
338        :param encoding: the encoding of the `source`
339        """
340        raise NotImplementedError
341
342    def _prepare(self, stream):
343        """Call the `attach` method of every directive found in the template.
344       
345        :param stream: the event stream of the template
346        """
347        for kind, data, pos in stream:
348            if kind is SUB:
349                directives = []
350                substream = data[1]
351                for cls, value, namespaces, pos in data[0]:
352                    directive, substream = cls.attach(self, substream, value,
353                                                      namespaces, pos)
354                    if directive:
355                        directives.append(directive)
356                substream = self._prepare(substream)
357                if directives:
358                    yield kind, (directives, list(substream)), pos
359                else:
360                    for event in substream:
361                        yield event
362            else:
363                yield kind, data, pos
364
365    def generate(self, *args, **kwargs):
366        """Apply the template to the given context data.
367       
368        Any keyword arguments are made available to the template as context
369        data.
370       
371        Only one positional argument is accepted: if it is provided, it must be
372        an instance of the `Context` class, and keyword arguments are ignored.
373        This calling style is used for internal processing.
374       
375        :return: a markup event stream representing the result of applying
376                 the template to the context data.
377        """
378        if args:
379            assert len(args) == 1
380            ctxt = args[0]
381            if ctxt is None:
382                ctxt = Context(**kwargs)
383            assert isinstance(ctxt, Context)
384        else:
385            ctxt = Context(**kwargs)
386
387        stream = self.stream
388        for filter_ in self.filters:
389            stream = filter_(iter(stream), ctxt)
390        return Stream(stream)
391
392    def _eval(self, stream, ctxt):
393        """Internal stream filter that evaluates any expressions in `START` and
394        `TEXT` events.
395        """
396        filters = (self._flatten, self._eval)
397
398        for kind, data, pos in stream:
399
400            if kind is START and data[1]:
401                # Attributes may still contain expressions in start tags at
402                # this point, so do some evaluation
403                tag, attrs = data
404                new_attrs = []
405                for name, substream in attrs:
406                    if isinstance(substream, basestring):
407                        value = substream
408                    else:
409                        values = []
410                        for subkind, subdata, subpos in self._eval(substream,
411                                                                   ctxt):
412                            if subkind is TEXT:
413                                values.append(subdata)
414                        value = [x for x in values if x is not None]
415                        if not value:
416                            continue
417                    new_attrs.append((name, u''.join(value)))
418                yield kind, (tag, Attrs(new_attrs)), pos
419
420            elif kind is EXPR:
421                result = data.evaluate(ctxt)
422                if result is not None:
423                    # First check for a string, otherwise the iterable test below
424                    # succeeds, and the string will be chopped up into individual
425                    # characters
426                    if isinstance(result, basestring):
427                        yield TEXT, result, pos
428                    elif hasattr(result, '__iter__'):
429                        substream = _ensure(result)
430                        for filter_ in filters:
431                            substream = filter_(substream, ctxt)
432                        for event in substream:
433                            yield event
434                    else:
435                        yield TEXT, unicode(result), pos
436
437            else:
438                yield kind, data, pos
439
440    def _flatten(self, stream, ctxt):
441        """Internal stream filter that expands `SUB` events in the stream."""
442        for event in stream:
443            if event[0] is SUB:
444                # This event is a list of directives and a list of nested
445                # events to which those directives should be applied
446                directives, substream = event[1]
447                substream = _apply_directives(substream, ctxt, directives)
448                for event in self._flatten(substream, ctxt):
449                    yield event
450            else:
451                yield event
452
453
454EXPR = Template.EXPR
455SUB = Template.SUB
Note: See TracBrowser for help on using the repository browser.