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
18
19from genshi.compat import add_metaclass, numeric_types, string_types, text_type, StringIO, BytesIO
20from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
21from genshi.input import ParseError
22
23__all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError',
24 'TemplateRuntimeError', 'TemplateSyntaxError', 'BadDirectiveError']
25__docformat__ = 'restructuredtext en'
26
27
28class TemplateError(Exception):
29 """Base exception class for errors related to template processing."""
30
31 def __init__(self, message, filename=None, lineno=-1, offset=-1):
32 """Create the exception.
33
34 :param message: the error message
35 :param filename: the filename of the template
36 :param lineno: the number of line in the template at which the error
37 occurred
38 :param offset: the column number at which the error occurred
39 """
40 if filename is None:
41 filename = '<string>'
42 self.msg = message #: the error message string
43 if filename != '<string>' or lineno >= 0:
44 message = '%s (%s, line %d)' % (self.msg, filename, lineno)
45 Exception.__init__(self, message)
46 self.filename = filename #: the name of the template file
47 self.lineno = lineno #: the number of the line containing the error
48 self.offset = offset #: the offset on the line
49
50
51class TemplateSyntaxError(TemplateError):
52 """Exception raised when an expression in a template causes a Python syntax
53 error, or the template is not well-formed.
54 """
55
56 def __init__(self, message, filename=None, lineno=-1, offset=-1):
57 """Create the exception
58
59 :param message: the error message
60 :param filename: the filename of the template
61 :param lineno: the number of line in the template at which the error
62 occurred
63 :param offset: the column number at which the error occurred
64 """
65 if isinstance(message, SyntaxError) and message.lineno is not None:
66 message = str(message).replace(' (line %d)' % message.lineno, '')
67 TemplateError.__init__(self, message, filename, lineno)
68
69
70class BadDirectiveError(TemplateSyntaxError):
71 """Exception raised when an unknown directive is encountered when parsing
72 a template.
73
74 An unknown directive is any attribute using the namespace for directives,
75 with a local name that doesn't match any registered directive.
76 """
77
78 def __init__(self, name, filename=None, lineno=-1):
79 """Create the exception
80
81 :param name: the name of the directive
82 :param filename: the filename of the template
83 :param lineno: the number of line in the template at which the error
84 occurred
85 """
86 TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name,
87 filename, lineno)
88
89
90class TemplateRuntimeError(TemplateError):
91 """Exception raised when an the evaluation of a Python expression in a
92 template causes an error.
93 """
94
95
96class Context(object):
97 """Container for template input data.
98
99 A context provides a stack of scopes (represented by dictionaries).
100
101 Template directives such as loops can push a new scope on the stack with
102 data that should only be available inside the loop. When the loop
103 terminates, that scope can get popped off the stack again.
104
105 >>> ctxt = Context(one='foo', other=1)
106 >>> ctxt.get('one')
107 'foo'
108 >>> ctxt.get('other')
109 1
110 >>> ctxt.push(dict(one='frost'))
111 >>> ctxt.get('one')
112 'frost'
113 >>> ctxt.get('other')
114 1
115 >>> ctxt.pop()
116 {'one': 'frost'}
117 >>> ctxt.get('one')
118 'foo'
119 """
120
121 def __init__(self, **data):
122 """Initialize the template context with the given keyword arguments as
123 data.
124 """
125 self.frames = deque([data])
126 self.pop = self.frames.popleft
127 self.push = self.frames.appendleft
128 self._match_templates = []
129 self._choice_stack = []
130
131 # Helper functions for use in expressions
132 def defined(name):
133 """Return whether a variable with the specified name exists in the
134 expression scope."""
135 return name in self
136 def value_of(name, default=None):
137 """If a variable of the specified name is defined, return its value.
138 Otherwise, return the provided default value, or ``None``."""
139 return self.get(name, default)
140 data.setdefault('defined', defined)
141 data.setdefault('value_of', value_of)
142
143 def __repr__(self):
144 return repr(list(self.frames))
145
146 def __contains__(self, key):
147 """Return whether a variable exists in any of the scopes.
148
149 :param key: the name of the variable
150 """
151 return self._find(key)[1] is not None
152 has_key = __contains__
153
154 def __delitem__(self, key):
155 """Remove a variable from all scopes.
156
157 :param key: the name of the variable
158 """
159 for frame in self.frames:
160 if key in frame:
161 del frame[key]
162
163 def __getitem__(self, key):
164 """Get a variables's value, starting at the current scope and going
165 upward.
166
167 :param key: the name of the variable
168 :return: the variable value
169 :raises KeyError: if the requested variable wasn't found in any scope
170 """
171 value, frame = self._find(key)
172 if frame is None:
173 raise KeyError(key)
174 return value
175
176 def __len__(self):
177 """Return the number of distinctly named variables in the context.
178
179 :return: the number of variables in the context
180 """
181 return len(self.items())
182
183 def __setitem__(self, key, value):
184 """Set a variable in the current scope.
185
186 :param key: the name of the variable
187 :param value: the variable value
188 """
189 self.frames[0][key] = value
190
191 def _find(self, key, default=None):
192 """Retrieve a given variable's value and the frame it was found in.
193
194 Intended primarily for internal use by directives.
195
196 :param key: the name of the variable
197 :param default: the default value to return when the variable is not
198 found
199 """
200 for frame in self.frames:
201 if key in frame:
202 return frame[key], frame
203 return default, None
204
205 def get(self, key, default=None):
206 """Get a variable's value, starting at the current scope and going
207 upward.
208
209 :param key: the name of the variable
210 :param default: the default value to return when the variable is not
211 found
212 """
213 for frame in self.frames:
214 if key in frame:
215 return frame[key]
216 return default
217
218 def keys(self):
219 """Return the name of all variables in the context.
220
221 :return: a list of variable names
222 """
223 keys = []
224 for frame in self.frames:
225 keys += [key for key in frame if key not in keys]
226 return keys
227
228 def items(self):
229 """Return a list of ``(name, value)`` tuples for all variables in the
230 context.
231
232 :return: a list of variables
233 """
234 return [(key, self.get(key)) for key in self.keys()]
235
236 def update(self, mapping):
237 """Update the context from the mapping provided."""
238 self.frames[0].update(mapping)
239
240 def push(self, data):
241 """Push a new scope on the stack.
242
243 :param data: the data dictionary to push on the context stack.
244 """
245
246 def pop(self):
247 """Pop the top-most scope from the stack."""
248
249 def copy(self):
250 """Create a copy of this Context object."""
251 # required to make f_locals a dict-like object
252 # See http://genshi.edgewall.org/ticket/249 for
253 # example use case in Twisted tracebacks
254 ctxt = Context()
255 ctxt.frames.pop() # pop empty dummy context
256 ctxt.frames.extend(self.frames)
257 ctxt._match_templates.extend(self._match_templates)
258 ctxt._choice_stack.extend(self._choice_stack)
259 return ctxt
260
261
262def _apply_directives(stream, directives, ctxt, vars):
263 """Apply the given directives to the stream.
264
265 :param stream: the stream the directives should be applied to
266 :param directives: the list of directives to apply
267 :param ctxt: the `Context`
268 :param vars: additional variables that should be available when Python
269 code is executed
270 :return: the stream with the given directives applied
271 """
272 if directives:
273 stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
274 return stream
275
276
277def _eval_expr(expr, ctxt, vars=None):
278 """Evaluate the given `Expression` object.
279
280 :param expr: the expression to evaluate
281 :param ctxt: the `Context`
282 :param vars: additional variables that should be available to the
283 expression
284 :return: the result of the evaluation
285 """
286 if vars:
287 ctxt.push(vars)
288 retval = expr.evaluate(ctxt)
289 if vars:
290 ctxt.pop()
291 return retval
292
293
294def _exec_suite(suite, ctxt, vars=None):
295 """Execute the given `Suite` object.
296
297 :param suite: the code suite to execute
298 :param ctxt: the `Context`
299 :param vars: additional variables that should be available to the
300 code
301 """
302 if vars:
303 ctxt.push(vars)
304 ctxt.push({})
305 suite.execute(ctxt)
306 if vars:
307 top = ctxt.pop()
308 ctxt.pop()
309 ctxt.frames[0].update(top)
310
311
312class DirectiveFactoryMeta(type):
313 """Meta class for directive factories."""
314
315 def __new__(cls, name, bases, d):
316 if 'directives' in d:
317 d['_dir_by_name'] = dict(d['directives'])
318 d['_dir_order'] = [directive[1] for directive in d['directives']]
319
320 return type.__new__(cls, name, bases, d)
321
322
323@add_metaclass(DirectiveFactoryMeta)
324class DirectiveFactory(object):
325 """Base for classes that provide a set of template directives.
326
327 :since: version 0.6
328 """
329
330 directives = []
331 """A list of ``(name, cls)`` tuples that define the set of directives
332 provided by this factory.
333 """
334
335 def get_directive(self, name):
336 """Return the directive class for the given name.
337
338 :param name: the directive name as used in the template
339 :return: the directive class
340 :see: `Directive`
341 """
342 return self._dir_by_name.get(name)
343
344 def get_directive_index(self, dir_cls):
345 """Return a key for the given directive class that should be used to
346 sort it among other directives on the same `SUB` event.
347
348 The default implementation simply returns the index of the directive in
349 the `directives` list.
350
351 :param dir_cls: the directive class
352 :return: the sort key
353 """
354 if dir_cls in self._dir_order:
355 return self._dir_order.index(dir_cls)
356 return len(self._dir_order)
357
358
359class Template(DirectiveFactory):
360 """Abstract template base class.
361
362 This class implements most of the template processing model, but does not
363 specify the syntax of templates.
364 """
365
366 EXEC = StreamEventKind('EXEC')
367 """Stream event kind representing a Python code suite to execute."""
368
369 EXPR = StreamEventKind('EXPR')
370 """Stream event kind representing a Python expression."""
371
372 INCLUDE = StreamEventKind('INCLUDE')
373 """Stream event kind representing the inclusion of another template."""
374
375 SUB = StreamEventKind('SUB')
376 """Stream event kind representing a nested stream to which one or more
377 directives should be applied.
378 """
379
380 serializer = None
381 _number_conv = text_type # function used to convert numbers to event data
382
383 def __init__(self, source, filepath=None, filename=None, loader=None,
384 encoding=None, lookup='strict', allow_exec=True):
385 """Initialize a template from either a string, a file-like object, or
386 an already parsed markup stream.
387
388 :param source: a string, file-like object, or markup stream to read the
389 template from
390 :param filepath: the absolute path to the template file
391 :param filename: the path to the template file relative to the search
392 path
393 :param loader: the `TemplateLoader` to use for loading included
394 templates
395 :param encoding: the encoding of the `source`
396 :param lookup: the variable lookup mechanism; either "strict" (the
397 default), "lenient", or a custom lookup class
398 :param allow_exec: whether Python code blocks in templates should be
399 allowed
400
401 :note: Changed in 0.5: Added the `allow_exec` argument
402 """
403 self.filepath = filepath or filename
404 self.filename = filename
405 self.loader = loader
406 self.lookup = lookup
407 self.allow_exec = allow_exec
408 self._init_filters()
409 self._init_loader()
410 self._prepared = False
411
412 if not isinstance(source, Stream) and not hasattr(source, 'read'):
413 if isinstance(source, text_type):
414 source = StringIO(source)
415 else:
416 source = BytesIO(source)
417 try:
418 self._stream = self._parse(source, encoding)
419 except ParseError as 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(
490 data[0], key=lambda x: x[0]):
491 directive, substream = cls.attach(self, substream, value,
492 namespaces, pos)
493 if directive:
494 directives.append(directive)
495 substream = self._prepare(substream, inlined)
496 if directives:
497 yield kind, (directives, list(substream)), pos
498 else:
499 for event in substream:
500 yield event
501 else:
502 if kind is INCLUDE:
503 href, cls, fallback = data
504 tmpl_inlined = False
505 if (isinstance(href, string_types) and
506 not getattr(self.loader, 'auto_reload', True)):
507 # If the path to the included template is static, and
508 # auto-reloading is disabled on the template loader,
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
513 try:
514 tmpl = self.loader.load(href, relative_to=pos[0],
515 cls=cls or self.__class__)
516 except TemplateNotFound:
517 if fallback is None:
518 raise
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):
529 yield event
530 tmpl_inlined = True
531 if tmpl_inlined:
532 continue
533 if fallback:
534 # Otherwise the include is performed at run time
535 data = href, cls, list(
536 self._prepare(fallback, inlined))
537 yield kind, data, pos
538 else:
539 yield kind, data, pos
540
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
551 :return: a markup event stream representing the result of applying
552 the template to the context data.
553 """
554 vars = {}
555 if args:
556 assert len(args) == 1
557 ctxt = args[0]
558 if ctxt is None:
559 ctxt = Context(**kwargs)
560 else:
561 vars = kwargs
562 assert isinstance(ctxt, Context)
563 else:
564 ctxt = Context(**kwargs)
565
566 stream = self.stream
567 for filter_ in self.filters:
568 stream = filter_(iter(stream), ctxt, **vars)
569 return Stream(stream, self.serializer)
570
571 def _flatten(self, stream, ctxt, **vars):
572 number_conv = self._number_conv
573 stack = []
574 push = stack.append
575 pop = stack.pop
576 stream = iter(stream)
577
578 while 1:
579 for kind, data, pos in stream:
580
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
594 value = ''.join(values)
595 new_attrs.append((name, value))
596 yield kind, (tag, Attrs(new_attrs)), pos
597
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, string_types):
605 yield TEXT, result, pos
606 elif isinstance(result, numeric_types):
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, text_type(result), pos
614
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
621
622 elif kind is EXEC:
623 _exec_suite(data, ctxt, vars)
624
625 else:
626 yield kind, data, pos
627
628 else:
629 if not stack:
630 break
631 stream = pop()
632
633 def _include(self, stream, ctxt, **vars):
634 """Internal stream filter that performs inclusion of external
635 template files.
636 """
637 from genshi.template.loader import TemplateNotFound
638
639 for event in stream:
640 if event[0] is INCLUDE:
641 href, cls, fallback = event[1]
642 if not isinstance(href, string_types):
643 parts = []
644 for subkind, subdata, subpos in self._flatten(href, ctxt,
645 **vars):
646 if subkind is TEXT:
647 parts.append(subdata)
648 href = ''.join([x for x in parts if x is not None])
649 try:
650 tmpl = self.loader.load(href, relative_to=event[2][0],
651 cls=cls or self.__class__)
652 for event in tmpl.generate(ctxt, **vars):
653 yield event
654 except TemplateNotFound:
655 if fallback is None:
656 raise
657 for filter_ in self.filters:
658 fallback = filter_(iter(fallback), ctxt, **vars)
659 for event in fallback:
660 yield event
661 else:
662 yield event
663
664
665EXEC = Template.EXEC
666EXPR = Template.EXPR
667INCLUDE = Template.INCLUDE
668SUB = Template.SUB