Edgewall Software

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

Last change on this file was 855, checked in by cmlenz, 15 years ago

Workaround for a Python 2.4 bug that broke star imports in template code blocks. Closes #221. Many thanks to Armin Ronacher for the patch.

  • Property svn:eol-style set to native
File size: 21.2 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2008 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
24import sys
25
26from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
27from genshi.input import ParseError
28
29__all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError',
30           'TemplateSyntaxError', 'BadDirectiveError']
31__docformat__ = 'restructuredtext en'
32
33if sys.version_info < (2, 4):
34    _ctxt2dict = lambda ctxt: ctxt.frames[0]
35else:
36    _ctxt2dict = lambda ctxt: ctxt
37
38
39class TemplateError(Exception):
40    """Base exception class for errors related to template processing."""
41
42    def __init__(self, message, filename=None, lineno=-1, offset=-1):
43        """Create the exception.
44       
45        :param message: the error message
46        :param filename: the filename of the template
47        :param lineno: the number of line in the template at which the error
48                       occurred
49        :param offset: the column number at which the error occurred
50        """
51        if filename is None:
52            filename = '<string>'
53        self.msg = message #: the error message string
54        if filename != '<string>' or lineno >= 0:
55            message = '%s (%s, line %d)' % (self.msg, filename, lineno)
56        Exception.__init__(self, message)
57        self.filename = filename #: the name of the template file
58        self.lineno = lineno #: the number of the line containing the error
59        self.offset = offset #: the offset on the line
60
61
62class TemplateSyntaxError(TemplateError):
63    """Exception raised when an expression in a template causes a Python syntax
64    error, or the template is not well-formed.
65    """
66
67    def __init__(self, message, filename=None, lineno=-1, offset=-1):
68        """Create the exception
69       
70        :param message: the error message
71        :param filename: the filename of the template
72        :param lineno: the number of line in the template at which the error
73                       occurred
74        :param offset: the column number at which the error occurred
75        """
76        if isinstance(message, SyntaxError) and message.lineno is not None:
77            message = str(message).replace(' (line %d)' % message.lineno, '')
78        TemplateError.__init__(self, message, filename, lineno)
79
80
81class BadDirectiveError(TemplateSyntaxError):
82    """Exception raised when an unknown directive is encountered when parsing
83    a template.
84   
85    An unknown directive is any attribute using the namespace for directives,
86    with a local name that doesn't match any registered directive.
87    """
88
89    def __init__(self, name, filename=None, lineno=-1):
90        """Create the exception
91       
92        :param name: the name of the directive
93        :param filename: the filename of the template
94        :param lineno: the number of line in the template at which the error
95                       occurred
96        """
97        TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name,
98                                     filename, lineno)
99
100
101class TemplateRuntimeError(TemplateError):
102    """Exception raised when an the evaluation of a Python expression in a
103    template causes an error.
104    """
105
106
107class Context(object):
108    """Container for template input data.
109   
110    A context provides a stack of scopes (represented by dictionaries).
111   
112    Template directives such as loops can push a new scope on the stack with
113    data that should only be available inside the loop. When the loop
114    terminates, that scope can get popped off the stack again.
115   
116    >>> ctxt = Context(one='foo', other=1)
117    >>> ctxt.get('one')
118    'foo'
119    >>> ctxt.get('other')
120    1
121    >>> ctxt.push(dict(one='frost'))
122    >>> ctxt.get('one')
123    'frost'
124    >>> ctxt.get('other')
125    1
126    >>> ctxt.pop()
127    {'one': 'frost'}
128    >>> ctxt.get('one')
129    'foo'
130    """
131
132    def __init__(self, **data):
133        """Initialize the template context with the given keyword arguments as
134        data.
135        """
136        self.frames = deque([data])
137        self.pop = self.frames.popleft
138        self.push = self.frames.appendleft
139        self._match_templates = []
140        self._choice_stack = []
141
142        # Helper functions for use in expressions
143        def defined(name):
144            """Return whether a variable with the specified name exists in the
145            expression scope."""
146            return name in self
147        def value_of(name, default=None):
148            """If a variable of the specified name is defined, return its value.
149            Otherwise, return the provided default value, or ``None``."""
150            return self.get(name, default)
151        data.setdefault('defined', defined)
152        data.setdefault('value_of', value_of)
153
154    def __repr__(self):
155        return repr(list(self.frames))
156
157    def __contains__(self, key):
158        """Return whether a variable exists in any of the scopes.
159       
160        :param key: the name of the variable
161        """
162        return self._find(key)[1] is not None
163    has_key = __contains__
164
165    def __delitem__(self, key):
166        """Remove a variable from all scopes.
167       
168        :param key: the name of the variable
169        """
170        for frame in self.frames:
171            if key in frame:
172                del frame[key]
173
174    def __getitem__(self, key):
175        """Get a variables's value, starting at the current scope and going
176        upward.
177       
178        :param key: the name of the variable
179        :return: the variable value
180        :raises KeyError: if the requested variable wasn't found in any scope
181        """
182        value, frame = self._find(key)
183        if frame is None:
184            raise KeyError(key)
185        return value
186
187    def __len__(self):
188        """Return the number of distinctly named variables in the context.
189       
190        :return: the number of variables in the context
191        """
192        return len(self.items())
193
194    def __setitem__(self, key, value):
195        """Set a variable in the current scope.
196       
197        :param key: the name of the variable
198        :param value: the variable value
199        """
200        self.frames[0][key] = value
201
202    def _find(self, key, default=None):
203        """Retrieve a given variable's value and the frame it was found in.
204
205        Intended primarily for internal use by directives.
206       
207        :param key: the name of the variable
208        :param default: the default value to return when the variable is not
209                        found
210        """
211        for frame in self.frames:
212            if key in frame:
213                return frame[key], frame
214        return default, None
215
216    def get(self, key, default=None):
217        """Get a variable's value, starting at the current scope and going
218        upward.
219       
220        :param key: the name of the variable
221        :param default: the default value to return when the variable is not
222                        found
223        """
224        for frame in self.frames:
225            if key in frame:
226                return frame[key]
227        return default
228
229    def keys(self):
230        """Return the name of all variables in the context.
231       
232        :return: a list of variable names
233        """
234        keys = []
235        for frame in self.frames:
236            keys += [key for key in frame if key not in keys]
237        return keys
238
239    def items(self):
240        """Return a list of ``(name, value)`` tuples for all variables in the
241        context.
242       
243        :return: a list of variables
244        """
245        return [(key, self.get(key)) for key in self.keys()]
246
247    def update(self, mapping):
248        """Update the context from the mapping provided."""
249        self.frames[0].update(mapping)
250
251    def push(self, data):
252        """Push a new scope on the stack.
253       
254        :param data: the data dictionary to push on the context stack.
255        """
256
257    def pop(self):
258        """Pop the top-most scope from the stack."""
259
260
261def _apply_directives(stream, directives, ctxt, **vars):
262    """Apply the given directives to the stream.
263   
264    :param stream: the stream the directives should be applied to
265    :param directives: the list of directives to apply
266    :param ctxt: the `Context`
267    :param vars: additional variables that should be available when Python
268                 code is executed
269    :return: the stream with the given directives applied
270    """
271    if directives:
272        stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
273    return stream
274
275def _eval_expr(expr, ctxt, **vars):
276    """Evaluate the given `Expression` object.
277   
278    :param expr: the expression to evaluate
279    :param ctxt: the `Context`
280    :param vars: additional variables that should be available to the
281                 expression
282    :return: the result of the evaluation
283    """
284    if vars:
285        ctxt.push(vars)
286    retval = expr.evaluate(ctxt)
287    if vars:
288        ctxt.pop()
289    return retval
290
291def _exec_suite(suite, ctxt, **vars):
292    """Execute the given `Suite` object.
293   
294    :param suite: the code suite to execute
295    :param ctxt: the `Context`
296    :param vars: additional variables that should be available to the
297                 code
298    """
299    if vars:
300        ctxt.push(vars)
301        ctxt.push({})
302    suite.execute(_ctxt2dict(ctxt))
303    if vars:
304        top = ctxt.pop()
305        ctxt.pop()
306        ctxt.frames[0].update(top)
307
308
309class TemplateMeta(type):
310    """Meta class for templates."""
311
312    def __new__(cls, name, bases, d):
313        if 'directives' in d:
314            d['_dir_by_name'] = dict(d['directives'])
315            d['_dir_order'] = [directive[1] for directive in d['directives']]
316
317        return type.__new__(cls, name, bases, d)
318
319
320class Template(object):
321    """Abstract template base class.
322   
323    This class implements most of the template processing model, but does not
324    specify the syntax of templates.
325    """
326    __metaclass__ = TemplateMeta
327
328    EXEC = StreamEventKind('EXEC')
329    """Stream event kind representing a Python code suite to execute."""
330
331    EXPR = StreamEventKind('EXPR')
332    """Stream event kind representing a Python expression."""
333
334    INCLUDE = StreamEventKind('INCLUDE')
335    """Stream event kind representing the inclusion of another template."""
336
337    SUB = StreamEventKind('SUB')
338    """Stream event kind representing a nested stream to which one or more
339    directives should be applied.
340    """
341
342    serializer = None
343    _number_conv = unicode # function used to convert numbers to event data
344
345    def __init__(self, source, filepath=None, filename=None, loader=None,
346                 encoding=None, lookup='strict', allow_exec=True):
347        """Initialize a template from either a string, a file-like object, or
348        an already parsed markup stream.
349       
350        :param source: a string, file-like object, or markup stream to read the
351                       template from
352        :param filepath: the absolute path to the template file
353        :param filename: the path to the template file relative to the search
354                         path
355        :param loader: the `TemplateLoader` to use for loading included
356                       templates
357        :param encoding: the encoding of the `source`
358        :param lookup: the variable lookup mechanism; either "strict" (the
359                       default), "lenient", or a custom lookup class
360        :param allow_exec: whether Python code blocks in templates should be
361                           allowed
362       
363        :note: Changed in 0.5: Added the `allow_exec` argument
364        """
365        self.filepath = filepath or filename
366        self.filename = filename
367        self.loader = loader
368        self.lookup = lookup
369        self.allow_exec = allow_exec
370        self._init_filters()
371
372        if isinstance(source, basestring):
373            source = StringIO(source)
374        else:
375            source = source
376        try:
377            self.stream = list(self._prepare(self._parse(source, encoding)))
378        except ParseError, e:
379            raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
380
381    def __getstate__(self):
382        state = self.__dict__.copy()
383        state['filters'] = []
384        return state
385
386    def __setstate__(self, state):
387        self.__dict__ = state
388        self._init_filters()
389
390    def __repr__(self):
391        return '<%s "%s">' % (self.__class__.__name__, self.filename)
392
393    def _init_filters(self):
394        self.filters = [self._flatten, self._eval, self._exec]
395        if self.loader:
396            self.filters.append(self._include)
397
398    def _parse(self, source, encoding):
399        """Parse the template.
400       
401        The parsing stage parses the template and constructs a list of
402        directives that will be executed in the render stage. The input is
403        split up into literal output (text that does not depend on the context
404        data) and directives or expressions.
405       
406        :param source: a file-like object containing the XML source of the
407                       template, or an XML event stream
408        :param encoding: the encoding of the `source`
409        """
410        raise NotImplementedError
411
412    def _prepare(self, stream):
413        """Call the `attach` method of every directive found in the template.
414       
415        :param stream: the event stream of the template
416        """
417        from genshi.template.loader import TemplateNotFound
418
419        for kind, data, pos in stream:
420            if kind is SUB:
421                directives = []
422                substream = data[1]
423                for cls, value, namespaces, pos in data[0]:
424                    directive, substream = cls.attach(self, substream, value,
425                                                      namespaces, pos)
426                    if directive:
427                        directives.append(directive)
428                substream = self._prepare(substream)
429                if directives:
430                    yield kind, (directives, list(substream)), pos
431                else:
432                    for event in substream:
433                        yield event
434            else:
435                if kind is INCLUDE:
436                    href, cls, fallback = data
437                    if isinstance(href, basestring) and \
438                            not getattr(self.loader, 'auto_reload', True):
439                        # If the path to the included template is static, and
440                        # auto-reloading is disabled on the template loader,
441                        # the template is inlined into the stream
442                        try:
443                            tmpl = self.loader.load(href, relative_to=pos[0],
444                                                    cls=cls or self.__class__)
445                            for event in tmpl.stream:
446                                yield event
447                        except TemplateNotFound:
448                            if fallback is None:
449                                raise
450                            for event in self._prepare(fallback):
451                                yield event
452                        continue
453                    elif fallback:
454                        # Otherwise the include is performed at run time
455                        data = href, cls, list(self._prepare(fallback))
456
457                yield kind, data, pos
458
459    def generate(self, *args, **kwargs):
460        """Apply the template to the given context data.
461       
462        Any keyword arguments are made available to the template as context
463        data.
464       
465        Only one positional argument is accepted: if it is provided, it must be
466        an instance of the `Context` class, and keyword arguments are ignored.
467        This calling style is used for internal processing.
468       
469        :return: a markup event stream representing the result of applying
470                 the template to the context data.
471        """
472        vars = {}
473        if args:
474            assert len(args) == 1
475            ctxt = args[0]
476            if ctxt is None:
477                ctxt = Context(**kwargs)
478            else:
479                vars = kwargs
480            assert isinstance(ctxt, Context)
481        else:
482            ctxt = Context(**kwargs)
483
484        stream = self.stream
485        for filter_ in self.filters:
486            stream = filter_(iter(stream), ctxt, **vars)
487        return Stream(stream, self.serializer)
488
489    def _eval(self, stream, ctxt, **vars):
490        """Internal stream filter that evaluates any expressions in `START` and
491        `TEXT` events.
492        """
493        filters = (self._flatten, self._eval)
494        number_conv = self._number_conv
495
496        for kind, data, pos in stream:
497
498            if kind is START and data[1]:
499                # Attributes may still contain expressions in start tags at
500                # this point, so do some evaluation
501                tag, attrs = data
502                new_attrs = []
503                for name, substream in attrs:
504                    if isinstance(substream, basestring):
505                        value = substream
506                    else:
507                        values = []
508                        for subkind, subdata, subpos in self._eval(substream,
509                                                                   ctxt,
510                                                                   **vars):
511                            if subkind is TEXT:
512                                values.append(subdata)
513                        value = [x for x in values if x is not None]
514                        if not value:
515                            continue
516                    new_attrs.append((name, u''.join(value)))
517                yield kind, (tag, Attrs(new_attrs)), pos
518
519            elif kind is EXPR:
520                result = _eval_expr(data, ctxt, **vars)
521                if result is not None:
522                    # First check for a string, otherwise the iterable test
523                    # below succeeds, and the string will be chopped up into
524                    # individual characters
525                    if isinstance(result, basestring):
526                        yield TEXT, result, pos
527                    elif isinstance(result, (int, float, long)):
528                        yield TEXT, number_conv(result), pos
529                    elif hasattr(result, '__iter__'):
530                        substream = _ensure(result)
531                        for filter_ in filters:
532                            substream = filter_(substream, ctxt, **vars)
533                        for event in substream:
534                            yield event
535                    else:
536                        yield TEXT, unicode(result), pos
537
538            else:
539                yield kind, data, pos
540
541    def _exec(self, stream, ctxt, **vars):
542        """Internal stream filter that executes Python code blocks."""
543        for event in stream:
544            if event[0] is EXEC:
545                _exec_suite(event[1], ctxt, **vars)
546            else:
547                yield event
548
549    def _flatten(self, stream, ctxt, **vars):
550        """Internal stream filter that expands `SUB` events in the stream."""
551        for event in stream:
552            if event[0] is SUB:
553                # This event is a list of directives and a list of nested
554                # events to which those directives should be applied
555                directives, substream = event[1]
556                substream = _apply_directives(substream, directives, ctxt,
557                                              **vars)
558                for event in self._flatten(substream, ctxt, **vars):
559                    yield event
560            else:
561                yield event
562
563    def _include(self, stream, ctxt, **vars):
564        """Internal stream filter that performs inclusion of external
565        template files.
566        """
567        from genshi.template.loader import TemplateNotFound
568
569        for event in stream:
570            if event[0] is INCLUDE:
571                href, cls, fallback = event[1]
572                if not isinstance(href, basestring):
573                    parts = []
574                    for subkind, subdata, subpos in self._eval(href, ctxt,
575                                                               **vars):
576                        if subkind is TEXT:
577                            parts.append(subdata)
578                    href = u''.join([x for x in parts if x is not None])
579                try:
580                    tmpl = self.loader.load(href, relative_to=event[2][0],
581                                            cls=cls or self.__class__)
582                    for event in tmpl.generate(ctxt, **vars):
583                        yield event
584                except TemplateNotFound:
585                    if fallback is None:
586                        raise
587                    for filter_ in self.filters:
588                        fallback = filter_(iter(fallback), ctxt, **vars)
589                    for event in fallback:
590                        yield event
591            else:
592                yield event
593
594
595EXEC = Template.EXEC
596EXPR = Template.EXPR
597INCLUDE = Template.INCLUDE
598SUB = Template.SUB
Note: See TracBrowser for help on using the repository browser.