Package spade :: Module pyratemp
[hide private]
[frames] | no frames]

Source Code for Module spade.pyratemp

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