libuav original
Dependents: UAVCAN UAVCAN_Subscriber
pyratemp.py
00001 #!/usr/bin/env python 00002 # -*- coding: utf-8 -*- 00003 """ 00004 Small, simple and powerful template-engine for Python. 00005 00006 A template-engine for Python, which is very simple, easy to use, small, 00007 fast, powerful, modular, extensible, well documented and pythonic. 00008 00009 See documentation for a list of features, template-syntax etc. 00010 00011 :Version: 0.3.0 00012 :Requires: Python >=2.6 / 3.x 00013 00014 :Usage: 00015 see class ``Template`` and examples below. 00016 00017 :Example: 00018 00019 Note that the examples are in Python 2; they also work in 00020 Python 3 if you replace u"..." by "...", unicode() by str() 00021 and partly "..." by b"...". 00022 00023 quickstart:: 00024 >>> t = Template("hello @!name!@") 00025 >>> print(t(name="marvin")) 00026 hello marvin 00027 00028 quickstart with a template-file:: 00029 # >>> t = Template(filename="mytemplate.tmpl") 00030 # >>> print(t(name="marvin")) 00031 # hello marvin 00032 00033 generic usage:: 00034 >>> t = Template(u"output is in Unicode \\xe4\\xf6\\xfc\\u20ac") 00035 >>> t #doctest: +ELLIPSIS 00036 <...Template instance at 0x...> 00037 >>> t() 00038 u'output is in Unicode \\xe4\\xf6\\xfc\\u20ac' 00039 >>> unicode(t) 00040 u'output is in Unicode \\xe4\\xf6\\xfc\\u20ac' 00041 00042 with data:: 00043 >>> t = Template("hello @!name!@", data={"name":"world"}) 00044 >>> t() 00045 u'hello world' 00046 >>> t(name="worlds") 00047 u'hello worlds' 00048 00049 # >>> t(note="data must be Unicode or ASCII", name=u"\\xe4") 00050 # u'hello \\xe4' 00051 00052 escaping:: 00053 >>> t = Template("hello escaped: @!name!@, unescaped: $!name!$") 00054 >>> t(name='''<>&'"''') 00055 u'hello escaped: <>&'", unescaped: <>&\\'"' 00056 00057 result-encoding:: 00058 # encode the unicode-object to your encoding with encode() 00059 >>> t = Template(u"hello \\xe4\\xf6\\xfc\\u20ac") 00060 >>> result = t() 00061 >>> result 00062 u'hello \\xe4\\xf6\\xfc\\u20ac' 00063 >>> result.encode("utf-8") 00064 'hello \\xc3\\xa4\\xc3\\xb6\\xc3\\xbc\\xe2\\x82\\xac' 00065 >>> result.encode("ascii") 00066 Traceback (most recent call last): 00067 ... 00068 UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-9: ordinal not in range(128) 00069 >>> result.encode("ascii", 'xmlcharrefreplace') 00070 'hello äöü€' 00071 00072 Python-expressions:: 00073 >>> Template('formatted: @! "%8.5f" % value !@')(value=3.141592653) 00074 u'formatted: 3.14159' 00075 >>> Template("hello --@!name.upper().center(20)!@--")(name="world") 00076 u'hello -- WORLD --' 00077 >>> Template("calculate @!var*5+7!@")(var=7) 00078 u'calculate 42' 00079 00080 blocks (if/for/macros/...):: 00081 >>> t = Template("<!--(if foo == 1)-->bar<!--(elif foo == 2)-->baz<!--(else)-->unknown(@!foo!@)<!--(end)-->") 00082 >>> t(foo=2) 00083 u'baz' 00084 >>> t(foo=5) 00085 u'unknown(5)' 00086 00087 >>> t = Template("<!--(for i in mylist)-->@!i!@ <!--(else)-->(empty)<!--(end)-->") 00088 >>> t(mylist=[]) 00089 u'(empty)' 00090 >>> t(mylist=[1,2,3]) 00091 u'1 2 3 ' 00092 00093 >>> t = Template("<!--(for i,elem in enumerate(mylist))--> - @!i!@: @!elem!@<!--(end)-->") 00094 >>> t(mylist=["a","b","c"]) 00095 u' - 0: a - 1: b - 2: c' 00096 00097 >>> t = Template('<!--(macro greetings)-->hello <strong>@!name!@</strong><!--(end)--> @!greetings(name=user)!@') 00098 >>> t(user="monty") 00099 u' hello <strong>monty</strong>' 00100 00101 exists:: 00102 >>> t = Template('<!--(if exists("foo"))-->YES<!--(else)-->NO<!--(end)-->') 00103 >>> t() 00104 u'NO' 00105 >>> t(foo=1) 00106 u'YES' 00107 >>> t(foo=None) # note this difference to 'default()' 00108 u'YES' 00109 00110 default-values:: 00111 # non-existing variables raise an error 00112 >>> Template('hi @!optional!@')() 00113 Traceback (most recent call last): 00114 ... 00115 TemplateRenderError: Cannot eval expression 'optional'. (NameError: name 'optional' is not defined) 00116 00117 >>> t = Template('hi @!default("optional","anyone")!@') 00118 >>> t() 00119 u'hi anyone' 00120 >>> t(optional=None) 00121 u'hi anyone' 00122 >>> t(optional="there") 00123 u'hi there' 00124 00125 # the 1st parameter can be any eval-expression 00126 >>> t = Template('@!default("5*var1+var2","missing variable")!@') 00127 >>> t(var1=10) 00128 u'missing variable' 00129 >>> t(var1=10, var2=2) 00130 u'52' 00131 00132 # also in blocks 00133 >>> t = Template('<!--(if default("opt1+opt2",0) > 0)-->yes<!--(else)-->no<!--(end)-->') 00134 >>> t() 00135 u'no' 00136 >>> t(opt1=23, opt2=42) 00137 u'yes' 00138 00139 >>> t = Template('<!--(for i in default("optional_list",[]))-->@!i!@<!--(end)-->') 00140 >>> t() 00141 u'' 00142 >>> t(optional_list=[1,2,3]) 00143 u'123' 00144 00145 00146 # but make sure to put the expression in quotation marks, otherwise: 00147 >>> Template('@!default(optional,"fallback")!@')() 00148 Traceback (most recent call last): 00149 ... 00150 TemplateRenderError: Cannot eval expression 'default(optional,"fallback")'. (NameError: name 'optional' is not defined) 00151 00152 setvar:: 00153 >>> t = Template('$!setvar("i", "i+1")!$@!i!@') 00154 >>> t(i=6) 00155 u'7' 00156 00157 >>> t = Template('''<!--(if isinstance(s, (list,tuple)))-->$!setvar("s", '"\\\\\\\\n".join(s)')!$<!--(end)-->@!s!@''') 00158 >>> t(isinstance=isinstance, s="123") 00159 u'123' 00160 >>> t(isinstance=isinstance, s=["123", "456"]) 00161 u'123\\n456' 00162 00163 :Author: Roland Koebler (rk at simple-is-better dot org) 00164 :Copyright: Roland Koebler 00165 :License: MIT/X11-like, see __license__ 00166 00167 :RCS: $Id: pyratemp.py,v 1.12 2013/04/02 20:26:06 rk Exp $ 00168 """ 00169 from __future__ import unicode_literals 00170 00171 __version__ = "0.3.0" 00172 __author__ = "Roland Koebler <rk at simple-is-better dot org>" 00173 __license__ = """Copyright (c) Roland Koebler, 2007-2013 00174 00175 Permission is hereby granted, free of charge, to any person obtaining a copy 00176 of this software and associated documentation files (the "Software"), to deal 00177 in the Software without restriction, including without limitation the rights 00178 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 00179 copies of the Software, and to permit persons to whom the Software is 00180 furnished to do so, subject to the following conditions: 00181 00182 The above copyright notice and this permission notice shall be included in 00183 all copies or substantial portions of the Software. 00184 00185 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 00186 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 00187 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 00188 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 00189 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 00190 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 00191 IN THE SOFTWARE.""" 00192 00193 #========================================= 00194 00195 import os, re, sys 00196 if sys.version_info[0] >= 3: 00197 import builtins 00198 unicode = str 00199 long = int 00200 else: 00201 import __builtin__ as builtins 00202 from codecs import open 00203 00204 #========================================= 00205 # some useful functions 00206 00207 #---------------------- 00208 # string-position: i <-> row,col 00209 00210 def srow(string, i): 00211 """Get line numer of ``string[i]`` in `string`. 00212 00213 :Returns: row, starting at 1 00214 :Note: This works for text-strings with ``\\n`` or ``\\r\\n``. 00215 """ 00216 return string.count('\n', 0, max(0, i)) + 1 00217 00218 def scol(string, i): 00219 """Get column number of ``string[i]`` in `string`. 00220 00221 :Returns: column, starting at 1 (but may be <1 if i<0) 00222 :Note: This works for text-strings with ``\\n`` or ``\\r\\n``. 00223 """ 00224 return i - string.rfind('\n', 0, max(0, i)) 00225 00226 def sindex(string, row, col): 00227 """Get index of the character at `row`/`col` in `string`. 00228 00229 :Parameters: 00230 - `row`: row number, starting at 1. 00231 - `col`: column number, starting at 1. 00232 :Returns: ``i``, starting at 0 (but may be <1 if row/col<0) 00233 :Note: This works for text-strings with '\\n' or '\\r\\n'. 00234 """ 00235 n = 0 00236 for _ in range(row-1): 00237 n = string.find('\n', n) + 1 00238 return n+col-1 00239 00240 #---------------------- 00241 00242 def dictkeyclean(d): 00243 """Convert all keys of the dict `d` to strings. 00244 """ 00245 new_d = {} 00246 for k, v in d.items(): 00247 new_d[str(k)] = v 00248 return new_d 00249 00250 #---------------------- 00251 00252 def dummy(*_, **__): 00253 """Dummy function, doing nothing. 00254 """ 00255 pass 00256 00257 def dummy_raise(exception, value): 00258 """Create an exception-raising dummy function. 00259 00260 :Returns: dummy function, raising ``exception(value)`` 00261 """ 00262 def mydummy(*_, **__): 00263 raise exception(value) 00264 return mydummy 00265 00266 #========================================= 00267 # escaping 00268 00269 (NONE, HTML, LATEX, MAIL_HEADER) = range(0, 4) 00270 ESCAPE_SUPPORTED = {"NONE":None, "HTML":HTML, "LATEX":LATEX, "MAIL_HEADER":MAIL_HEADER} 00271 00272 def escape(s, format=HTML): 00273 """Replace special characters by their escape sequence. 00274 00275 :Parameters: 00276 - `s`: unicode-string to escape 00277 - `format`: 00278 00279 - `NONE`: nothing is replaced 00280 - `HTML`: replace &<>'" by &...; 00281 - `LATEX`: replace \#$%&_{}~^ 00282 - `MAIL_HEADER`: escape non-ASCII mail-header-contents 00283 :Returns: 00284 the escaped string in unicode 00285 :Exceptions: 00286 - `ValueError`: if `format` is invalid. 00287 00288 :Uses: 00289 MAIL_HEADER uses module email 00290 """ 00291 #Note: If you have to make sure that every character gets replaced 00292 # only once (and if you cannot achieve this with the following code), 00293 # use something like "".join([replacedict.get(c,c) for c in s]) 00294 # which is about 2-3 times slower (but maybe needs less memory). 00295 #Note: This is one of the most time-consuming parts of the template. 00296 if format is None or format == NONE: 00297 pass 00298 elif format == HTML: 00299 s = s.replace("&", "&") # must be done first! 00300 s = s.replace("<", "<") 00301 s = s.replace(">", ">") 00302 s = s.replace('"', """) 00303 s = s.replace("'", "'") 00304 elif format == LATEX: 00305 s = s.replace("\\", "\\x") #must be done first! 00306 s = s.replace("#", "\\#") 00307 s = s.replace("$", "\\$") 00308 s = s.replace("%", "\\%") 00309 s = s.replace("&", "\\&") 00310 s = s.replace("_", "\\_") 00311 s = s.replace("{", "\\{") 00312 s = s.replace("}", "\\}") 00313 s = s.replace("\\x","\\textbackslash{}") 00314 s = s.replace("~", "\\textasciitilde{}") 00315 s = s.replace("^", "\\textasciicircum{}") 00316 elif format == MAIL_HEADER: 00317 import email.header 00318 try: 00319 s.encode("ascii") 00320 return s 00321 except UnicodeEncodeError: 00322 return email.header.make_header([(s, "utf-8")]).encode() 00323 else: 00324 raise ValueError('Invalid format (only None, HTML, LATEX and MAIL_HEADER are supported).') 00325 return s 00326 00327 #========================================= 00328 00329 #----------------------------------------- 00330 # Exceptions 00331 00332 class TemplateException (Exception): 00333 """Base class for template-exceptions.""" 00334 pass 00335 00336 class TemplateParseError(TemplateException): 00337 """Template parsing failed.""" 00338 def __init__ (self, err, errpos): 00339 """ 00340 :Parameters: 00341 - `err`: error-message or exception to wrap 00342 - `errpos`: ``(filename,row,col)`` where the error occured. 00343 """ 00344 self.err = err 00345 self.filename, self.row, self.col = errpos 00346 TemplateException.__init__(self) 00347 def __str__(self): 00348 if not self.filename: 00349 return "line %d, col %d: %s" % (self.row, self.col , str(self.err )) 00350 else: 00351 return "file %s, line %d, col %d: %s" % (self.filename, self.row, self.col , str(self.err )) 00352 00353 class TemplateSyntaxError (TemplateParseError , SyntaxError): 00354 """Template syntax-error.""" 00355 pass 00356 00357 class TemplateIncludeError(TemplateParseError): 00358 """Template 'include' failed.""" 00359 pass 00360 00361 class TemplateRenderError (TemplateException ): 00362 """Template rendering failed.""" 00363 pass 00364 00365 #----------------------------------------- 00366 # Loader 00367 00368 class LoaderString : 00369 """Load template from a string/unicode. 00370 00371 Note that 'include' is not possible in such templates. 00372 """ 00373 def __init__(self, encoding='utf-8'): 00374 self.encoding = encoding 00375 00376 def load (self, s): 00377 """Return template-string as unicode. 00378 """ 00379 if isinstance(s, unicode): 00380 u = s 00381 else: 00382 u = s.decode(self.encoding ) 00383 return u 00384 00385 class LoaderFile : 00386 """Load template from a file. 00387 00388 When loading a template from a file, it's possible to including other 00389 templates (by using 'include' in the template). But for simplicity 00390 and security, all included templates have to be in the same directory! 00391 (see ``allowed_path``) 00392 """ 00393 def __init__ (self, allowed_path=None, encoding='utf-8'): 00394 """Init the loader. 00395 00396 :Parameters: 00397 - `allowed_path`: path of the template-files 00398 - `encoding`: encoding of the template-files 00399 :Exceptions: 00400 - `ValueError`: if `allowed_path` is not a directory 00401 """ 00402 if allowed_path and not os.path.isdir(allowed_path): 00403 raise ValueError("'allowed_path' has to be a directory.") 00404 self.path = allowed_path 00405 self.encoding = encoding 00406 00407 def load (self, filename): 00408 """Load a template from a file. 00409 00410 Check if filename is allowed and return its contens in unicode. 00411 00412 :Parameters: 00413 - `filename`: filename of the template without path 00414 :Returns: 00415 the contents of the template-file in unicode 00416 :Exceptions: 00417 - `ValueError`: if `filename` contains a path 00418 """ 00419 if filename != os.path.basename(filename): 00420 raise ValueError("No path allowed in filename. (%s)" %(filename)) 00421 filename = os.path.join(self.path , filename) 00422 00423 f = open(filename, 'r', encoding=self.encoding) 00424 u = f.read() 00425 f.close() 00426 00427 return u 00428 00429 #----------------------------------------- 00430 # Parser 00431 00432 class Parser (object): 00433 """Parse a template into a parse-tree. 00434 00435 Includes a syntax-check, an optional expression-check and verbose 00436 error-messages. 00437 00438 See documentation for a description of the parse-tree. 00439 """ 00440 # template-syntax 00441 _comment_start = "#!" 00442 _comment_end = "!#" 00443 _sub_start = "$!" 00444 _sub_end = "!$" 00445 _subesc_start = "@!" 00446 _subesc_end = "!@" 00447 _block_start = "<!--(" 00448 _block_end = ")-->" 00449 00450 # build regexps 00451 # comment 00452 # single-line, until end-tag or end-of-line. 00453 _strComment = r"""%s(?P<content>.*?)(?P<end>%s|\n|$)""" \ 00454 % (re.escape(_comment_start), re.escape(_comment_end)) 00455 _reComment = re.compile(_strComment, re.M) 00456 00457 # escaped or unescaped substitution 00458 # single-line ("|$" is needed to be able to generate good error-messges) 00459 _strSubstitution = r""" 00460 ( 00461 %s\s*(?P<sub>.*?)\s*(?P<end>%s|$) #substitution 00462 | 00463 %s\s*(?P<escsub>.*?)\s*(?P<escend>%s|$) #escaped substitution 00464 ) 00465 """ % (re.escape(_sub_start), re.escape(_sub_end), 00466 re.escape(_subesc_start), re.escape(_subesc_end)) 00467 _reSubstitution = re.compile(_strSubstitution, re.X|re.M) 00468 00469 # block 00470 # - single-line, no nesting. 00471 # or 00472 # - multi-line, nested by whitespace indentation: 00473 # * start- and end-tag of a block must have exactly the same indentation. 00474 # * start- and end-tags of *nested* blocks should have a greater indentation. 00475 # NOTE: A single-line block must not start at beginning of the line with 00476 # the same indentation as the enclosing multi-line blocks! 00477 # Note that " " and "\t" are different, although they may 00478 # look the same in an editor! 00479 _s = re.escape(_block_start) 00480 _e = re.escape(_block_end) 00481 _strBlock = r""" 00482 ^(?P<mEnd>[ \t]*)%send%s(?P<meIgnored>.*)\r?\n? # multi-line end (^ <!--(end)-->IGNORED_TEXT\n) 00483 | 00484 (?P<sEnd>)%send%s # single-line end (<!--(end)-->) 00485 | 00486 (?P<sSpace>[ \t]*) # single-line tag (no nesting) 00487 %s(?P<sKeyw>\w+)[ \t]*(?P<sParam>.*?)%s 00488 (?P<sContent>.*?) 00489 (?=(?:%s.*?%s.*?)??%send%s) # (match until end or i.e. <!--(elif/else...)-->) 00490 | 00491 # multi-line tag, nested by whitespace indentation 00492 ^(?P<indent>[ \t]*) # save indentation of start tag 00493 %s(?P<mKeyw>\w+)\s*(?P<mParam>.*?)%s(?P<mIgnored>.*)\r?\n 00494 (?P<mContent>(?:.*\n)*?) 00495 (?=(?P=indent)%s(?:.|\s)*?%s) # match indentation 00496 """ % (_s, _e, 00497 _s, _e, 00498 _s, _e, _s, _e, _s, _e, 00499 _s, _e, _s, _e) 00500 _reBlock = re.compile(_strBlock, re.X|re.M) 00501 00502 # "for"-block parameters: "var(,var)* in ..." 00503 _strForParam = r"""^(?P<names>\w+(?:\s*,\s*\w+)*)\s+in\s+(?P<iter>.+)$""" 00504 _reForParam = re.compile(_strForParam) 00505 00506 # allowed macro-names 00507 _reMacroParam = re.compile(r"""^\w+$""") 00508 00509 00510 def __init__ (self, loadfunc=None, testexpr=None, escape=HTML): 00511 """Init the parser. 00512 00513 :Parameters: 00514 - `loadfunc`: function to load included templates 00515 (i.e. ``LoaderFile(...).load``) 00516 - `testexpr`: function to test if a template-expressions is valid 00517 (i.e. ``EvalPseudoSandbox().compile``) 00518 - `escape`: default-escaping (may be modified by the template) 00519 :Exceptions: 00520 - `ValueError`: if `testexpr` or `escape` is invalid. 00521 """ 00522 if loadfunc is None: 00523 self._load = dummy_raise(NotImplementedError, "'include' not supported, since no 'loadfunc' was given.") 00524 else: 00525 self._load = loadfunc 00526 00527 if testexpr is None: 00528 self._testexprfunc = dummy 00529 else: 00530 try: # test if testexpr() works 00531 testexpr("i==1") 00532 except Exception as err: 00533 raise ValueError("Invalid 'testexpr'. (%s)" %(err)) 00534 self._testexprfunc = testexpr 00535 00536 if escape not in ESCAPE_SUPPORTED.values(): 00537 raise ValueError("Unsupported 'escape'. (%s)" %(escape)) 00538 self.escape = escape 00539 self._includestack = [] 00540 00541 def parse (self, template): 00542 """Parse a template. 00543 00544 :Parameters: 00545 - `template`: template-unicode-string 00546 :Returns: the resulting parse-tree 00547 :Exceptions: 00548 - `TemplateSyntaxError`: for template-syntax-errors 00549 - `TemplateIncludeError`: if template-inclusion failed 00550 - `TemplateException` 00551 """ 00552 self._includestack = [(None, template)] # for error-messages (_errpos) 00553 return self._parse (template) 00554 00555 def _errpos(self, fpos): 00556 """Convert `fpos` to ``(filename,row,column)`` for error-messages.""" 00557 filename, string = self._includestack [-1] 00558 return filename, srow(string, fpos), scol(string, fpos) 00559 00560 def _testexpr(self, expr, fpos=0): 00561 """Test a template-expression to detect errors.""" 00562 try: 00563 self._testexprfunc (expr) 00564 except SyntaxError as err: 00565 raise TemplateSyntaxError(err, self._errpos (fpos)) 00566 00567 def _parse_sub(self, parsetree, text, fpos=0): 00568 """Parse substitutions, and append them to the parse-tree. 00569 00570 Additionally, remove comments. 00571 """ 00572 curr = 0 00573 for match in self._reSubstitution .finditer(text): 00574 start = match.start() 00575 if start > curr: 00576 parsetree.append(("str", self._reComment .sub('', text[curr:start]))) 00577 00578 if match.group("sub") is not None: 00579 if not match.group("end"): 00580 raise TemplateSyntaxError("Missing closing tag '%s' for '%s'." 00581 % (self._sub_end , match.group()), self._errpos (fpos+start)) 00582 if len(match.group("sub")) > 0: 00583 self._testexpr (match.group("sub"), fpos+start) 00584 parsetree.append(("sub", match.group("sub"))) 00585 else: 00586 assert(match.group("escsub") is not None) 00587 if not match.group("escend"): 00588 raise TemplateSyntaxError("Missing closing tag '%s' for '%s'." 00589 % (self._subesc_end , match.group()), self._errpos (fpos+start)) 00590 if len(match.group("escsub")) > 0: 00591 self._testexpr (match.group("escsub"), fpos+start) 00592 parsetree.append(("esc", self.escape , match.group("escsub"))) 00593 00594 curr = match.end() 00595 00596 if len(text) > curr: 00597 parsetree.append(("str", self._reComment .sub('', text[curr:]))) 00598 00599 def _parse(self, template, fpos=0): 00600 """Recursive part of `parse()`. 00601 00602 :Parameters: 00603 - template 00604 - fpos: position of ``template`` in the complete template (for error-messages) 00605 """ 00606 # blank out comments 00607 # (So that its content does not collide with other syntax, and 00608 # because removing them completely would falsify the character- 00609 # position ("match.start()") of error-messages) 00610 template = self._reComment .sub(lambda match: self._comment_start +" "*len(match.group(1))+match.group(2), template) 00611 00612 # init parser 00613 parsetree = [] 00614 curr = 0 # current position (= end of previous block) 00615 block_type = None # block type: if,for,macro,raw,... 00616 block_indent = None # None: single-line, >=0: multi-line 00617 00618 # find blocks 00619 for match in self._reBlock .finditer(template): 00620 start = match.start() 00621 # process template-part before this block 00622 if start > curr: 00623 self._parse_sub (parsetree, template[curr:start], fpos) 00624 00625 # analyze block syntax (incl. error-checking and -messages) 00626 keyword = None 00627 block = match.groupdict() 00628 pos__ = fpos + start # shortcut 00629 if block["sKeyw"] is not None: # single-line block tag 00630 block_indent = None 00631 keyword = block["sKeyw"] 00632 param = block["sParam"] 00633 content = block["sContent"] 00634 if block["sSpace"]: # restore spaces before start-tag 00635 if len(parsetree) > 0 and parsetree[-1][0] == "str": 00636 parsetree[-1] = ("str", parsetree[-1][1] + block["sSpace"]) 00637 else: 00638 parsetree.append(("str", block["sSpace"])) 00639 pos_p = fpos + match.start("sParam") # shortcuts 00640 pos_c = fpos + match.start("sContent") 00641 elif block["mKeyw"] is not None: # multi-line block tag 00642 block_indent = len(block["indent"]) 00643 keyword = block["mKeyw"] 00644 param = block["mParam"] 00645 content = block["mContent"] 00646 pos_p = fpos + match.start("mParam") 00647 pos_c = fpos + match.start("mContent") 00648 ignored = block["mIgnored"].strip() 00649 if ignored and ignored != self._comment_start : 00650 raise TemplateSyntaxError("No code allowed after block-tag.", self._errpos (fpos+match.start("mIgnored"))) 00651 elif block["mEnd"] is not None: # multi-line block end 00652 if block_type is None: 00653 raise TemplateSyntaxError("No block to end here/invalid indent.", self._errpos (pos__) ) 00654 if block_indent != len(block["mEnd"]): 00655 raise TemplateSyntaxError("Invalid indent for end-tag.", self._errpos (pos__) ) 00656 ignored = block["meIgnored"].strip() 00657 if ignored and ignored != self._comment_start : 00658 raise TemplateSyntaxError("No code allowed after end-tag.", self._errpos (fpos+match.start("meIgnored"))) 00659 block_type = None 00660 elif block["sEnd"] is not None: # single-line block end 00661 if block_type is None: 00662 raise TemplateSyntaxError("No block to end here/invalid indent.", self._errpos (pos__)) 00663 if block_indent is not None: 00664 raise TemplateSyntaxError("Invalid indent for end-tag.", self._errpos (pos__)) 00665 block_type = None 00666 else: 00667 raise TemplateException("FATAL: Block regexp error. Please contact the author. (%s)" % match.group()) 00668 00669 # analyze block content (mainly error-checking and -messages) 00670 if keyword: 00671 keyword = keyword.lower() 00672 if 'for' == keyword: 00673 if block_type is not None: 00674 raise TemplateSyntaxError("Missing block-end-tag before new block at '%s'." %(match.group()), self._errpos (pos__)) 00675 block_type = 'for' 00676 cond = self._reForParam .match(param) 00677 if cond is None: 00678 raise TemplateSyntaxError("Invalid 'for ...' at '%s'." %(param), self._errpos (pos_p)) 00679 names = tuple(n.strip() for n in cond.group("names").split(",")) 00680 self._testexpr (cond.group("iter"), pos_p+cond.start("iter")) 00681 parsetree.append(("for", names, cond.group("iter"), self._parse (content, pos_c))) 00682 elif 'if' == keyword: 00683 if block_type is not None: 00684 raise TemplateSyntaxError("Missing block-end-tag before new block at '%s'." %(match.group()), self._errpos (pos__)) 00685 if not param: 00686 raise TemplateSyntaxError("Missing condition for 'if' at '%s'." %(match.group()), self._errpos (pos__)) 00687 block_type = 'if' 00688 self._testexpr (param, pos_p) 00689 parsetree.append(("if", param, self._parse (content, pos_c))) 00690 elif 'elif' == keyword: 00691 if block_type != 'if': 00692 raise TemplateSyntaxError("'elif' may only appear after 'if' at '%s'." %(match.group()), self._errpos (pos__)) 00693 if not param: 00694 raise TemplateSyntaxError("Missing condition for 'elif' at '%s'." %(match.group()), self._errpos (pos__)) 00695 self._testexpr (param, pos_p) 00696 parsetree.append(("elif", param, self._parse (content, pos_c))) 00697 elif 'else' == keyword: 00698 if block_type not in ('if', 'for'): 00699 raise TemplateSyntaxError("'else' may only appear after 'if' or 'for' at '%s'." %(match.group()), self._errpos (pos__)) 00700 if param: 00701 raise TemplateSyntaxError("'else' may not have parameters at '%s'." %(match.group()), self._errpos (pos__)) 00702 parsetree.append(("else", self._parse (content, pos_c))) 00703 elif 'macro' == keyword: 00704 if block_type is not None: 00705 raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos (pos__)) 00706 block_type = 'macro' 00707 # make sure param is "\w+" (instead of ".+") 00708 if not param: 00709 raise TemplateSyntaxError("Missing name for 'macro' at '%s'." %(match.group()), self._errpos (pos__)) 00710 if not self._reMacroParam .match(param): 00711 raise TemplateSyntaxError("Invalid name for 'macro' at '%s'." %(match.group()), self._errpos (pos__)) 00712 #remove last newline 00713 if len(content) > 0 and content[-1] == '\n': 00714 content = content[:-1] 00715 if len(content) > 0 and content[-1] == '\r': 00716 content = content[:-1] 00717 parsetree.append(("macro", param, self._parse (content, pos_c))) 00718 00719 # parser-commands 00720 elif 'raw' == keyword: 00721 if block_type is not None: 00722 raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos (pos__)) 00723 if param: 00724 raise TemplateSyntaxError("'raw' may not have parameters at '%s'." %(match.group()), self._errpos (pos__)) 00725 block_type = 'raw' 00726 parsetree.append(("str", content)) 00727 elif 'include' == keyword: 00728 if block_type is not None: 00729 raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos (pos__)) 00730 if param: 00731 raise TemplateSyntaxError("'include' may not have parameters at '%s'." %(match.group()), self._errpos (pos__)) 00732 block_type = 'include' 00733 try: 00734 u = self._load (content.strip()) 00735 except Exception as err: 00736 raise TemplateIncludeError(err, self._errpos (pos__)) 00737 self._includestack .append((content.strip(), u)) # current filename/template for error-msg. 00738 p = self._parse (u) 00739 self._includestack .pop() 00740 parsetree.extend(p) 00741 elif 'set_escape' == keyword: 00742 if block_type is not None: 00743 raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos (pos__)) 00744 if param: 00745 raise TemplateSyntaxError("'set_escape' may not have parameters at '%s'." %(match.group()), self._errpos (pos__)) 00746 block_type = 'set_escape' 00747 esc = content.strip().upper() 00748 if esc not in ESCAPE_SUPPORTED: 00749 raise TemplateSyntaxError("Unsupported escape '%s'." %(esc), self._errpos (pos__)) 00750 self.escape = ESCAPE_SUPPORTED[esc] 00751 else: 00752 raise TemplateSyntaxError("Invalid keyword '%s'." %(keyword), self._errpos (pos__)) 00753 curr = match.end() 00754 00755 if block_type is not None: 00756 raise TemplateSyntaxError("Missing end-tag.", self._errpos (pos__)) 00757 00758 if len(template) > curr: # process template-part after last block 00759 self._parse_sub (parsetree, template[curr:], fpos+curr) 00760 00761 return parsetree 00762 00763 #----------------------------------------- 00764 # Evaluation 00765 00766 # some checks 00767 assert len(eval("dir()", {'__builtins__':{'dir':dir}})) == 1, \ 00768 "FATAL: 'eval' does not work as expected (%s)." 00769 assert compile("0 .__class__", "<string>", "eval").co_names == ('__class__',), \ 00770 "FATAL: 'compile' does not work as expected." 00771 00772 class EvalPseudoSandbox : 00773 """An eval-pseudo-sandbox. 00774 00775 The pseudo-sandbox restricts the available functions/objects, so the 00776 code can only access: 00777 00778 - some of the builtin Python-functions, which are considered "safe" 00779 (see safe_builtins) 00780 - some additional functions (exists(), default(), setvar(), escape()) 00781 - the passed objects incl. their methods. 00782 00783 Additionally, names beginning with "_" are forbidden. 00784 This is to prevent things like '0 .__class__', with which you could 00785 easily break out of a "sandbox". 00786 00787 Be careful to only pass "safe" objects/functions to the template, 00788 because any unsafe function/method could break the sandbox! 00789 For maximum security, restrict the access to as few objects/functions 00790 as possible! 00791 00792 :Warning: 00793 Note that this is no real sandbox! (And although I don't know any 00794 way to break out of the sandbox without passing-in an unsafe object, 00795 I cannot guarantee that there is no such way. So use with care.) 00796 00797 Take care if you want to use it for untrusted code!! 00798 """ 00799 00800 safe_builtins = { 00801 "True" : True, 00802 "False" : False, 00803 "None" : None, 00804 00805 "abs" : builtins.abs, 00806 "chr" : builtins.chr, 00807 "divmod" : builtins.divmod, 00808 "hash" : builtins.hash, 00809 "hex" : builtins.hex, 00810 "len" : builtins.len, 00811 "max" : builtins.max, 00812 "min" : builtins.min, 00813 "oct" : builtins.oct, 00814 "ord" : builtins.ord, 00815 "pow" : builtins.pow, 00816 "range" : builtins.range, 00817 "round" : builtins.round, 00818 "sorted" : builtins.sorted, 00819 "sum" : builtins.sum, 00820 "unichr" : builtins.chr, 00821 "zip" : builtins.zip, 00822 00823 "bool" : builtins.bool, 00824 "bytes" : builtins.bytes, 00825 "complex" : builtins.complex, 00826 "dict" : builtins.dict, 00827 "enumerate" : builtins.enumerate, 00828 "float" : builtins.float, 00829 "int" : builtins.int, 00830 "list" : builtins.list, 00831 "long" : long, 00832 "reversed" : builtins.reversed, 00833 "str" : builtins.str, 00834 "tuple" : builtins.tuple, 00835 "unicode" : unicode, 00836 } 00837 if sys.version_info[0] < 3: 00838 safe_builtins["unichr"] = builtins.unichr 00839 00840 def __init__(self): 00841 self._compile_cache = {} 00842 self.locals_ptr = None 00843 self.eval_allowed_globals = self.safe_builtins .copy() 00844 self.register ("__import__", self.f_import ) 00845 self.register ("exists", self.f_exists ) 00846 self.register ("default", self.f_default ) 00847 self.register ("setvar", self.f_setvar ) 00848 self.register ("escape", self.f_escape ) 00849 00850 def register (self, name, obj): 00851 """Add an object to the "allowed eval-globals". 00852 00853 Mainly useful to add user-defined functions to the pseudo-sandbox. 00854 """ 00855 self.eval_allowed_globals [name] = obj 00856 00857 def compile (self, expr): 00858 """Compile a Python-eval-expression. 00859 00860 - Use a compile-cache. 00861 - Raise a `NameError` if `expr` contains a name beginning with ``_``. 00862 00863 :Returns: the compiled `expr` 00864 :Exceptions: 00865 - `SyntaxError`: for compile-errors 00866 - `NameError`: if expr contains a name beginning with ``_`` 00867 """ 00868 if expr not in self._compile_cache : 00869 c = compile(expr, "", "eval") 00870 for i in c.co_names: #prevent breakout via new-style-classes 00871 if i[0] == '_': 00872 raise NameError("Name '%s' is not allowed." % i) 00873 self._compile_cache [expr] = c 00874 return self._compile_cache [expr] 00875 00876 def eval (self, expr, locals): 00877 """Eval a Python-eval-expression. 00878 00879 Sets ``self.locals_ptr`` to ``locales`` and compiles the code 00880 before evaluating. 00881 """ 00882 sav = self.locals_ptr 00883 self.locals_ptr = locals 00884 x = eval(self.compile (expr), {"__builtins__":self.eval_allowed_globals }, locals) 00885 self.locals_ptr = sav 00886 return x 00887 00888 def f_import (self, name, *_, **__): 00889 """``import``/``__import__()`` for the sandboxed code. 00890 00891 Since "import" is insecure, the PseudoSandbox does not allow to 00892 import other modules. But since some functions need to import 00893 other modules (e.g. "datetime.datetime.strftime" imports "time"), 00894 this function replaces the builtin "import" and allows to use 00895 modules which are already accessible by the sandboxed code. 00896 00897 :Note: 00898 - This probably only works for rather simple imports. 00899 - For security, it may be better to avoid such (complex) modules 00900 which import other modules. (e.g. use time.localtime and 00901 time.strftime instead of datetime.datetime.strftime, 00902 or write a small wrapper.) 00903 00904 :Example: 00905 00906 >>> from datetime import datetime 00907 >>> import pyratemp 00908 >>> t = pyratemp.Template('@!mytime.strftime("%H:%M:%S")!@') 00909 00910 # >>> print(t(mytime=datetime.now())) 00911 # Traceback (most recent call last): 00912 # ... 00913 # ImportError: import not allowed in pseudo-sandbox; try to import 'time' yourself and pass it to the sandbox/template 00914 00915 >>> import time 00916 >>> print(t(mytime=datetime.strptime("13:40:54", "%H:%M:%S"), time=time)) 00917 13:40:54 00918 00919 # >>> print(t(mytime=datetime.now(), time=time)) 00920 # 13:40:54 00921 """ 00922 import types 00923 if self.locals_ptr is not None and name in self.locals_ptr and isinstance(self.locals_ptr [name], types.ModuleType): 00924 return self.locals_ptr [name] 00925 else: 00926 raise ImportError("import not allowed in pseudo-sandbox; try to import '%s' yourself (and maybe pass it to the sandbox/template)" % name) 00927 00928 def f_exists (self, varname): 00929 """``exists()`` for the sandboxed code. 00930 00931 Test if the variable `varname` exists in the current locals-namespace. 00932 00933 This only works for single variable names. If you want to test 00934 complicated expressions, use i.e. `default`. 00935 (i.e. `default("expr",False)`) 00936 00937 :Note: the variable-name has to be quoted! (like in eval) 00938 :Example: see module-docstring 00939 """ 00940 return (varname in self.locals_ptr ) 00941 00942 def f_default (self, expr, default=None): 00943 """``default()`` for the sandboxed code. 00944 00945 Try to evaluate an expression and return the result or a 00946 fallback-/default-value; the `default`-value is used 00947 if `expr` does not exist/is invalid/results in None. 00948 00949 This is very useful for optional data. 00950 00951 :Parameter: 00952 - expr: eval-expression 00953 - default: fallback-falue if eval(expr) fails or is None. 00954 :Returns: 00955 the eval-result or the "fallback"-value. 00956 00957 :Note: the eval-expression has to be quoted! (like in eval) 00958 :Example: see module-docstring 00959 """ 00960 try: 00961 r = self.eval (expr, self.locals_ptr ) 00962 if r is None: 00963 return default 00964 return r 00965 #TODO: which exceptions should be catched here? 00966 except (NameError, LookupError, TypeError): 00967 return default 00968 00969 def f_setvar (self, name, expr): 00970 """``setvar()`` for the sandboxed code. 00971 00972 Set a variable. 00973 00974 :Example: see module-docstring 00975 """ 00976 self.locals_ptr [name] = self.eval (expr, self.locals_ptr ) 00977 return "" 00978 00979 def f_escape (self, s, format="HTML"): 00980 """``escape()`` for the sandboxed code. 00981 """ 00982 if isinstance(format, (str, unicode)): 00983 format = ESCAPE_SUPPORTED[format.upper()] 00984 return escape(unicode(s), format) 00985 00986 #----------------------------------------- 00987 # basic template / subtemplate 00988 00989 class TemplateBase : 00990 """Basic template-class. 00991 00992 Used both for the template itself and for 'macro's ("subtemplates") in 00993 the template. 00994 """ 00995 00996 def __init__ (self, parsetree, renderfunc, data=None): 00997 """Create the Template/Subtemplate/Macro. 00998 00999 :Parameters: 01000 - `parsetree`: parse-tree of the template/subtemplate/macro 01001 - `renderfunc`: render-function 01002 - `data`: data to fill into the template by default (dictionary). 01003 This data may later be overridden when rendering the template. 01004 :Exceptions: 01005 - `TypeError`: if `data` is not a dictionary 01006 """ 01007 #TODO: parameter-checking? 01008 self.parsetree = parsetree 01009 if isinstance(data, dict): 01010 self.data = data 01011 elif data is None: 01012 self.data = {} 01013 else: 01014 raise TypeError('"data" must be a dict (or None).') 01015 self.current_data = data 01016 self._render = renderfunc 01017 01018 def __call__ (self, **override): 01019 """Fill out/render the template. 01020 01021 :Parameters: 01022 - `override`: objects to add to the data-namespace, overriding 01023 the "default"-data. 01024 :Returns: the filled template (in unicode) 01025 :Note: This is also called when invoking macros 01026 (i.e. ``$!mymacro()!$``). 01027 """ 01028 self.current_data = self.data .copy() 01029 self.current_data .update(override) 01030 u = "".join(self._render (self.parsetree , self.current_data )) 01031 self.current_data = self.data # restore current_data 01032 return _dontescape(u) # (see class _dontescape) 01033 01034 def __unicode__ (self): 01035 """Alias for __call__().""" 01036 return self.__call__ () 01037 def __str__ (self): 01038 """Alias for __call__().""" 01039 return self.__call__ () 01040 01041 #----------------------------------------- 01042 # Renderer 01043 01044 class _dontescape (unicode): 01045 """Unicode-string which should not be escaped. 01046 01047 If ``isinstance(object,_dontescape)``, then don't escape the object in 01048 ``@!...!@``. It's useful for not double-escaping macros, and it's 01049 automatically used for macros/subtemplates. 01050 01051 :Note: This only works if the object is used on its own in ``@!...!@``. 01052 It i.e. does not work in ``@!object*2!@`` or ``@!object + "hi"!@``. 01053 """ 01054 __slots__ = [] 01055 01056 01057 class Renderer (object): 01058 """Render a template-parse-tree. 01059 01060 :Uses: `TemplateBase` for macros 01061 """ 01062 01063 def __init__ (self, evalfunc, escapefunc): 01064 """Init the renderer. 01065 01066 :Parameters: 01067 - `evalfunc`: function for template-expression-evaluation 01068 (i.e. ``EvalPseudoSandbox().eval``) 01069 - `escapefunc`: function for escaping special characters 01070 (i.e. `escape`) 01071 """ 01072 #TODO: test evalfunc 01073 self.evalfunc = evalfunc 01074 self.escapefunc = escapefunc 01075 01076 def _eval(self, expr, data): 01077 """evalfunc with error-messages""" 01078 try: 01079 return self.evalfunc (expr, data) 01080 #TODO: any other errors to catch here? 01081 except (TypeError,NameError,LookupError,AttributeError, SyntaxError) as err: 01082 raise TemplateRenderError("Cannot eval expression '%s'. (%s: %s)" %(expr, err.__class__.__name__, err)) 01083 01084 def render (self, parsetree, data): 01085 """Render a parse-tree of a template. 01086 01087 :Parameters: 01088 - `parsetree`: the parse-tree 01089 - `data`: the data to fill into the template (dictionary) 01090 :Returns: the rendered output-unicode-string 01091 :Exceptions: 01092 - `TemplateRenderError` 01093 """ 01094 _eval = self._eval # shortcut 01095 output = [] 01096 do_else = False # use else/elif-branch? 01097 01098 if parsetree is None: 01099 return "" 01100 for elem in parsetree: 01101 if "str" == elem[0]: 01102 output.append(elem[1]) 01103 elif "sub" == elem[0]: 01104 output.append(unicode(_eval(elem[1], data))) 01105 elif "esc" == elem[0]: 01106 obj = _eval(elem[2], data) 01107 #prevent double-escape 01108 if isinstance(obj, _dontescape) or isinstance(obj, TemplateBase): 01109 output.append(unicode(obj)) 01110 else: 01111 output.append(self.escapefunc (unicode(obj), elem[1])) 01112 elif "for" == elem[0]: 01113 do_else = True 01114 (names, iterable) = elem[1:3] 01115 try: 01116 loop_iter = iter(_eval(iterable, data)) 01117 except TypeError: 01118 raise TemplateRenderError("Cannot loop over '%s'." % iterable) 01119 for i in loop_iter: 01120 do_else = False 01121 if len(names) == 1: 01122 data[names[0]] = i 01123 else: 01124 data.update(zip(names, i)) #"for a,b,.. in list" 01125 output.extend(self.render (elem[3], data)) 01126 elif "if" == elem[0]: 01127 do_else = True 01128 if _eval(elem[1], data): 01129 do_else = False 01130 output.extend(self.render (elem[2], data)) 01131 elif "elif" == elem[0]: 01132 if do_else and _eval(elem[1], data): 01133 do_else = False 01134 output.extend(self.render (elem[2], data)) 01135 elif "else" == elem[0]: 01136 if do_else: 01137 do_else = False 01138 output.extend(self.render (elem[1], data)) 01139 elif "macro" == elem[0]: 01140 data[elem[1]] = TemplateBase(elem[2], self.render , data) 01141 else: 01142 raise TemplateRenderError("Invalid parse-tree (%s)." %(elem)) 01143 01144 return output 01145 01146 #----------------------------------------- 01147 # template user-interface (putting it all together) 01148 01149 class Template (TemplateBase ): 01150 """Template-User-Interface. 01151 01152 :Usage: 01153 :: 01154 t = Template(...) (<- see __init__) 01155 output = t(...) (<- see TemplateBase.__call__) 01156 01157 :Example: 01158 see module-docstring 01159 """ 01160 01161 def __init__ (self, string=None,filename=None,parsetree=None, encoding='utf-8', data=None, escape=HTML, 01162 loader_class=LoaderFile, 01163 parser_class=Parser, 01164 renderer_class=Renderer, 01165 eval_class=EvalPseudoSandbox, 01166 escape_func=escape): 01167 """Load (+parse) a template. 01168 01169 :Parameters: 01170 - `string,filename,parsetree`: a template-string, 01171 filename of a template to load, 01172 or a template-parsetree. 01173 (only one of these 3 is allowed) 01174 - `encoding`: encoding of the template-files (only used for "filename") 01175 - `data`: data to fill into the template by default (dictionary). 01176 This data may later be overridden when rendering the template. 01177 - `escape`: default-escaping for the template, may be overwritten by the template! 01178 - `loader_class` 01179 - `parser_class` 01180 - `renderer_class` 01181 - `eval_class` 01182 - `escapefunc` 01183 """ 01184 if [string, filename, parsetree].count(None) != 2: 01185 raise ValueError('Exactly 1 of string,filename,parsetree is necessary.') 01186 01187 tmpl = None 01188 # load template 01189 if filename is not None: 01190 incl_load = loader_class(os.path.dirname(filename), encoding).load 01191 tmpl = incl_load(os.path.basename(filename)) 01192 if string is not None: 01193 incl_load = dummy_raise(NotImplementedError, "'include' not supported for template-strings.") 01194 tmpl = LoaderString(encoding).load(string) 01195 01196 # eval (incl. compile-cache) 01197 templateeval = eval_class() 01198 01199 # parse 01200 if tmpl is not None: 01201 p = parser_class(loadfunc=incl_load, testexpr=templateeval.compile, escape=escape) 01202 parsetree = p.parse(tmpl) 01203 del p 01204 01205 # renderer 01206 renderfunc = renderer_class(templateeval.eval, escape_func).render 01207 01208 #create template 01209 TemplateBase.__init__(self, parsetree, renderfunc, data) 01210 01211 01212 #========================================= 01213 #doctest 01214 01215 def _doctest(): 01216 """doctest this module.""" 01217 import doctest 01218 doctest.testmod() 01219 01220 #---------------------- 01221 if __name__ == '__main__': 01222 if sys.version_info[0] <= 2: 01223 _doctest() 01224 01225 #========================================= 01226
Generated on Tue Jul 12 2022 17:17:33 by 1.7.2