1
2
3 """
4 Small, simple and powerful template-engine for python.
5
6 This is a template-engine for python, which is very simple, easy to use,
7 small, fast, powerful and pythonic.
8
9 See documentation for a list of features, template-syntax etc.
10
11 :Version: 0.1.4 (2008-12-21)
12 :Status: beta
13
14 :Usage:
15 see class 'Template' and examples below.
16
17 :Example:
18
19 quickstart::
20 >>> t = Template("hello @!name!@")
21 >>> print t(name="marvin")
22 hello marvin
23
24 generic usage::
25 >>> t = Template("output is in Unicode äöü€")
26 >>> t #doctest: +ELLIPSIS
27 <...Template instance at 0x...>
28 >>> t()
29 u'output is in Unicode \\xe4\\xf6\\xfc\\u20ac'
30 >>> unicode(t)
31 u'output is in Unicode \\xe4\\xf6\\xfc\\u20ac'
32
33 with data::
34 >>> t = Template("hello @!name!@", data={"name":"world"})
35 >>> t()
36 u'hello world'
37 >>> t(name="worlds")
38 u'hello worlds'
39
40 # >>> t(note="data must be Unicode or ASCII", name=u"ä")
41 # u'hello \\xe4\\xf6\\xe4\\u20ac'
42
43 python-expressions::
44 >>> Template('formatted: @! "%10.7f" % value !@')(value=3.141592653)
45 u'formatted: 3.1415927'
46 >>> Template("hello --@!name.upper().center(20)!@--")(name="world")
47 u'hello -- WORLD --'
48 >>> Template("calculate @!var*5+7!@")(var=7)
49 u'calculate 42'
50
51 escaping::
52 >>> t = Template("hello escaped @!name!@")
53 >>> t(name='''<>&'" ''')
54 u'hello escaped <>&'" '
55 >>> t = Template("hello unescaped $!name!$")
56 >>> t(name='''<>&'" ''')
57 u'hello unescaped <>&\\'" '
58
59 result-encoding::
60 # encode the unicode-object to your encoding with encode()
61 >>> result = Template("hello äöü€")()
62 >>> result
63 u'hello \\xe4\\xf6\\xfc\\u20ac'
64 >>> result.encode("utf-8")
65 'hello \\xc3\\xa4\\xc3\\xb6\\xc3\\xbc\\xe2\\x82\\xac'
66 >>> result.encode("ascii")
67 Traceback (most recent call last):
68 ...
69 UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-9: ordinal not in range(128)
70 >>> result.encode("ascii", 'xmlcharrefreplace')
71 'hello äöü€'
72
73 default-values::
74 # non-existing variables raise an error
75 >>> Template('hi @!optional!@')()
76 Traceback (most recent call last):
77 ...
78 TemplateRenderError: Cannot eval expression 'optional' (NameError: name 'optional' is not defined)
79
80 >>> t = Template('hi @!default("optional","anyone")!@')
81 >>> t()
82 u'hi anyone'
83 >>> t(optional=None)
84 u'hi anyone'
85 >>> t(optional="there")
86 u'hi there'
87
88 # also in blocks
89 >>> t = Template('<!--(if default("optional",False))-->yes<!--(else)-->no<!--(end)-->')
90 >>> t()
91 u'no'
92 >>> t(optional=23)
93 u'yes'
94
95 # the 1st parameter can be any eval-expression
96 >>> t = Template('@!default("5*var1+var2","missing variable")!@')
97 >>> t(var1=10)
98 u'missing variable'
99 >>> t(var1=10, var2=2)
100 u'52'
101
102 # but make sure to put the expression in quotation marks, otherwise:
103 >>> Template('@!default(optional,"fallback")!@')()
104 Traceback (most recent call last):
105 ...
106 TemplateRenderError: Cannot eval expression 'default(optional,"fallback")' (NameError: name 'optional' is not defined)
107
108 exists:
109 >>> t = Template('<!--(if exists("foo"))-->YES<!--(else)-->NO<!--(end)-->')
110 >>> t()
111 u'NO'
112 >>> t(foo=1)
113 u'YES'
114 >>> t(foo=None) # note this difference to 'default()'
115 u'YES'
116
117 :Note:
118
119 :Author: Roland Koebler (rk at simple-is-better dot org)
120 :Copyright: 2007-2008 by Roland Koebler
121 :License: MIT/X11-like, see __license__
122
123 :TODO:
124 - enhance/extend escape()
125 - speedup:
126 - ? load/save parsed (marshal+check python-version)
127 - ? psyco
128 - ? escape -> C ?
129 - ? compiler, (eval("\w+") -> data[..])
130
131 - extensions:
132 - ? set/define variables / capture output ?
133 (i.e. <!--(set var)-->[1,2,3]<!--(end)-->)
134 - ? filter ? (function over block)
135 """
136
137 __version__ = "0.1.4"
138 __author__ = "Roland Koebler <rk at simple-is-better dot org>"
139 __license__ = """Copyright (c) 2007-2008 by Roland Koebler
140
141 Permission is hereby granted, free of charge, to any person obtaining a copy
142 of this software and associated documentation files (the "Software"), to deal
143 in the Software without restriction, including without limitation the rights
144 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
145 copies of the Software, and to permit persons to whom the Software is
146 furnished to do so, subject to the following conditions:
147
148 The above copyright notice and this permission notice shall be included in
149 all copies or substantial portions of the Software.
150
151 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
152 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
153 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
154 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
155 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
156 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
157 IN THE SOFTWARE."""
158
159
160
161 import __builtin__, os
162 import re
163
164
165
166
167
168
169
170 -def srow(string, i):
171 """Get row/lineno of string[i].
172
173 :Returns: row, starting at 1
174 :Note: This works for text-strings with '\\n' or '\\r\\n'.
175 """
176 return string.count('\n', 0, max(0, i)) + 1
177
178 -def scol(string, i):
179 """Get column of string[i].
180
181 :Returns: column, starting at 1 (but may be <1 if i<0)
182 :Note: This works for text-strings with '\\n' or '\\r\\n'.
183 """
184 return i - string.rfind('\n', 0, max(0, i))
185
187 """Get string-index of the character at row/lineno,col.
188
189 :Parameters: row,col, starting at 1.
190 :Returns: i, starting at 0. (but may be <1 if row/col<0)
191 :Note: This works for text-strings with '\\n' or '\\r\\n'.
192 """
193 n = 0
194 for _ in range(row-1):
195 n = string.find('\n', n) + 1
196 return n+col-1
197
198
199
201 """Convert all keys of d to strings.
202 """
203 new_d = {}
204 for k, v in d.iteritems():
205 new_d[str(k)] = v
206 return new_d
207
208
209
210
211 (HTML, LATEX) = range(0, 2)
212 ESCAPE_SUPPORTED = {"NONE":None, "HTML":HTML, "LATEX":LATEX}
213
215 """Replace special characters by their escape sequence.
216
217 :Parameters:
218 - `s`: string or unicode-string to escape
219 - `format`:
220 - None: nothing is replaced
221 - HTML: replace &<>'" by &...;
222 - LATEX: replace #$%&_{}"\ (TODO! - this is very incomplete!)
223 :Returns:
224 the escaped string in unicode
225 :TODO: complete LaTeX-escaping
226 """
227
228
229
230
231
232
233
234 if format is None:
235 pass
236 elif format == HTML:
237 s = s.replace(u"&", u"&")
238 s = s.replace(u"<", u"<")
239 s = s.replace(u">", u">")
240 s = s.replace(u'"', u""")
241 s = s.replace(u"'", u"'")
242 elif format == LATEX:
243
244
245
246 s = s.replace("\\", u"\\backslash{}")
247 s = s.replace("#", u"\\#")
248 s = s.replace("$", u"\\$")
249 s = s.replace("%", u"\\%")
250 s = s.replace("&", u"\\&")
251 s = s.replace("_", u"\\_")
252 s = s.replace("{", u"\\{")
253 s = s.replace("}", u"\\}")
254 s = s.replace('"', u"{''}")
255 else:
256 raise ValueError('invalid format. (only None, HTML and LATEX are valid.)')
257 return unicode(s)
258
259
260
261 -def dummy(*args, **kwargs):
262 """Dummy function, doing nothing.
263 """
264 pass
265
267 """Dummy-function-creater.
268
269 :Returns: dummy function, raising exception(value)
270 """
271 def mydummy(*args, **kwargs):
272 raise exception(value)
273 return mydummy
274
275
276
277
278
279
280
282 """Base class for template-exceptions."""
283 pass
284
286 """Template parsing failed."""
288 """:Parameters:
289 - err: error-message or exception to wrap
290 - errpos: (filename,row,col) where the error occured.
291 """
292 self.err = err
293 self.filename, self.row, self.col = errpos
294 TemplateException.__init__(self)
296 if not self.filename:
297 return "line %d, col %d: %s" % (self.row, self.col, str(self.err))
298 else:
299 return "file %s, line %d, col %d: %s" % (self.filename, self.row, self.col, str(self.err))
300
302 """Template syntax-error."""
303 pass
304
306 """Template 'include' failed."""
307 pass
308
310 """Template rendering failed."""
311 pass
312
313
314
315
317 """Basic template-class.
318
319 Used both for the template itself and for 'macro's ("subtemplates") in
320 the template.
321 """
322
323 - def __init__(self, parsetree, data, renderfunc):
324 """Create the Template/Subtemplate/Macro.
325
326 :Parameter:
327 - parsetree: parse-tree of the template/subtemplate/macro
328 - data: data to fill into the template by default (dictionary).
329 This data may later be overridden when rendering the template.
330 - renderfunc: render-function
331 """
332
333 self.parsetree = parsetree
334 if isinstance(data, dict):
335 self.data = data
336 elif data is None:
337 self.data = {}
338 else:
339 raise TypeError('"data" must be a dict (or None).')
340 self.current_data = data
341 self._render = renderfunc
342
344 """Fill out/render the template.
345
346 :Parameters:
347 - override: objects to add to the data-namespace, overriding
348 the "default"-data.
349 :Returns: the filled template (in unicode)
350 :Note: this is also called when invoking macros
351 (i.e. "$!mymacro()!$").
352 """
353 self.current_data = self.data.copy()
354 self.current_data.update(override)
355 u = u"".join(self._render(self.parsetree, self.current_data))
356 self.current_data = self.data
357 return _dontescape(u)
358
360 """Alias for __call__()."""
361 return self.__call__()
363 """Only here for completeness. Use __unicode__ instead!"""
364 return self.__call__()
365
366
368 """Template-User-Interface.
369
370 :Usage:
371 ::
372 t = Template(...) (<- see __init__)
373 output = t(...) (<- see TemplateBase.__call__)
374
375 :Example:
376 see module-docstring
377 """
378
379 - def __init__(self, string=None,filename=None,parsetree=None, data=None, encoding='utf-8', escape=HTML ):
380 """Load (+parse) a template.
381
382 :Parameter:
383 - string,filename,parsetree: a template-string,
384 filename of a template to load,
385 or a template-parsetree.
386 (only one of these 3 is allowed)
387 - data: data to fill into the template by default (dictionary).
388 This data may later be overridden when rendering the template.
389 - encoding: encoding of the template-files (only used for "filename")
390 - escape: default-escaping for the template, may be overwritten by the template!
391 """
392 if [string, filename, parsetree].count(None) != 2:
393 raise ValueError('only 1 of string,filename,parsetree is allowed.')
394
395 u = None
396
397 if filename is not None:
398 incl_load = FileLoader(os.path.dirname(filename), encoding).load
399 u = incl_load(os.path.basename(filename))
400 if string is not None:
401 incl_load = dummy_raise(NotImplementedError, "'include' not supported for template-strings")
402 u = StringLoader(encoding).load(string)
403
404
405 templateeval = TemplateEval()
406
407
408 if u is not None:
409 p = Parser(loadfunc=incl_load, testexpr=templateeval.compile, escape=escape)
410 parsetree = p.parse(u)
411 del p
412
413
414 renderer = Renderer(templateeval.eval)
415
416
417 TemplateBase.__init__(self, parsetree, data, renderer.render)
418
419
420
421
422
424 """Load a template from a string.
425
426 Note that 'include' is not possible.
427 """
429 self.encoding = encoding
430
431 - def load(self, string):
432 """Return template-string as unicode."""
433 if isinstance(string, unicode):
434 u = string
435 else:
436 u = unicode(string, self.encoding)
437 return u
438
440 """Load template from a file.
441
442 When loading a template from a file, it's possible to including other
443 templates (by using 'include' in the template). But for simplicity
444 and security, all included templates have to be in the same directory!
445 (see 'allowed_path')
446 """
447
448 - def __init__(self, allowed_path, encoding='utf-8'):
449 """Init the loader.
450
451 :Parameters:
452 - allowed_path: path of the template-files
453 - encoding: encoding of the template-files
454 """
455 if allowed_path and not os.path.isdir(allowed_path):
456
457 raise ValueError("'allowed_path' has to be a directory.")
458 self.path = allowed_path
459 self.encoding = encoding
460
461 - def load(self, filename):
462 """Load a template from a file.
463
464 Check if filename is allowed and return its contens in unicode.
465 :Parameters:
466 - filename: filename of the template without path
467 :Returns:
468 the contents of the template-file in unicode
469 """
470 if filename != os.path.basename(filename):
471 raise ValueError("No pathname allowed (%s)." %(filename))
472 filename = os.path.join(self.path, filename)
473
474 f = open(filename, 'rb')
475 string = f.read()
476 f.close()
477
478 u = unicode(string, self.encoding)
479
480 return u
481
482
483
484
486 """Parse a template into a parse-tree.
487
488 :TODO: describe the parse-tree
489 """
490
491 _comment_start = "#!"
492 _comment_end = "!#"
493 _sub_start = "$!"
494 _sub_end = "!$"
495 _subesc_start = "@!"
496 _subesc_end = "!@"
497 _block_start = "<!--("
498 _block_end = ")-->"
499
500
501
502 _strComment = r"""%s(?P<content>.*?)(?P<end>%s|\n|$)""" \
503 % (re.escape(_comment_start), re.escape(_comment_end))
504 _reComment = re.compile(_strComment, re.M)
505
506
507
508 _strSubstitution = r"""
509 (
510 %s\s*(?P<sub>.*?)\s*(?P<end>%s|$) #substitution
511 |
512 %s\s*(?P<escsub>.*?)\s*(?P<escend>%s|$) #escaped substitution
513 )
514 """ % (re.escape(_sub_start), re.escape(_sub_end),
515 re.escape(_subesc_start), re.escape(_subesc_end))
516 _reSubstitution = re.compile(_strSubstitution, re.X|re.M)
517
518
519
520
521
522
523
524
525
526
527
528
529 _strBlock = r"""
530 ^(?P<mEnd>[ \t]*)%send%s(?P<meIgnored>.*)\r?\n? # multi-line end (^ <!--(end)-->IGNORED_TEXT\n)
531 |
532 (?P<sEnd>)%send%s # single-line end (<!--(end)-->)
533 |
534 (?P<sSpace>\s*)%s # single-line tag (no nesting)
535 (?P<sKeyw>\w+)[ \t]*(?P<sParam>.*?)
536 %s
537 (?P<sContent>.*?)
538 (?=(?:%s.*?%s.*?)??%send%s)
539 |
540 # multi-line tag, nested by whitespace indentation
541 ^(?P<indent>[ \t]*)%s # save indentation of start tag
542 (?P<mKeyw>\w+)\s*(?P<mParam>.*?)
543 %s(?P<mIgnored>.*)\r?\n
544 (?P<mContent>(?:.*\n)*?)
545 (?=(?P=indent)%s(?:.|\s)*?%s) # match indentation
546 """ % (re.escape(_block_start), re.escape(_block_end),
547 re.escape(_block_start), re.escape(_block_end),
548 re.escape(_block_start), re.escape(_block_end),
549 re.escape(_block_start), re.escape(_block_end),
550 re.escape(_block_start), re.escape(_block_end),
551 re.escape(_block_start), re.escape(_block_end),
552 re.escape(_block_start), re.escape(_block_end))
553 _reBlock = re.compile(_strBlock, re.X|re.M)
554
555
556 _strForParam = r"""^(?P<names>\w+(?:\s*,\s*\w+)*)\s+in\s+(?P<iter>.+)$"""
557 _reForParam = re.compile(_strForParam)
558
559
560 - def __init__(self, loadfunc=None, testexpr=None, escape=HTML):
561 """Init the parser.
562
563 :Parameters:
564 - loadfunc: function to load included templates
565 (i.e. FileLoader(...).load)
566 - testexpr: function to test if a template-expressions is valid
567 (i.e. TempateEval().compile)
568 - escape: default-escaping (may be modified by the template <- TODO)
569 """
570 if loadfunc is None:
571 self._load = dummy_raise(NotImplementedError, "'include' not supported")
572 else:
573 self._load = loadfunc
574 if testexpr is None:
575 self._testexprfunc = dummy
576 else:
577 try:
578 testexpr("i==1")
579 except Exception,err:
580 raise ValueError("invalid 'testexpr' (%s)" %(err))
581 self._testexprfunc = testexpr
582 if escape not in ESCAPE_SUPPORTED.values():
583 raise ValueError("unsupported 'escape' (%s)" %(escape))
584 self.escape = escape
585 self._block_cache = {}
586 self._includestack = []
587
588 - def parse(self, template):
589 """Parse a template.
590
591 :Parameters:
592 - template: template-unicode-string
593 :Returns: the resulting parse-tree
594 :Raises:
595 - TemplateSyntaxError: for template-syntax-errors
596 - TemplateIncludeError: if template-inclusion failed
597 - TemplateException
598 """
599 self._includestack = [(None, template)]
600 return self._parse(template)
601
603 """Convert fpos to (filename,row,column) for error-messages."""
604 filename, string = self._includestack[-1]
605 return filename, srow(string, fpos), scol(string,fpos)
606
608 """Test a template-expression to detect errors."""
609 try:
610 self._testexprfunc(expr)
611 except SyntaxError,err:
612 raise TemplateSyntaxError(err, self._errpos(fpos))
613
614 - def _parse(self, template, fpos=0):
615 """Recursive part of parse()."""
616 def sub_append(parsetree, text, fpos=0):
617 curr = 0
618 for match in self._reSubstitution.finditer(text):
619 start = match.start()
620 if start > curr:
621 parsetree.append(("str", self._reComment.sub('', text[curr:start])))
622
623 if match.group("sub") is not None:
624 if not match.group("end"):
625 raise TemplateSyntaxError("missing closing tag '%s' for '%s'"
626 % (self._sub_end, match.group()), self._errpos(fpos+start))
627 if len(match.group("sub")) > 0:
628 self._testexpr(match.group("sub"), fpos+start)
629 parsetree.append(("sub", match.group("sub")))
630 else:
631 assert(match.group("escsub") is not None)
632 if not match.group("escend"):
633 raise TemplateSyntaxError("missing closing tag '%s' for '%s'"
634 % (self._subesc_end, match.group()), self._errpos(fpos+start))
635 if len(match.group("escsub")) > 0:
636 self._testexpr(match.group("escsub"), fpos+start)
637 parsetree.append(("esc", self.escape, match.group("escsub")))
638
639 curr = match.end()
640
641 if len(text) > curr:
642 parsetree.append(("str", self._reComment.sub('', text[curr:])))
643
644
645
646
647 template = self._reComment.sub(lambda match: "#!"+" "*len(match.group(1))+match.group(2), template)
648
649
650 parsetree = []
651 curr = 0
652 block_type = None
653 block_indent = None
654
655
656 if template not in self._block_cache:
657 self._block_cache[template] = list(self._reBlock.finditer(template))
658 for match in self._block_cache[template]:
659 start = match.start()
660 if start > curr:
661 sub_append(parsetree, template[curr:start], fpos)
662
663
664 keyword = None
665 block = match.groupdict()
666 pos__ = fpos + start
667 if block["sKeyw"] is not None:
668 block_indent = None
669 keyword = block["sKeyw"]
670 param = block["sParam"]
671 content = block["sContent"]
672 if block["sSpace"]:
673 if len(parsetree) > 0 and parsetree[-1][0] == "str":
674 parsetree[-1] = ("str", parsetree[-1][1] + block["sSpace"])
675 else:
676 parsetree.append(("str", block["sSpace"]))
677 pos_p = fpos + match.start("sParam")
678 pos_c = fpos + match.start("sContent")
679 elif block["mKeyw"] is not None:
680 block_indent = len(block["indent"])
681 keyword = block["mKeyw"]
682 param = block["mParam"]
683 content = block["mContent"]
684 pos_p = fpos + match.start("mParam")
685 pos_c = fpos + match.start("mContent")
686 if block["mIgnored"].strip():
687 raise TemplateSyntaxError("no code allowed after block-tag", self._errpos(fpos+match.start("mIgnored")))
688 elif block["mEnd"] is not None:
689 if block_type is None:
690 raise TemplateSyntaxError("no block to end here/invalid indent", self._errpos(pos__) )
691 if block_indent != len(block["mEnd"]):
692 raise TemplateSyntaxError("invalid indent for end-tag", self._errpos(pos__) )
693 if block["meIgnored"].strip():
694 raise TemplateSyntaxError("no code allowed after end-tag", self._errpos(fpos+match.start("meIgnored")))
695 block_type = None
696 elif block["sEnd"] is not None:
697 if block_type is None:
698 raise TemplateSyntaxError("no block to end here/invalid indent", self._errpos(pos__))
699 if block_indent is not None:
700 raise TemplateSyntaxError("invalid indent for end-tag", self._errpos(pos__))
701 block_type = None
702 else:
703 raise TemplateException("FATAL: block regexp error. please contact the author. (%s)" % match.group())
704
705
706 if keyword:
707 keyword = keyword.lower()
708 if 'for' == keyword:
709 if block_type is not None:
710 raise TemplateSyntaxError("missing block-end-tag before new block at '%s'" %(match.group()), self._errpos(pos__))
711 block_type = 'for'
712 cond = self._reForParam.match(param)
713 if cond is None:
714 raise TemplateSyntaxError("invalid 'for ...' at '%s'" %(param), self._errpos(pos_p))
715 names = tuple(n.strip() for n in cond.group("names").split(","))
716 self._testexpr(cond.group("iter"), pos_p+cond.start("iter"))
717 parsetree.append(("for", names, cond.group("iter"), self._parse(content, pos_c)))
718 elif 'if' == keyword:
719 if block_type is not None:
720 raise TemplateSyntaxError("missing block-end-tag before new block at '%s'" %(match.group()), self._errpos(pos__))
721 if not param:
722 raise TemplateSyntaxError("missing condition for 'if' at '%s'" %(match.group()), self._errpos(pos__))
723 block_type = 'if'
724 self._testexpr(param, pos_p)
725 parsetree.append(("if", param, self._parse(content, pos_c)))
726 elif 'elif' == keyword:
727 if block_type != 'if':
728 raise TemplateSyntaxError("'elif' may only appear after 'if' at '%s'" %(match.group()), self._errpos(pos__))
729 if not param:
730 raise TemplateSyntaxError("missing condition for 'elif' at '%s'" %(match.group()), self._errpos(pos__))
731 self._testexpr(param, pos_p)
732 parsetree.append(("elif", param, self._parse(content, pos_c)))
733 elif 'else' == keyword:
734 if block_type not in ['if', 'for']:
735 raise TemplateSyntaxError("'else' may only appear after 'if' of 'for' at '%s'" %(match.group()), self._errpos(pos__))
736 if param:
737 raise TemplateSyntaxError("'else' may not have parameters at '%s'" %(match.group()), self._errpos(pos__))
738 parsetree.append(("else", self._parse(content, pos_c)))
739 elif 'macro' == keyword:
740 if block_type is not None:
741 raise TemplateSyntaxError("missing block-end-tag before new block '%s'" %(match.group()), self._errpos(pos__))
742 block_type = 'macro'
743
744 if not param:
745 raise TemplateSyntaxError("missing name for 'macro' at '%s'" %(match.group()), self._errpos(pos__))
746
747 if len(content) > 0 and content[-1] == '\n':
748 content = content[:-1]
749 if len(content) > 0 and content[-1] == '\r':
750 content = content[:-1]
751 parsetree.append(("macro", param, self._parse(content, pos_c)))
752
753
754 elif 'raw' == keyword:
755 if block_type is not None:
756 raise TemplateSyntaxError("missing block-end-tag before new block '%s'" %(match.group()), self._errpos(pos__))
757 if param:
758 raise TemplateSyntaxError("'raw' may not have parameters at '%s'" %(match.group()), self._errpos(pos__))
759 block_type = 'raw'
760 parsetree.append(("str", content))
761 elif 'include' == keyword:
762 if block_type is not None:
763 raise TemplateSyntaxError("missing block-end-tag before new block '%s'" %(match.group()), self._errpos(pos__))
764 if param:
765 raise TemplateSyntaxError("'include' may not have parameters at '%s'" %(match.group()), self._errpos(pos__))
766 block_type = 'include'
767 try:
768 u = self._load(content.strip())
769 except Exception,err:
770 raise TemplateIncludeError(err, self._errpos(pos__))
771 self._includestack.append((content.strip(), u))
772 p = self._parse(u)
773 self._includestack.pop()
774 parsetree.extend(p)
775 elif 'set_escape' == keyword:
776 if block_type is not None:
777 raise TemplateSyntaxError("missing block-end-tag before new block '%s'" %(match.group()), self._errpos(pos__))
778 if param:
779 raise TemplateSyntaxError("'set_escape' may not have parameters at '%s'" %(match.group()), self._errpos(pos__))
780 block_type = 'set_escape'
781 esc = content.strip().upper()
782 if esc in ESCAPE_SUPPORTED:
783 self.escape = ESCAPE_SUPPORTED[esc]
784 else:
785 raise TemplateSyntaxError("unsupported escape '%s'" %(esc), self._errpos(pos__))
786
787 else:
788 raise TemplateSyntaxError("invalid keyword '%s'" %(keyword), self._errpos(pos__))
789 curr = match.end()
790
791 if block_type is not None:
792 raise TemplateSyntaxError("missing end-tag", self._errpos(pos__))
793
794 if len(template) > curr:
795 sub_append(parsetree, template[curr:], fpos)
796
797 return parsetree
798
799
800
801
802
803 assert len(eval("dir()", {'__builtins__':{'dir':dir}})) == 1, "FATAL: eval does not work as expected (%s)."
804 assert compile("0 .__class__", "<string>", "eval").co_names == ('__class__',), "FATAL: compile does not work as expected."
805
807 """A pseudo-eval-sandbox.
808
809 - Allow only some of the builtin python-functions
810 (see eval_allowed_globals), which are considered "save".
811 - Forbid names beginning with "_".
812 This is to prevent things like '0 .__class__', with which you could
813 easily break out of a "sandbox".
814
815 Note that this is no real sandbox!
816 Don't use it for untrusted code!!
817 """
818
819 eval_allowed_globals = {
820 "True" : __builtin__.True,
821 "False" : __builtin__.False,
822 "None" : __builtin__.None,
823
824 "abs" : __builtin__.abs,
825 "chr" : __builtin__.chr,
826 "cmp" : __builtin__.cmp,
827 "divmod" : __builtin__.divmod,
828 "hash" : __builtin__.hash,
829 "hex" : __builtin__.hex,
830 "len" : __builtin__.len,
831 "max" : __builtin__.max,
832 "min" : __builtin__.min,
833 "oct" : __builtin__.oct,
834 "ord" : __builtin__.ord,
835 "pow" : __builtin__.pow,
836 "range" : __builtin__.range,
837 "round" : __builtin__.round,
838 "sorted" : __builtin__.sorted,
839 "sum" : __builtin__.sum,
840 "unichr" : __builtin__.unichr,
841 "zip" : __builtin__.zip,
842
843 "bool" : __builtin__.bool,
844 "complex" : __builtin__.complex,
845 "dict" : __builtin__.dict,
846 "enumerate" : __builtin__.enumerate,
847 "float" : __builtin__.float,
848 "int" : __builtin__.int,
849 "list" : __builtin__.list,
850 "long" : __builtin__.long,
851 "reversed" : __builtin__.reversed,
852 "str" : __builtin__.str,
853 "tuple" : __builtin__.tuple,
854 "unicode" : __builtin__.unicode,
855 "xrange" : __builtin__.xrange,
856
857
858 }
859
861 self._compile_cache = {}
862 self.locals = None
863
865 """Add an object to the "allowed eval-globals".
866
867 Mainly useful to add user-defined functions to the pseudo-sandbox.
868 """
869 self.eval_allowed_globals[name] = obj
870
872 """Compile a python-eval-expression.
873
874 - Use a compile-cache
875 - Raise an NameError if expr contains a name beginning with '_'.
876
877 :Returns: the compiled expr
878 :Raises: SyntaxError for compile-errors,
879 NameError if expr contains a name beginning with '_'
880 """
881 if expr not in self._compile_cache:
882 c = compile(expr, "", "eval")
883 for i in c.co_names:
884 if i[0] == '_':
885 raise NameError("name '%s' is not allowed" %(i))
886 self._compile_cache[expr] = c
887 return self._compile_cache[expr]
888
889 - def eval(self, expr, locals):
890 """Eval a python-eval-expression.
891
892 Uses the compile-cache.
893 """
894 self.locals = locals
895 if expr not in self._compile_cache:
896 self._compile_cache[expr] = self.compile(expr)
897 compiled = self._compile_cache[expr]
898
899 return eval(compiled, {"__builtins__":self.eval_allowed_globals}, locals)
900
901
902
904 """PseudoSandbox with some additional functions, which are useful
905 in the template.
906
907 Additional functions:
908 - "default" calls _default()
909 - "exists": calls _exists()
910 """
911
916
917 - def _default(self, expr, default=None):
918 """Return the eval-result of expr or a "fallback"-value.
919
920 Use this in the template to use default-values for optional data.
921 The default-value is used if 'expr' does not exist/is invalid/results in None.
922
923 :Note: the variable-name has to be quoted! (like in eval)
924
925 :Example:
926 ::
927 @! default("optional_var","fallback_value") !@
928 <!--(if default("optional",False))-->"YES"<!--(else)-->"NO"<!--(end)-->
929 <!--(for i in default("optional_list",[]))-->
930 """
931 try:
932 r = self.eval(expr, self.locals)
933 if r is None:
934 return default
935 return r
936
937 except Exception:
938 return default
939
941 """Test if a variable exists.
942
943 This tests if 'varname' exists in the current locals-namespace.
944
945 :Note: the variable-name has to be quoted! (like in eval)
946
947 This only works for single variable names. If you want to test
948 complicated expressions, use i.e. _default.
949 (i.e. _default("expr",False))
950 """
951 return (varname in self.locals)
952
953
954
955
957 """Unicode-string which should not be escaped.
958
959 If ``isinstance(object,_dontescape)``, then don't escape it in @!...!@.
960 It's useful for not double-escaping macros, and it's automatically
961 used for macros/subtemplates.
962
963 :Note: This only works if the object is used on its own in @!...!@.
964 It i.e. does not work in @! object*2 !@ or @! object + "hi" !@.
965 """
966 __slots__ = []
967
968
970 """Render a template-parse-tree."""
971
973 """Init the renderer.
974
975 :Parameter:
976 - evalfunc: function for template-expression-evaluation (i.e. TemplateEval().eval)
977 """
978
979 self.evalfunc = evalfunc
980
981 - def _eval(self, expr, data):
982 """evalfunc with error-messages"""
983 try:
984 return self.evalfunc(expr, data)
985
986 except (TypeError,NameError,IndexError,KeyError,AttributeError, SyntaxError), err:
987 raise TemplateRenderError("Cannot eval expression '%s' (%s: %s)" %(expr, err.__class__.__name__, err))
988
989 - def render(self, parsetree, data):
990 """Render a parse-tree of a template.
991
992 :Parameter:
993 - parsetree: the parse-tree
994 - data: the data to fill into the template (dictionary)
995 :Returns: the rendered output-unicode-string
996 :Raises: TemplateRenderError
997 """
998 _eval = self._eval
999 output = []
1000
1001 do_else = False
1002 if parsetree is None:
1003 return ""
1004 for elem in parsetree:
1005 if "str" == elem[0]:
1006 output.append(elem[1])
1007 elif "sub" == elem[0]:
1008 output.append(unicode(_eval(elem[1], data)))
1009 elif "esc" == elem[0]:
1010 obj = _eval(elem[2], data)
1011
1012 if isinstance(obj, _dontescape) or isinstance(obj, TemplateBase):
1013 output.append(unicode(obj))
1014 else:
1015 output.append(escape(unicode(obj), elem[1]))
1016 elif "for" == elem[0]:
1017 do_else = True
1018 (names, iterable) = elem[1:3]
1019 try:
1020 loop_iter = iter(_eval(iterable, data))
1021 except TypeError:
1022 raise TemplateRenderError("Cannot loop over '%s'." % iterable)
1023 for i in loop_iter:
1024 do_else = False
1025 if len(names) == 1:
1026 data[names[0]] = i
1027 else:
1028 data.update(zip(names, i))
1029 output.extend(self.render(elem[3], data))
1030 elif "if" == elem[0]:
1031 do_else = True
1032 if _eval(elem[1], data):
1033 do_else = False
1034 output.extend(self.render(elem[2], data))
1035 elif "elif" == elem[0]:
1036 if do_else and _eval(elem[1], data):
1037 do_else = False
1038 output.extend(self.render(elem[2], data))
1039 elif "else" == elem[0]:
1040 if do_else:
1041 do_else = False
1042 output.extend(self.render(elem[1], data))
1043 elif "macro" == elem[0]:
1044 data[elem[1]] = TemplateBase(elem[2], data, self.render)
1045 else:
1046 raise TemplateRenderError("invalid parse-tree (%s)" %(elem))
1047
1048 return output
1049
1050
1051
1052
1053
1055 """doctest this module."""
1056 import doctest
1057 doctest.testmod()
1058
1059
1060 if __name__ == '__main__':
1061 _doctest()
1062
1063
1064