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