Edgewall Software

source: trunk/genshi/template/base.py

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

Fix infinite recursion in template inlining (fixes #584).

  • Property svn:eol-style set to native
File size: 23.6 KB
RevLine 
[414]1# -*- coding: utf-8 -*-
2#
[1120]3# Copyright (C) 2006-2010 Edgewall Software
[414]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
[519]14"""Basic templating functionality."""
15
[1031]16from collections import deque
[414]17import os
[725]18import sys
[414]19
[1160]20from genshi.compat import StringIO, BytesIO
[752]21from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
[526]22from genshi.input import ParseError
[414]23
[954]24__all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError',
25           'TemplateRuntimeError', 'TemplateSyntaxError', 'BadDirectiveError']
[517]26__docformat__ = 'restructuredtext en'
[414]27
28
29class TemplateError(Exception):
30    """Base exception class for errors related to template processing."""
31
[726]32    def __init__(self, message, filename=None, lineno=-1, offset=-1):
[530]33        """Create the exception.
[527]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        """
[726]41        if filename is None:
42            filename = '<string>'
[530]43        self.msg = message #: the error message string
[499]44        if filename != '<string>' or lineno >= 0:
45            message = '%s (%s, line %d)' % (self.msg, filename, lineno)
[530]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
[414]50
51
52class TemplateSyntaxError(TemplateError):
53    """Exception raised when an expression in a template causes a Python syntax
[530]54    error, or the template is not well-formed.
55    """
[414]56
[726]57    def __init__(self, message, filename=None, lineno=-1, offset=-1):
[527]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        """
[414]66        if isinstance(message, SyntaxError) and message.lineno is not None:
67            message = str(message).replace(' (line %d)' % message.lineno, '')
[530]68        TemplateError.__init__(self, message, filename, lineno)
[414]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
[726]79    def __init__(self, name, filename=None, lineno=-1):
[527]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        """
[530]87        TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name,
88                                     filename, lineno)
[414]89
90
[530]91class TemplateRuntimeError(TemplateError):
92    """Exception raised when an the evaluation of a Python expression in a
93    template causes an error.
94    """
95
96
[414]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):
[527]123        """Initialize the template context with the given keyword arguments as
124        data.
125        """
[414]126        self.frames = deque([data])
127        self.pop = self.frames.popleft
128        self.push = self.frames.appendleft
129        self._match_templates = []
[664]130        self._choice_stack = []
[414]131
[534]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
[414]144    def __repr__(self):
145        return repr(list(self.frames))
146
[497]147    def __contains__(self, key):
[527]148        """Return whether a variable exists in any of the scopes.
149       
150        :param key: the name of the variable
151        """
[497]152        return self._find(key)[1] is not None
[675]153    has_key = __contains__
[497]154
155    def __delitem__(self, key):
[527]156        """Remove a variable from all scopes.
157       
158        :param key: the name of the variable
159        """
[497]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       
[527]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
[497]171        """
172        value, frame = self._find(key)
173        if frame is None:
174            raise KeyError(key)
175        return value
176
[512]177    def __len__(self):
[527]178        """Return the number of distinctly named variables in the context.
179       
180        :return: the number of variables in the context
181        """
[512]182        return len(self.items())
183
[414]184    def __setitem__(self, key, value):
[527]185        """Set a variable in the current scope.
186       
187        :param key: the name of the variable
188        :param value: the variable value
189        """
[414]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
[527]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
[414]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.
[527]209       
210        :param key: the name of the variable
211        :param default: the default value to return when the variable is not
212                        found
[414]213        """
214        for frame in self.frames:
215            if key in frame:
216                return frame[key]
217        return default
218
[497]219    def keys(self):
[527]220        """Return the name of all variables in the context.
221       
222        :return: a list of variable names
223        """
[497]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):
[527]230        """Return a list of ``(name, value)`` tuples for all variables in the
231        context.
232       
233        :return: a list of variables
234        """
[497]235        return [(key, self.get(key)) for key in self.keys()]
236
[855]237    def update(self, mapping):
238        """Update the context from the mapping provided."""
239        self.frames[0].update(mapping)
240
[414]241    def push(self, data):
[527]242        """Push a new scope on the stack.
243       
244        :param data: the data dictionary to push on the context stack.
245        """
[414]246
247    def pop(self):
248        """Pop the top-most scope from the stack."""
249
[1172]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
[414]261
[1172]262
[1036]263def _apply_directives(stream, directives, ctxt, vars):
[527]264    """Apply the given directives to the stream.
265   
266    :param stream: the stream the directives should be applied to
[816]267    :param directives: the list of directives to apply
[527]268    :param ctxt: the `Context`
[816]269    :param vars: additional variables that should be available when Python
270                 code is executed
[527]271    :return: the stream with the given directives applied
272    """
[414]273    if directives:
[816]274        stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
[414]275    return stream
276
[1036]277
278def _eval_expr(expr, ctxt, vars=None):
[816]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
[414]293
[1036]294
295def _exec_suite(suite, ctxt, vars=None):
[816]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({})
[876]306    suite.execute(ctxt)
[816]307    if vars:
308        top = ctxt.pop()
309        ctxt.pop()
310        ctxt.frames[0].update(top)
311
312
[954]313class DirectiveFactoryMeta(type):
314    """Meta class for directive factories."""
[414]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
[954]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 = []
[1070]332    """A list of ``(name, cls)`` tuples that define the set of directives
[954]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
[1069]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)
[954]358
[1069]359
[954]360class Template(DirectiveFactory):
[414]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
[725]367    EXEC = StreamEventKind('EXEC')
368    """Stream event kind representing a Python code suite to execute."""
369
[519]370    EXPR = StreamEventKind('EXPR')
371    """Stream event kind representing a Python expression."""
[414]372
[575]373    INCLUDE = StreamEventKind('INCLUDE')
374    """Stream event kind representing the inclusion of another template."""
375
[519]376    SUB = StreamEventKind('SUB')
377    """Stream event kind representing a nested stream to which one or more
378    directives should be applied.
379    """
380
[721]381    serializer = None
[752]382    _number_conv = unicode # function used to convert numbers to event data
[721]383
[830]384    def __init__(self, source, filepath=None, filename=None, loader=None,
[722]385                 encoding=None, lookup='strict', allow_exec=True):
[519]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
[830]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
[594]394        :param loader: the `TemplateLoader` to use for loading included
395                       templates
[519]396        :param encoding: the encoding of the `source`
[722]397        :param lookup: the variable lookup mechanism; either "strict" (the
398                       default), "lenient", or a custom lookup class
[654]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
[519]403        """
[830]404        self.filepath = filepath or filename
[414]405        self.filename = filename
[443]406        self.loader = loader
[534]407        self.lookup = lookup
[654]408        self.allow_exec = allow_exec
[831]409        self._init_filters()
[1099]410        self._init_loader()
[954]411        self._prepared = False
[414]412
[1160]413        if not isinstance(source, Stream) and not hasattr(source, 'read'):
414            if isinstance(source, unicode):
415                source = StringIO(source)
416            else:
417                source = BytesIO(source)
[526]418        try:
[954]419            self._stream = self._parse(source, encoding)
[526]420        except ParseError, e:
421            raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
[414]422
[831]423    def __getstate__(self):
424        state = self.__dict__.copy()
425        state['filters'] = []
426        return state
427
428    def __setstate__(self, state):
429        self.__dict__ = state
430        self._init_filters()
431
[414]432    def __repr__(self):
[1083]433        return '<%s "%s">' % (type(self).__name__, self.filename)
[414]434
[831]435    def _init_filters(self):
[1099]436        self.filters = [self._flatten, self._include]
[831]437
[1099]438    def _init_loader(self):
439        if self.loader is None:
440            from genshi.template.loader import TemplateLoader
441            if self.filename:
442                if self.filepath != self.filename:
443                    basedir = os.path.normpath(self.filepath)[:-len(
444                        os.path.normpath(self.filename))
445                    ]
446                else:
447                    basedir = os.path.dirname(self.filename)
448            else:
449                basedir = '.'
450            self.loader = TemplateLoader([os.path.abspath(basedir)])
451
[1031]452    @property
453    def stream(self):
[954]454        if not self._prepared:
[1257]455            self._prepare_self()
[954]456        return self._stream
457
[456]458    def _parse(self, source, encoding):
[414]459        """Parse the template.
460       
461        The parsing stage parses the template and constructs a list of
462        directives that will be executed in the render stage. The input is
463        split up into literal output (text that does not depend on the context
464        data) and directives or expressions.
[519]465       
466        :param source: a file-like object containing the XML source of the
467                       template, or an XML event stream
468        :param encoding: the encoding of the `source`
[414]469        """
470        raise NotImplementedError
471
[1257]472    def _prepare_self(self, inlined=None):
473        if not self._prepared:
474            self._stream = list(self._prepare(self._stream, inlined))
475            self._prepared = True
476
477    def _prepare(self, stream, inlined):
[519]478        """Call the `attach` method of every directive found in the template.
479       
480        :param stream: the event stream of the template
481        """
[657]482        from genshi.template.loader import TemplateNotFound
[1257]483        if inlined is None:
484            inlined = set((self.filepath,))
[657]485
[431]486        for kind, data, pos in stream:
487            if kind is SUB:
[442]488                directives = []
489                substream = data[1]
[1069]490                for _, cls, value, namespaces, pos in sorted(data[0]):
[442]491                    directive, substream = cls.attach(self, substream, value,
492                                                      namespaces, pos)
493                    if directive:
494                        directives.append(directive)
[1257]495                substream = self._prepare(substream, inlined)
[431]496                if directives:
497                    yield kind, (directives, list(substream)), pos
498                else:
499                    for event in substream:
500                        yield event
501            else:
[575]502                if kind is INCLUDE:
[726]503                    href, cls, fallback = data
[1257]504                    tmpl_inlined = False
505                    if (isinstance(href, basestring) and
506                            not getattr(self.loader, 'auto_reload', True)):
[657]507                        # If the path to the included template is static, and
508                        # auto-reloading is disabled on the template loader,
[1257]509                        # the template is inlined into the stream provided it
510                        # is not already in the stack of templates being
511                        # processed.
512                        tmpl = None
[657]513                        try:
514                            tmpl = self.loader.load(href, relative_to=pos[0],
[726]515                                                    cls=cls or self.__class__)
[657]516                        except TemplateNotFound:
517                            if fallback is None:
518                                raise
[1257]519                        if tmpl is not None:
520                            if tmpl.filepath not in inlined:
521                                inlined.add(tmpl.filepath)
522                                tmpl._prepare_self(inlined)
523                                for event in tmpl.stream:
524                                    yield event
525                                inlined.discard(tmpl.filepath)
526                                tmpl_inlined = True
527                        else:
528                            for event in self._prepare(fallback, inlined):
[657]529                                yield event
[1257]530                            tmpl_inlined = True
531                    if tmpl_inlined:
[657]532                        continue
[1257]533                    if fallback:
[657]534                        # Otherwise the include is performed at run time
[1257]535                        data = href, cls, list(
536                            self._prepare(fallback, inlined))
537                    yield kind, data, pos
538                else:
539                    yield kind, data, pos
[657]540
[414]541    def generate(self, *args, **kwargs):
542        """Apply the template to the given context data.
543       
544        Any keyword arguments are made available to the template as context
545        data.
546       
547        Only one positional argument is accepted: if it is provided, it must be
548        an instance of the `Context` class, and keyword arguments are ignored.
549        This calling style is used for internal processing.
550       
[519]551        :return: a markup event stream representing the result of applying
552                 the template to the context data.
[414]553        """
[816]554        vars = {}
[414]555        if args:
556            assert len(args) == 1
557            ctxt = args[0]
558            if ctxt is None:
559                ctxt = Context(**kwargs)
[816]560            else:
561                vars = kwargs
[414]562            assert isinstance(ctxt, Context)
563        else:
564            ctxt = Context(**kwargs)
565
566        stream = self.stream
567        for filter_ in self.filters:
[816]568            stream = filter_(iter(stream), ctxt, **vars)
[721]569        return Stream(stream, self.serializer)
[414]570
[1015]571    def _flatten(self, stream, ctxt, **vars):
[752]572        number_conv = self._number_conv
[1052]573        stack = []
574        push = stack.append
575        pop = stack.pop
576        stream = iter(stream)
[414]577
[1052]578        while 1:
579            for kind, data, pos in stream:
[414]580
[1052]581                if kind is START and data[1]:
582                    # Attributes may still contain expressions in start tags at
583                    # this point, so do some evaluation
584                    tag, attrs = data
585                    new_attrs = []
586                    for name, value in attrs:
587                        if type(value) is list: # this is an interpolated string
588                            values = [event[1]
589                                for event in self._flatten(value, ctxt, **vars)
590                                if event[0] is TEXT and event[1] is not None
591                            ]
592                            if not values:
593                                continue
[1078]594                            value = ''.join(values)
[1052]595                        new_attrs.append((name, value))
596                    yield kind, (tag, Attrs(new_attrs)), pos
[414]597
[1052]598                elif kind is EXPR:
599                    result = _eval_expr(data, ctxt, vars)
600                    if result is not None:
601                        # First check for a string, otherwise the iterable test
602                        # below succeeds, and the string will be chopped up into
603                        # individual characters
604                        if isinstance(result, basestring):
605                            yield TEXT, result, pos
606                        elif isinstance(result, (int, float, long)):
607                            yield TEXT, number_conv(result), pos
608                        elif hasattr(result, '__iter__'):
609                            push(stream)
610                            stream = _ensure(result)
611                            break
612                        else:
613                            yield TEXT, unicode(result), pos
[414]614
[1052]615                elif kind is SUB:
616                    # This event is a list of directives and a list of nested
617                    # events to which those directives should be applied
618                    push(stream)
619                    stream = _apply_directives(data[1], data[0], ctxt, vars)
620                    break
[414]621
[1052]622                elif kind is EXEC:
623                    _exec_suite(data, ctxt, vars)
[1015]624
[1052]625                else:
626                    yield kind, data, pos
627
[414]628            else:
[1052]629                if not stack:
630                    break
631                stream = pop()
[414]632
[816]633    def _include(self, stream, ctxt, **vars):
[575]634        """Internal stream filter that performs inclusion of external
635        template files.
636        """
637        from genshi.template.loader import TemplateNotFound
[414]638
[575]639        for event in stream:
640            if event[0] is INCLUDE:
[726]641                href, cls, fallback = event[1]
[575]642                if not isinstance(href, basestring):
643                    parts = []
[1015]644                    for subkind, subdata, subpos in self._flatten(href, ctxt,
645                                                                  **vars):
[575]646                        if subkind is TEXT:
647                            parts.append(subdata)
[1078]648                    href = ''.join([x for x in parts if x is not None])
[575]649                try:
650                    tmpl = self.loader.load(href, relative_to=event[2][0],
[726]651                                            cls=cls or self.__class__)
[816]652                    for event in tmpl.generate(ctxt, **vars):
[575]653                        yield event
654                except TemplateNotFound:
655                    if fallback is None:
656                        raise
657                    for filter_ in self.filters:
[816]658                        fallback = filter_(iter(fallback), ctxt, **vars)
[575]659                    for event in fallback:
660                        yield event
661            else:
662                yield event
663
664
[725]665EXEC = Template.EXEC
[414]666EXPR = Template.EXPR
[575]667INCLUDE = Template.INCLUDE
[414]668SUB = Template.SUB
Note: See TracBrowser for help on using the repository browser.