Package PyFoam :: Package ThirdParty :: Module pyratemp
[hide private]
[frames] | no frames]

Source Code for Module PyFoam.ThirdParty.pyratemp

   1  #!/usr/bin/env python 
   2  # -*- coding: utf-8 -*- 
   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: &lt;&gt;&amp;&#39;&quot;, 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 &#228;&#246;&#252;&#8364;' 
  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  # convert to unicode (independent of python3) 
191 -def toUniCode(s):
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 # some useful functions 201 202 #---------------------- 203 # string-position: i <-> row,col 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
221 -def sindex(string, row, col):
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
237 -def dictkeyclean(d):
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
252 -def dummy_raise(exception, value):
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 # escaping 263 264 (NONE, HTML, LATEX) = range(0, 3) 265 ESCAPE_SUPPORTED = {"NONE":None, "HTML":HTML, "LATEX":LATEX} #for error-/parameter-checking 266
267 -def escape(s, format=HTML):
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 #Note: If you have to make sure that every character gets replaced 285 # only once (and if you cannot achieve this with the following code), 286 # use something like u"".join([replacedict.get(c,c) for c in s]) 287 # which is about 2-3 times slower (but maybe needs less memory). 288 #Note: This is one of the most time-consuming parts of the template. 289 # So maybe speed this up. 290 291 if format is None or format == NONE: 292 pass 293 elif format == HTML: 294 s = s.replace(toUniCode("&"), toUniCode("&amp;")) # must be done first! 295 s = s.replace(toUniCode("<"), toUniCode("&lt;")) 296 s = s.replace(toUniCode(">"), toUniCode("&gt;")) 297 s = s.replace(u('"'), toUniCode("&quot;")) 298 s = s.replace(toUniCode("'"), toUniCode("&#39;")) 299 elif format == LATEX: 300 #TODO: which are the "reserved" characters for LaTeX? 301 # are there more than these? 302 s = s.replace("\\", toUniCode("\\backslash{}")) #must be done first! 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 # Exceptions 319
320 -class TemplateException(Exception):
321 """Base class for template-exceptions.""" 322 pass
323
324 -class TemplateParseError(TemplateException):
325 """Template parsing failed."""
326 - def __init__(self, err, errpos):
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)
335 - def __str__(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
341 -class TemplateSyntaxError(TemplateParseError, SyntaxError):
342 """Template syntax-error.""" 343 pass
344
345 -class TemplateIncludeError(TemplateParseError):
346 """Template 'include' failed.""" 347 pass
348
349 -class TemplateRenderError(TemplateException):
350 """Template rendering failed.""" 351 pass
352 353 #----------------------------------------- 354 # Loader 355
356 -class LoaderString:
357 """Load template from a string/unicode. 358 359 Note that 'include' is not possible in such templates. 360 """
361 - def __init__(self, encoding='utf-8'):
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
373 -class LoaderFile:
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 # Parser 424
425 -class Parser(object):
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 # template-syntax 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 # build regexps 444 # comment 445 # single-line, until end-tag or end-of-line. 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 # escaped or unescaped substitution 451 # single-line ("|$" is needed to be able to generate good error-messges) 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 # block 463 # - single-line, no nesting. 464 # or 465 # - multi-line, nested by whitespace indentation: 466 # * start- and end-tag of a block must have exactly the same indentation. 467 # * start- and end-tags of *nested* blocks should have a greater indentation. 468 # NOTE: A single-line block must not start at beginning of the line with 469 # the same indentation as the enclosing multi-line blocks! 470 # Note that " " and "\t" are different, although they may 471 # look the same in an editor! 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 # "for"-block parameters: "var(,var)* in ..." 496 _strForParam = r"""^(?P<names>\w+(?:\s*,\s*\w+)*)\s+in\s+(?P<iter>.+)$""" 497 _reForParam = re.compile(_strForParam) 498 499 # allowed macro-names 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: # test if testexpr() works 524 testexpr("i==1") 525 except Exception: 526 err = sys.exc_info()[1] # Needed because python 2.5 does not support 'as e' 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)] # for error-messages (_errpos) 547 return self._parse(template)
548
549 - def _errpos(self, fpos):
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
554 - def _testexpr(self, expr, fpos=0):
555 """Test a template-expression to detect errors.""" 556 try: 557 self._testexprfunc(expr) 558 except SyntaxError: 559 err = sys.exc_info()[1] # Needed because python 2.5 does not support 'as e' 560 raise TemplateSyntaxError(err, self._errpos(fpos))
561
562 - def _parse_sub(self, parsetree, text, fpos=0):
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 # blank out comments 602 # (So that its content does not collide with other syntax, and 603 # because removing them completely would falsify the character- 604 # position ("match.start()") of error-messages) 605 template = self._reComment.sub(lambda match: self._comment_start+" "*len(match.group(1))+match.group(2), template) 606 607 # init parser 608 parsetree = [] 609 curr = 0 # current position (= end of previous block) 610 block_type = None # block type: if,for,macro,raw,... 611 block_indent = None # None: single-line, >=0: multi-line 612 613 # find blocks 614 for match in self._reBlock.finditer(template): 615 start = match.start() 616 # process template-part before this block 617 if start > curr: 618 self._parse_sub(parsetree, template[curr:start], fpos) 619 620 # analyze block syntax (incl. error-checking and -messages) 621 keyword = None 622 block = match.groupdict() 623 pos__ = fpos + start # shortcut 624 if block["sKeyw"] is not None: # single-line block tag 625 block_indent = None 626 keyword = block["sKeyw"] 627 param = block["sParam"] 628 content = block["sContent"] 629 if block["sSpace"]: # restore spaces before start-tag 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") # shortcuts 635 pos_c = fpos + match.start("sContent") 636 elif block["mKeyw"] is not None: # multi-line block tag 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: # multi-line block end 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: # single-line block end 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 # analyze block content (mainly error-checking and -messages) 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 # make sure param is "\w+" (instead of ".+") 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 #remove last newline 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 # parser-commands 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] # Needed because python 2.5 does not support 'as e' 732 raise TemplateIncludeError(err, self._errpos(pos__)) 733 self._includestack.append((content.strip(), u)) # current filename/template for error-msg. 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: # process template-part after last block 755 self._parse_sub(parsetree, template[curr:], fpos) 756 757 return parsetree
758 759 #----------------------------------------- 760 # Evaluation 761 762 # some checks 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
768 -class EvalPseudoSandbox:
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 }
835 - def __init__(self):
836 self._compile_cache = {} 837 self.locals_ptr = None 838 self.eval_allowed_globals = self.safe_builtins.copy() 839 if not PY3: 840 for k,d in iteritems(self.safe_builtins_python2): 841 self.eval_allowed_globals[k]=eval(d) 842 self.register("__import__", self.f_import) 843 self.register("exists", self.f_exists) 844 self.register("default", self.f_default) 845 self.register("setvar", self.f_setvar)
846
847 - def register(self, name, obj):
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
854 - def compile(self, expr):
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: #prevent breakout via new-style-classes 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
922 - def f_exists(self, varname):
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
936 - def f_default(self, expr, default=None):
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 #TODO: which exceptions should be catched here? 960 except (NameError, IndexError, KeyError): 961 return default
962
963 - def f_setvar(self, name, expr):
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 # basic template / subtemplate 975
976 -class TemplateBase:
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 #TODO: parameter-checking? 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
1005 - def __call__(self, **override):
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 # restore current_data 1019 return _dontescape(u) # (see class _dontescape)
1020
1021 - def __unicode__(self):
1022 """Alias for __call__().""" 1023 return self.__call__()
1024 - def __str__(self):
1025 """Only here for completeness. Use __unicode__ instead!""" 1026 return self.__call__()
1027 1028 #----------------------------------------- 1029 # Renderer 1030
1031 -class _dontescape(text_type):
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
1044 -class Renderer(object):
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 #TODO: test evalfunc 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 #TODO: any other errors to catch here? 1068 except (TypeError,NameError,IndexError,KeyError,AttributeError, SyntaxError): 1069 err = sys.exc_info()[1] # Needed because python 2.5 does not support 'as e' 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 # shortcut 1083 output = [] 1084 do_else = False # use else/elif-branch? 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 #prevent double-escape 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))) #"for a,b,.. in list" 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 # template user-interface (putting all together) 1136
1137 -class Template(TemplateBase):
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 # load template 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 # eval (incl. compile-cache) 1185 templateeval = eval_class() 1186 1187 # parse 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 # renderer 1194 renderfunc = renderer_class(templateeval.eval, escape_func).render 1195 1196 #create template 1197 TemplateBase.__init__(self, parsetree, renderfunc, data)
1198 1199 1200 #========================================= 1201 #doctest 1202
1203 -def _doctest():
1204 """doctest this module.""" 1205 import doctest 1206 doctest.testmod()
1207 1208 #---------------------- 1209 if __name__ == '__main__': 1210 _doctest() 1211 1212 #========================================= 1213 1214 # Should work with Python3 and Python2 1215