Edgewall Software

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

Last change on this file was 1259, checked in by hodgestar, 10 years ago

Merge r1257 from trunk (fix for infinite template inlining).

  • Property svn:eol-style set to native
File size: 23.5 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2010 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
16from collections import deque
17import os
18from StringIO import StringIO
19import sys
20
21from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
22from genshi.input import ParseError
23
24__all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError',
25           'TemplateRuntimeError', 'TemplateSyntaxError', 'BadDirectiveError']
26__docformat__ = 'restructuredtext en'
27
28
29class TemplateError(Exception):
30    """Base exception class for errors related to template processing."""
31
32    def __init__(self, message, filename=None, lineno=-1, offset=-1):
33        """Create the exception.
34       
35        :param message: the error message
36        :param filename: the filename of the template
37        :param lineno: the number of line in the template at which the error
38                       occurred
39        :param offset: the column number at which the error occurred
40        """
41        if filename is None:
42            filename = '<string>'
43        self.msg = message #: the error message string
44        if filename != '<string>' or lineno >= 0:
45            message = '%s (%s, line %d)' % (self.msg, filename, lineno)
46        Exception.__init__(self, message)
47        self.filename = filename #: the name of the template file
48        self.lineno = lineno #: the number of the line containing the error
49        self.offset = offset #: the offset on the line
50
51
52class TemplateSyntaxError(TemplateError):
53    """Exception raised when an expression in a template causes a Python syntax
54    error, or the template is not well-formed.
55    """
56
57    def __init__(self, message, filename=None, lineno=-1, offset=-1):
58        """Create the exception
59       
60        :param message: the error message
61        :param filename: the filename of the template
62        :param lineno: the number of line in the template at which the error
63                       occurred
64        :param offset: the column number at which the error occurred
65        """
66        if isinstance(message, SyntaxError) and message.lineno is not None:
67            message = str(message).replace(' (line %d)' % message.lineno, '')
68        TemplateError.__init__(self, message, filename, lineno)
69
70
71class BadDirectiveError(TemplateSyntaxError):
72    """Exception raised when an unknown directive is encountered when parsing
73    a template.
74   
75    An unknown directive is any attribute using the namespace for directives,
76    with a local name that doesn't match any registered directive.
77    """
78
79    def __init__(self, name, filename=None, lineno=-1):
80        """Create the exception
81       
82        :param name: the name of the directive
83        :param filename: the filename of the template
84        :param lineno: the number of line in the template at which the error
85                       occurred
86        """
87        TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name,
88                                     filename, lineno)
89
90
91class TemplateRuntimeError(TemplateError):
92    """Exception raised when an the evaluation of a Python expression in a
93    template causes an error.
94    """
95
96
97class Context(object):
98    """Container for template input data.
99   
100    A context provides a stack of scopes (represented by dictionaries).
101   
102    Template directives such as loops can push a new scope on the stack with
103    data that should only be available inside the loop. When the loop
104    terminates, that scope can get popped off the stack again.
105   
106    >>> ctxt = Context(one='foo', other=1)
107    >>> ctxt.get('one')
108    'foo'
109    >>> ctxt.get('other')
110    1
111    >>> ctxt.push(dict(one='frost'))
112    >>> ctxt.get('one')
113    'frost'
114    >>> ctxt.get('other')
115    1
116    >>> ctxt.pop()
117    {'one': 'frost'}
118    >>> ctxt.get('one')
119    'foo'
120    """
121
122    def __init__(self, **data):
123        """Initialize the template context with the given keyword arguments as
124        data.
125        """
126        self.frames = deque([data])
127        self.pop = self.frames.popleft
128        self.push = self.frames.appendleft
129        self._match_templates = []
130        self._choice_stack = []
131
132        # Helper functions for use in expressions
133        def defined(name):
134            """Return whether a variable with the specified name exists in the
135            expression scope."""
136            return name in self
137        def value_of(name, default=None):
138            """If a variable of the specified name is defined, return its value.
139            Otherwise, return the provided default value, or ``None``."""
140            return self.get(name, default)
141        data.setdefault('defined', defined)
142        data.setdefault('value_of', value_of)
143
144    def __repr__(self):
145        return repr(list(self.frames))
146
147    def __contains__(self, key):
148        """Return whether a variable exists in any of the scopes.
149       
150        :param key: the name of the variable
151        """
152        return self._find(key)[1] is not None
153    has_key = __contains__
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 update(self, mapping):
238        """Update the context from the mapping provided."""
239        self.frames[0].update(mapping)
240
241    def push(self, data):
242        """Push a new scope on the stack.
243       
244        :param data: the data dictionary to push on the context stack.
245        """
246
247    def pop(self):
248        """Pop the top-most scope from the stack."""
249
250    def copy(self):
251        """Create a copy of this Context object."""
252        # required to make f_locals a dict-like object
253        # See http://genshi.edgewall.org/ticket/249 for
254        # example use case in Twisted tracebacks
255        ctxt = Context()
256        ctxt.frames.pop()  # pop empty dummy context
257        ctxt.frames.extend(self.frames)
258        ctxt._match_templates.extend(self._match_templates)
259        ctxt._choice_stack.extend(self._choice_stack)
260        return ctxt
261
262
263def _apply_directives(stream, directives, ctxt, vars):
264    """Apply the given directives to the stream.
265   
266    :param stream: the stream the directives should be applied to
267    :param directives: the list of directives to apply
268    :param ctxt: the `Context`
269    :param vars: additional variables that should be available when Python
270                 code is executed
271    :return: the stream with the given directives applied
272    """
273    if directives:
274        stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
275    return stream
276
277
278def _eval_expr(expr, ctxt, vars=None):
279    """Evaluate the given `Expression` object.
280   
281    :param expr: the expression to evaluate
282    :param ctxt: the `Context`
283    :param vars: additional variables that should be available to the
284                 expression
285    :return: the result of the evaluation
286    """
287    if vars:
288        ctxt.push(vars)
289    retval = expr.evaluate(ctxt)
290    if vars:
291        ctxt.pop()
292    return retval
293
294
295def _exec_suite(suite, ctxt, vars=None):
296    """Execute the given `Suite` object.
297   
298    :param suite: the code suite to execute
299    :param ctxt: the `Context`
300    :param vars: additional variables that should be available to the
301                 code
302    """
303    if vars:
304        ctxt.push(vars)
305        ctxt.push({})
306    suite.execute(ctxt)
307    if vars:
308        top = ctxt.pop()
309        ctxt.pop()
310        ctxt.frames[0].update(top)
311
312
313class DirectiveFactoryMeta(type):
314    """Meta class for directive factories."""
315
316    def __new__(cls, name, bases, d):
317        if 'directives' in d:
318            d['_dir_by_name'] = dict(d['directives'])
319            d['_dir_order'] = [directive[1] for directive in d['directives']]
320
321        return type.__new__(cls, name, bases, d)
322
323
324class DirectiveFactory(object):
325    """Base for classes that provide a set of template directives.
326   
327    :since: version 0.6
328    """
329    __metaclass__ = DirectiveFactoryMeta
330
331    directives = []
332    """A list of ``(name, cls)`` tuples that define the set of directives
333    provided by this factory.
334    """
335
336    def get_directive(self, name):
337        """Return the directive class for the given name.
338       
339        :param name: the directive name as used in the template
340        :return: the directive class
341        :see: `Directive`
342        """
343        return self._dir_by_name.get(name)
344
345    def get_directive_index(self, dir_cls):
346        """Return a key for the given directive class that should be used to
347        sort it among other directives on the same `SUB` event.
348       
349        The default implementation simply returns the index of the directive in
350        the `directives` list.
351       
352        :param dir_cls: the directive class
353        :return: the sort key
354        """
355        if dir_cls in self._dir_order:
356            return self._dir_order.index(dir_cls)
357        return len(self._dir_order)
358
359
360class Template(DirectiveFactory):
361    """Abstract template base class.
362   
363    This class implements most of the template processing model, but does not
364    specify the syntax of templates.
365    """
366
367    EXEC = StreamEventKind('EXEC')
368    """Stream event kind representing a Python code suite to execute."""
369
370    EXPR = StreamEventKind('EXPR')
371    """Stream event kind representing a Python expression."""
372
373    INCLUDE = StreamEventKind('INCLUDE')
374    """Stream event kind representing the inclusion of another template."""
375
376    SUB = StreamEventKind('SUB')
377    """Stream event kind representing a nested stream to which one or more
378    directives should be applied.
379    """
380
381    serializer = None
382    _number_conv = unicode # function used to convert numbers to event data
383
384    def __init__(self, source, filepath=None, filename=None, loader=None,
385                 encoding=None, lookup='strict', allow_exec=True):
386        """Initialize a template from either a string, a file-like object, or
387        an already parsed markup stream.
388       
389        :param source: a string, file-like object, or markup stream to read the
390                       template from
391        :param filepath: the absolute path to the template file
392        :param filename: the path to the template file relative to the search
393                         path
394        :param loader: the `TemplateLoader` to use for loading included
395                       templates
396        :param encoding: the encoding of the `source`
397        :param lookup: the variable lookup mechanism; either "strict" (the
398                       default), "lenient", or a custom lookup class
399        :param allow_exec: whether Python code blocks in templates should be
400                           allowed
401       
402        :note: Changed in 0.5: Added the `allow_exec` argument
403        """
404        self.filepath = filepath or filename
405        self.filename = filename
406        self.loader = loader
407        self.lookup = lookup
408        self.allow_exec = allow_exec
409        self._init_filters()
410        self._init_loader()
411        self._prepared = False
412
413        if isinstance(source, basestring):
414            source = StringIO(source)
415        else:
416            source = source
417        try:
418            self._stream = self._parse(source, encoding)
419        except ParseError, e:
420            raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
421
422    def __getstate__(self):
423        state = self.__dict__.copy()
424        state['filters'] = []
425        return state
426
427    def __setstate__(self, state):
428        self.__dict__ = state
429        self._init_filters()
430
431    def __repr__(self):
432        return '<%s "%s">' % (type(self).__name__, self.filename)
433
434    def _init_filters(self):
435        self.filters = [self._flatten, self._include]
436
437    def _init_loader(self):
438        if self.loader is None:
439            from genshi.template.loader import TemplateLoader
440            if self.filename:
441                if self.filepath != self.filename:
442                    basedir = os.path.normpath(self.filepath)[:-len(
443                        os.path.normpath(self.filename))
444                    ]
445                else:
446                    basedir = os.path.dirname(self.filename)
447            else:
448                basedir = '.'
449            self.loader = TemplateLoader([os.path.abspath(basedir)])
450
451    @property
452    def stream(self):
453        if not self._prepared:
454            self._prepare_self()
455        return self._stream
456
457    def _parse(self, source, encoding):
458        """Parse the template.
459       
460        The parsing stage parses the template and constructs a list of
461        directives that will be executed in the render stage. The input is
462        split up into literal output (text that does not depend on the context
463        data) and directives or expressions.
464       
465        :param source: a file-like object containing the XML source of the
466                       template, or an XML event stream
467        :param encoding: the encoding of the `source`
468        """
469        raise NotImplementedError
470
471    def _prepare_self(self, inlined=None):
472        if not self._prepared:
473            self._stream = list(self._prepare(self._stream, inlined))
474            self._prepared = True
475
476    def _prepare(self, stream, inlined):
477        """Call the `attach` method of every directive found in the template.
478       
479        :param stream: the event stream of the template
480        """
481        from genshi.template.loader import TemplateNotFound
482        if inlined is None:
483            inlined = set((self.filepath,))
484
485        for kind, data, pos in stream:
486            if kind is SUB:
487                directives = []
488                substream = data[1]
489                for _, cls, value, namespaces, pos in sorted(data[0]):
490                    directive, substream = cls.attach(self, substream, value,
491                                                      namespaces, pos)
492                    if directive:
493                        directives.append(directive)
494                substream = self._prepare(substream, inlined)
495                if directives:
496                    yield kind, (directives, list(substream)), pos
497                else:
498                    for event in substream:
499                        yield event
500            else:
501                if kind is INCLUDE:
502                    href, cls, fallback = data
503                    tmpl_inlined = False
504                    if (isinstance(href, basestring) and
505                            not getattr(self.loader, 'auto_reload', True)):
506                        # If the path to the included template is static, and
507                        # auto-reloading is disabled on the template loader,
508                        # the template is inlined into the stream provided it
509                        # is not already in the stack of templates being
510                        # processed.
511                        tmpl = None
512                        try:
513                            tmpl = self.loader.load(href, relative_to=pos[0],
514                                                    cls=cls or self.__class__)
515                        except TemplateNotFound:
516                            if fallback is None:
517                                raise
518                        if tmpl is not None:
519                            if tmpl.filepath not in inlined:
520                                inlined.add(tmpl.filepath)
521                                tmpl._prepare_self(inlined)
522                                for event in tmpl.stream:
523                                    yield event
524                                inlined.discard(tmpl.filepath)
525                                tmpl_inlined = True
526                        else:
527                            for event in self._prepare(fallback, inlined):
528                                yield event
529                            tmpl_inlined = True
530                    if tmpl_inlined:
531                        continue
532                    if fallback:
533                        # Otherwise the include is performed at run time
534                        data = href, cls, list(
535                            self._prepare(fallback, inlined))
536                    yield kind, data, pos
537                else:
538                    yield kind, data, pos
539
540    def generate(self, *args, **kwargs):
541        """Apply the template to the given context data.
542       
543        Any keyword arguments are made available to the template as context
544        data.
545       
546        Only one positional argument is accepted: if it is provided, it must be
547        an instance of the `Context` class, and keyword arguments are ignored.
548        This calling style is used for internal processing.
549       
550        :return: a markup event stream representing the result of applying
551                 the template to the context data.
552        """
553        vars = {}
554        if args:
555            assert len(args) == 1
556            ctxt = args[0]
557            if ctxt is None:
558                ctxt = Context(**kwargs)
559            else:
560                vars = kwargs
561            assert isinstance(ctxt, Context)
562        else:
563            ctxt = Context(**kwargs)
564
565        stream = self.stream
566        for filter_ in self.filters:
567            stream = filter_(iter(stream), ctxt, **vars)
568        return Stream(stream, self.serializer)
569
570    def _flatten(self, stream, ctxt, **vars):
571        number_conv = self._number_conv
572        stack = []
573        push = stack.append
574        pop = stack.pop
575        stream = iter(stream)
576
577        while 1:
578            for kind, data, pos in stream:
579
580                if kind is START and data[1]:
581                    # Attributes may still contain expressions in start tags at
582                    # this point, so do some evaluation
583                    tag, attrs = data
584                    new_attrs = []
585                    for name, value in attrs:
586                        if type(value) is list: # this is an interpolated string
587                            values = [event[1]
588                                for event in self._flatten(value, ctxt, **vars)
589                                if event[0] is TEXT and event[1] is not None
590                            ]
591                            if not values:
592                                continue
593                            value = ''.join(values)
594                        new_attrs.append((name, value))
595                    yield kind, (tag, Attrs(new_attrs)), pos
596
597                elif kind is EXPR:
598                    result = _eval_expr(data, ctxt, vars)
599                    if result is not None:
600                        # First check for a string, otherwise the iterable test
601                        # below succeeds, and the string will be chopped up into
602                        # individual characters
603                        if isinstance(result, basestring):
604                            yield TEXT, result, pos
605                        elif isinstance(result, (int, float, long)):
606                            yield TEXT, number_conv(result), pos
607                        elif hasattr(result, '__iter__'):
608                            push(stream)
609                            stream = _ensure(result)
610                            break
611                        else:
612                            yield TEXT, unicode(result), pos
613
614                elif kind is SUB:
615                    # This event is a list of directives and a list of nested
616                    # events to which those directives should be applied
617                    push(stream)
618                    stream = _apply_directives(data[1], data[0], ctxt, vars)
619                    break
620
621                elif kind is EXEC:
622                    _exec_suite(data, ctxt, vars)
623
624                else:
625                    yield kind, data, pos
626
627            else:
628                if not stack:
629                    break
630                stream = pop()
631
632    def _include(self, stream, ctxt, **vars):
633        """Internal stream filter that performs inclusion of external
634        template files.
635        """
636        from genshi.template.loader import TemplateNotFound
637
638        for event in stream:
639            if event[0] is INCLUDE:
640                href, cls, fallback = event[1]
641                if not isinstance(href, basestring):
642                    parts = []
643                    for subkind, subdata, subpos in self._flatten(href, ctxt,
644                                                                  **vars):
645                        if subkind is TEXT:
646                            parts.append(subdata)
647                    href = ''.join([x for x in parts if x is not None])
648                try:
649                    tmpl = self.loader.load(href, relative_to=event[2][0],
650                                            cls=cls or self.__class__)
651                    for event in tmpl.generate(ctxt, **vars):
652                        yield event
653                except TemplateNotFound:
654                    if fallback is None:
655                        raise
656                    for filter_ in self.filters:
657                        fallback = filter_(iter(fallback), ctxt, **vars)
658                    for event in fallback:
659                        yield event
660            else:
661                yield event
662
663
664EXEC = Template.EXEC
665EXPR = Template.EXPR
666INCLUDE = Template.INCLUDE
667SUB = Template.SUB
Note: See TracBrowser for help on using the repository browser.