Package web :: Package web :: Module template
[hide private]
[frames] | no frames]

Source Code for Module web.web.template

   1  """ 
   2  simple, elegant templating 
   3  (part of web.py) 
   4   
   5  Template design: 
   6   
   7  Template string is split into tokens and the tokens are combined into nodes.  
   8  Parse tree is a nodelist. TextNode and ExpressionNode are simple nodes and  
   9  for-loop, if-loop etc are block nodes, which contain multiple child nodes.  
  10   
  11  Each node can emit some python string. python string emitted by the  
  12  root node is validated for safeeval and executed using python in the given environment. 
  13   
  14  Enough care is taken to make sure the generated code and the template has line to line match,  
  15  so that the error messages can point to exact line number in template. (It doesn't work in some cases still.) 
  16   
  17  Grammar: 
  18   
  19      template -> defwith sections  
  20      defwith -> '$def with (' arguments ')' | '' 
  21      sections -> section* 
  22      section -> block | assignment | line 
  23   
  24      assignment -> '$ ' <assignment expression> 
  25      line -> (text|expr)* 
  26      text -> <any characters other than $> 
  27      expr -> '$' pyexpr | '$(' pyexpr ')' | '${' pyexpr '}' 
  28      pyexpr -> <python expression> 
  29  """ 
  30   
  31  __all__ = [ 
  32      "Template", 
  33      "Render", "render", "frender", 
  34      "ParseError", "SecurityError", 
  35      "test" 
  36  ] 
  37   
  38  import tokenize 
  39  import os 
  40  import glob 
  41  import re 
  42  from UserDict import DictMixin 
  43   
  44  from utils import storage, safeunicode, safestr, re_compile 
  45  from webapi import config 
  46  from net import websafe 
  47   
48 -def splitline(text):
49 r""" 50 Splits the given text at newline. 51 52 >>> splitline('foo\nbar') 53 ('foo\n', 'bar') 54 >>> splitline('foo') 55 ('foo', '') 56 >>> splitline('') 57 ('', '') 58 """ 59 index = text.find('\n') + 1 60 if index: 61 return text[:index], text[index:] 62 else: 63 return text, ''
64
65 -class Parser:
66 """Parser Base. 67 """
68 - def __init__(self):
69 self.statement_nodes = STATEMENT_NODES 70 self.keywords = KEYWORDS
71
72 - def parse(self, text, name="<template>"):
73 self.text = text 74 self.name = name 75 76 defwith, text = self.read_defwith(text) 77 suite = self.read_suite(text) 78 return DefwithNode(defwith, suite)
79
80 - def read_defwith(self, text):
81 if text.startswith('$def with'): 82 defwith, text = splitline(text) 83 defwith = defwith[1:].strip() # strip $ and spaces 84 return defwith, text 85 else: 86 return '', text
87
88 - def read_section(self, text):
89 r"""Reads one section from the given text. 90 91 section -> block | assignment | line 92 93 >>> read_section = Parser().read_section 94 >>> read_section('foo\nbar\n') 95 (<line: [t'foo\n']>, 'bar\n') 96 >>> read_section('$ a = b + 1\nfoo\n') 97 (<assignment: 'a = b + 1'>, 'foo\n') 98 99 read_section('$for in range(10):\n hello $i\nfoo) 100 """ 101 if text.lstrip(' ').startswith('$'): 102 index = text.index('$') 103 begin_indent, text2 = text[:index], text[index+1:] 104 ahead = self.python_lookahead(text2) 105 106 if ahead == 'var': 107 return self.read_var(text2) 108 elif ahead in self.statement_nodes: 109 return self.read_block_section(text2, begin_indent) 110 elif ahead in self.keywords: 111 return self.read_keyword(text2) 112 elif ahead.strip() == '': 113 # assignments starts with a space after $ 114 # ex: $ a = b + 2 115 return self.read_assignment(text2) 116 return self.readline(text)
117
118 - def read_var(self, text):
119 r"""Reads a var statement. 120 121 >>> read_var = Parser().read_var 122 >>> read_var('var x=10\nfoo') 123 (<var: x = 10>, 'foo') 124 >>> read_var('var x: hello $name\nfoo') 125 (<var: x = join_(u'hello ', escape_(name, True))>, 'foo') 126 """ 127 line, text = splitline(text) 128 tokens = self.python_tokens(line) 129 if len(tokens) < 4: 130 raise SyntaxError('Invalid var statement') 131 132 name = tokens[1] 133 sep = tokens[2] 134 value = line.split(sep, 1)[1].strip() 135 136 if sep == '=': 137 pass # no need to process value 138 elif sep == ':': 139 #@@ Hack for backward-compatability 140 if tokens[3] == '\n': # multi-line var statement 141 block, text = self.read_indented_block(text, ' ') 142 lines = [self.readline(x)[0] for x in block.splitlines()] 143 nodes = [] 144 for x in lines: 145 nodes.extend(x.nodes) 146 nodes.append(TextNode('\n')) 147 else: # single-line var statement 148 linenode, _ = self.readline(value) 149 nodes = linenode.nodes 150 parts = [node.emit('') for node in nodes] 151 value = "join_(%s)" % ", ".join(parts) 152 else: 153 raise SyntaxError('Invalid var statement') 154 return VarNode(name, value), text
155
156 - def read_suite(self, text):
157 r"""Reads section by section till end of text. 158 159 >>> read_suite = Parser().read_suite 160 >>> read_suite('hello $name\nfoo\n') 161 [<line: [t'hello ', $name, t'\n']>, <line: [t'foo\n']>] 162 """ 163 sections = [] 164 while text: 165 section, text = self.read_section(text) 166 sections.append(section) 167 return SuiteNode(sections)
168
169 - def readline(self, text):
170 r"""Reads one line from the text. Newline is supressed if the line ends with \. 171 172 >>> readline = Parser().readline 173 >>> readline('hello $name!\nbye!') 174 (<line: [t'hello ', $name, t'!\n']>, 'bye!') 175 >>> readline('hello $name!\\\nbye!') 176 (<line: [t'hello ', $name, t'!']>, 'bye!') 177 >>> readline('$f()\n\n') 178 (<line: [$f(), t'\n']>, '\n') 179 """ 180 line, text = splitline(text) 181 182 # supress new line if line ends with \ 183 if line.endswith('\\\n'): 184 line = line[:-2] 185 186 nodes = [] 187 while line: 188 node, line = self.read_node(line) 189 nodes.append(node) 190 191 return LineNode(nodes), text
192
193 - def read_node(self, text):
194 r"""Reads a node from the given text and returns the node and remaining text. 195 196 >>> read_node = Parser().read_node 197 >>> read_node('hello $name') 198 (t'hello ', '$name') 199 >>> read_node('$name') 200 ($name, '') 201 """ 202 if text.startswith('$$'): 203 return TextNode('$'), text[2:] 204 elif text.startswith('$#'): # comment 205 line, text = splitline(text) 206 return TextNode('\n'), text 207 elif text.startswith('$'): 208 text = text[1:] # strip $ 209 if text.startswith(':'): 210 escape = False 211 text = text[1:] # strip : 212 else: 213 escape = True 214 return self.read_expr(text, escape=escape) 215 else: 216 return self.read_text(text)
217
218 - def read_text(self, text):
219 r"""Reads a text node from the given text. 220 221 >>> read_text = Parser().read_text 222 >>> read_text('hello $name') 223 (t'hello ', '$name') 224 """ 225 index = text.find('$') 226 if index < 0: 227 return TextNode(text), '' 228 else: 229 return TextNode(text[:index]), text[index:]
230
231 - def read_keyword(self, text):
232 line, text = splitline(text) 233 return StatementNode(line.strip() + "\n"), text
234
235 - def read_expr(self, text, escape=True):
236 """Reads a python expression from the text and returns the expression and remaining text. 237 238 expr -> simple_expr | paren_expr 239 simple_expr -> id extended_expr 240 extended_expr -> attr_access | paren_expr extended_expr | '' 241 attr_access -> dot id extended_expr 242 paren_expr -> [ tokens ] | ( tokens ) | { tokens } 243 244 >>> read_expr = Parser().read_expr 245 >>> read_expr("name") 246 ($name, '') 247 >>> read_expr("a.b and c") 248 ($a.b, ' and c') 249 >>> read_expr("a. b") 250 ($a, '. b') 251 >>> read_expr("name</h1>") 252 ($name, '</h1>') 253 >>> read_expr("(limit)ing") 254 ($(limit), 'ing') 255 >>> read_expr('a[1, 2][:3].f(1+2, "weird string[).", 3 + 4) done.') 256 ($a[1, 2][:3].f(1+2, "weird string[).", 3 + 4), ' done.') 257 """ 258 def simple_expr(): 259 identifier() 260 extended_expr()
261 262 def identifier(): 263 tokens.next()
264 265 def extended_expr(): 266 lookahead = tokens.lookahead() 267 if lookahead is None: 268 return 269 elif lookahead.value == '.': 270 attr_access() 271 elif lookahead.value in parens: 272 paren_expr() 273 extended_expr() 274 else: 275 return 276 277 def attr_access(): 278 from token import NAME # python token constants 279 dot = tokens.lookahead() 280 if tokens.lookahead2().type == NAME: 281 tokens.next() # consume dot 282 identifier() 283 extended_expr() 284 285 def paren_expr(): 286 begin = tokens.next().value 287 end = parens[begin] 288 while True: 289 if tokens.lookahead().value in parens: 290 paren_expr() 291 else: 292 t = tokens.next() 293 if t.value == end: 294 break 295 return 296 297 parens = { 298 "(": ")", 299 "[": "]", 300 "{": "}" 301 } 302 303 def get_tokens(text): 304 """tokenize text using python tokenizer. 305 Python tokenizer ignores spaces, but they might be important in some cases. 306 This function introduces dummy space tokens when it identifies any ignored space. 307 Each token is a storage object containing type, value, begin and end. 308 """ 309 readline = iter([text]).next 310 end = None 311 for t in tokenize.generate_tokens(readline): 312 t = storage(type=t[0], value=t[1], begin=t[2], end=t[3]) 313 if end is not None and end != t.begin: 314 _, x1 = end 315 _, x2 = t.begin 316 yield storage(type=-1, value=text[x1:x2], begin=end, end=t.begin) 317 end = t.end 318 yield t 319 320 class BetterIter: 321 """Iterator like object with 2 support for 2 look aheads.""" 322 def __init__(self, items): 323 self.iteritems = iter(items) 324 self.items = [] 325 self.position = 0 326 self.current_item = None 327 328 def lookahead(self): 329 if len(self.items) <= self.position: 330 self.items.append(self._next()) 331 return self.items[self.position] 332 333 def _next(self): 334 try: 335 return self.iteritems.next() 336 except StopIteration: 337 return None 338 339 def lookahead2(self): 340 if len(self.items) <= self.position+1: 341 self.items.append(self._next()) 342 return self.items[self.position+1] 343 344 def next(self): 345 self.current_item = self.lookahead() 346 self.position += 1 347 return self.current_item 348 349 tokens = BetterIter(get_tokens(text)) 350 351 if tokens.lookahead().value in parens: 352 paren_expr() 353 else: 354 simple_expr() 355 row, col = tokens.current_item.end 356 return ExpressionNode(text[:col], escape=escape), text[col:] 357
358 - def read_assignment(self, text):
359 r"""Reads assignment statement from text. 360 361 >>> read_assignment = Parser().read_assignment 362 >>> read_assignment('a = b + 1\nfoo') 363 (<assignment: 'a = b + 1'>, 'foo') 364 """ 365 line, text = splitline(text) 366 return AssignmentNode(line.strip()), text
367
368 - def python_lookahead(self, text):
369 """Returns the first python token from the given text. 370 371 >>> python_lookahead = Parser().python_lookahead 372 >>> python_lookahead('for i in range(10):') 373 'for' 374 >>> python_lookahead('else:') 375 'else' 376 >>> python_lookahead(' x = 1') 377 ' ' 378 """ 379 readline = iter([text]).next 380 tokens = tokenize.generate_tokens(readline) 381 return tokens.next()[1]
382
383 - def python_tokens(self, text):
384 readline = iter([text]).next 385 tokens = tokenize.generate_tokens(readline) 386 return [t[1] for t in tokens]
387
388 - def read_indented_block(self, text, indent):
389 r"""Read a block of text. A block is what typically follows a for or it statement. 390 It can be in the same line as that of the statement or an indented block. 391 392 >>> read_indented_block = Parser().read_indented_block 393 >>> read_indented_block(' a\n b\nc', ' ') 394 ('a\nb\n', 'c') 395 >>> read_indented_block(' a\n b\n c\nd', ' ') 396 ('a\n b\nc\n', 'd') 397 >>> read_indented_block(' a\n\n b\nc', ' ') 398 ('a\n\n b\n', 'c') 399 """ 400 if indent == '': 401 return '', text 402 403 block = "" 404 while text: 405 line, text2 = splitline(text) 406 if line.strip() == "": 407 block += '\n' 408 elif line.startswith(indent): 409 block += line[len(indent):] 410 else: 411 break 412 text = text2 413 return block, text
414
415 - def read_statement(self, text):
416 r"""Reads a python statement. 417 418 >>> read_statement = Parser().read_statement 419 >>> read_statement('for i in range(10): hello $name') 420 ('for i in range(10):', ' hello $name') 421 """ 422 tok = PythonTokenizer(text) 423 tok.consume_till(':') 424 return text[:tok.index], text[tok.index:]
425
426 - def read_block_section(self, text, begin_indent=''):
427 r""" 428 >>> read_block_section = Parser().read_block_section 429 >>> read_block_section('for i in range(10): hello $i\nfoo') 430 (<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, 'foo') 431 >>> read_block_section('for i in range(10):\n hello $i\n foo', begin_indent=' ') 432 (<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, ' foo') 433 >>> read_block_section('for i in range(10):\n hello $i\nfoo') 434 (<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, 'foo') 435 """ 436 line, text = splitline(text) 437 stmt, line = self.read_statement(line) 438 keyword = self.python_lookahead(stmt) 439 440 # if there is some thing left in the line 441 if line.strip(): 442 block = line.lstrip() 443 else: 444 def find_indent(text): 445 rx = re_compile(' +') 446 match = rx.match(text) 447 first_indent = match and match.group(0) 448 return first_indent or ""
449 450 # find the indentation of the block by looking at the first line 451 first_indent = find_indent(text)[len(begin_indent):] 452 453 #TODO: fix this special case 454 if keyword == "code": 455 indent = begin_indent + first_indent 456 else: 457 indent = begin_indent + min(first_indent, INDENT) 458 459 block, text = self.read_indented_block(text, indent) 460 461 return self.create_block_node(keyword, stmt, block, begin_indent), text 462
463 - def create_block_node(self, keyword, stmt, block, begin_indent):
464 if keyword in self.statement_nodes: 465 return self.statement_nodes[keyword](stmt, block, begin_indent) 466 else: 467 raise ParseError, 'Unknown statement: %s' % repr(keyword)
468
469 -class PythonTokenizer:
470 """Utility wrapper over python tokenizer."""
471 - def __init__(self, text):
472 self.text = text 473 readline = iter([text]).next 474 self.tokens = tokenize.generate_tokens(readline) 475 self.index = 0
476
477 - def consume_till(self, delim):
478 """Consumes tokens till colon. 479 480 >>> tok = PythonTokenizer('for i in range(10): hello $i') 481 >>> tok.consume_till(':') 482 >>> tok.text[:tok.index] 483 'for i in range(10):' 484 >>> tok.text[tok.index:] 485 ' hello $i' 486 """ 487 try: 488 while True: 489 t = self.next() 490 if t.value == delim: 491 break 492 elif t.value == '(': 493 self.consume_till(')') 494 elif t.value == '[': 495 self.consume_till(']') 496 elif t.value == '{': 497 self.consume_till('}') 498 499 # if end of line is found, it is an exception. 500 # Since there is no easy way to report the line number, 501 # leave the error reporting to the python parser later 502 #@@ This should be fixed. 503 if t.value == '\n': 504 break 505 except: 506 #raise ParseError, "Expected %s, found end of line." % repr(delim) 507 508 # raising ParseError doesn't show the line number. 509 # if this error is ignored, then it will be caught when compiling the python code. 510 return
511
512 - def next(self):
513 type, t, begin, end, line = self.tokens.next() 514 row, col = end 515 self.index = col 516 return storage(type=type, value=t, begin=begin, end=end)
517
518 -class DefwithNode:
519 - def __init__(self, defwith, suite):
520 if defwith: 521 self.defwith = defwith.replace('with', '__template__') + ':' 522 # offset 3 lines. for __lineoffset__, loop and self. 523 self.defwith += "\n __lineoffset__ = -3" 524 else: 525 self.defwith = 'def __template__():' 526 # offset 4 lines for __template__, __lineoffset__, loop and self. 527 self.defwith += "\n __lineoffset__ = -4" 528 529 self.defwith += "\n loop = ForLoop()" 530 self.defwith += "\n self = TemplateResult(); extend_ = self.extend" 531 self.suite = suite 532 self.end = "\n return self"
533
534 - def emit(self, indent):
535 return self.defwith + self.suite.emit(indent + INDENT) + self.end
536
537 - def __repr__(self):
538 return "<defwith: %s, %s>" % (self.defwith, self.suite)
539
540 -class TextNode:
541 - def __init__(self, value):
542 self.value = value
543
544 - def emit(self, indent):
545 return repr(safeunicode(self.value))
546
547 - def __repr__(self):
548 return 't' + repr(self.value)
549
550 -class ExpressionNode:
551 - def __init__(self, value, escape=True):
552 self.value = value.strip() 553 554 # convert ${...} to $(...) 555 if value.startswith('{') and value.endswith('}'): 556 self.value = '(' + self.value[1:-1] + ')' 557 558 self.escape = escape
559
560 - def emit(self, indent):
561 return 'escape_(%s, %s)' % (self.value, bool(self.escape))
562
563 - def __repr__(self):
564 if self.escape: 565 escape = '' 566 else: 567 escape = ':' 568 return "$%s%s" % (escape, self.value)
569
570 -class AssignmentNode:
571 - def __init__(self, code):
572 self.code = code
573
574 - def emit(self, indent, begin_indent=''):
575 return indent + self.code + "\n"
576
577 - def __repr__(self):
578 return "<assignment: %s>" % repr(self.code)
579
580 -class LineNode:
581 - def __init__(self, nodes):
582 self.nodes = nodes
583
584 - def emit(self, indent, text_indent='', name=''):
585 text = [node.emit('') for node in self.nodes] 586 if text_indent: 587 text = [repr(text_indent)] + text 588 589 return indent + "extend_([%s])\n" % ", ".join(text)
590
591 - def __repr__(self):
592 return "<line: %s>" % repr(self.nodes)
593 594 INDENT = u' ' # 4 spaces 595
596 -class BlockNode:
597 - def __init__(self, stmt, block, begin_indent=''):
598 self.stmt = stmt 599 self.suite = Parser().read_suite(block) 600 self.begin_indent = begin_indent
601
602 - def emit(self, indent, text_indent=''):
603 text_indent = self.begin_indent + text_indent 604 out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent) 605 return out
606
607 - def __repr__(self):
608 return "<block: %s, %s>" % (repr(self.stmt), repr(self.suite))
609
610 -class ForNode(BlockNode):
611 - def __init__(self, stmt, block, begin_indent=''):
612 self.original_stmt = stmt 613 tok = PythonTokenizer(stmt) 614 tok.consume_till('in') 615 a = stmt[:tok.index] # for i in 616 b = stmt[tok.index:-1] # rest of for stmt excluding : 617 stmt = a + ' loop.setup(' + b.strip() + '):' 618 BlockNode.__init__(self, stmt, block, begin_indent)
619
620 - def __repr__(self):
621 return "<block: %s, %s>" % (repr(self.original_stmt), repr(self.suite))
622
623 -class CodeNode:
624 - def __init__(self, stmt, block, begin_indent=''):
625 # compensate one line for $code: 626 self.code = "\n" + block
627
628 - def emit(self, indent, text_indent=''):
629 import re 630 rx = re.compile('^', re.M) 631 return rx.sub(indent, self.code).rstrip(' ')
632
633 - def __repr__(self):
634 return "<code: %s>" % repr(self.code)
635
636 -class StatementNode:
637 - def __init__(self, stmt):
638 self.stmt = stmt
639
640 - def emit(self, indent):
641 return indent + self.stmt
642
643 - def __repr__(self):
644 return "<stmt: %s>" % repr(self.stmt)
645
646 -class IfNode(BlockNode):
647 pass
648
649 -class ElseNode(BlockNode):
650 pass
651
652 -class ElifNode(BlockNode):
653 pass
654
655 -class DefNode(BlockNode):
656 - def __init__(self, *a, **kw):
657 BlockNode.__init__(self, *a, **kw) 658 659 code = CodeNode("", "") 660 code.code = "self = TemplateResult(); extend_ = self.extend\n" 661 self.suite.sections.insert(0, code) 662 663 code = CodeNode("", "") 664 code.code = "return self\n" 665 self.suite.sections.append(code)
666
667 - def emit(self, indent, text_indent=''):
668 text_indent = self.begin_indent + text_indent 669 out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent) 670 return indent + "__lineoffset__ -= 3\n" + out
671
672 -class VarNode:
673 - def __init__(self, name, value):
674 self.name = name 675 self.value = value
676
677 - def emit(self, indent, text_indent):
678 return indent + "self[%s] = %s\n" % (repr(self.name), self.value)
679
680 - def __repr__(self):
681 return "<var: %s = %s>" % (self.name, self.value)
682
683 -class SuiteNode:
684 """Suite is a list of sections."""
685 - def __init__(self, sections):
686 self.sections = sections
687
688 - def emit(self, indent, text_indent=''):
689 return "\n" + "".join([s.emit(indent, text_indent) for s in self.sections])
690
691 - def __repr__(self):
692 return repr(self.sections)
693 694 STATEMENT_NODES = { 695 'for': ForNode, 696 'while': BlockNode, 697 'if': IfNode, 698 'elif': ElifNode, 699 'else': ElseNode, 700 'def': DefNode, 701 'code': CodeNode 702 } 703 704 KEYWORDS = [ 705 "pass", 706 "break", 707 "continue", 708 "return" 709 ] 710 711 TEMPLATE_BUILTIN_NAMES = [ 712 "dict", "enumerate", "float", "int", "bool", "list", "long", "reversed", 713 "set", "slice", "tuple", "xrange", 714 "abs", "all", "any", "callable", "chr", "cmp", "divmod", "filter", "hex", 715 "id", "isinstance", "iter", "len", "max", "min", "oct", "ord", "pow", "range", 716 "True", "False", 717 "None", 718 "__import__", # some c-libraries like datetime requires __import__ to present in the namespace 719 ] 720 721 import __builtin__ 722 TEMPLATE_BUILTINS = dict([(name, getattr(__builtin__, name)) for name in TEMPLATE_BUILTIN_NAMES if name in __builtin__.__dict__]) 723
724 -class ForLoop:
725 """ 726 Wrapper for expression in for stament to support loop.xxx helpers. 727 728 >>> loop = ForLoop() 729 >>> for x in loop.setup(['a', 'b', 'c']): 730 ... print loop.index, loop.revindex, loop.parity, x 731 ... 732 1 3 odd a 733 2 2 even b 734 3 1 odd c 735 >>> loop.index 736 Traceback (most recent call last): 737 ... 738 AttributeError: index 739 """
740 - def __init__(self):
741 self._ctx = None
742
743 - def __getattr__(self, name):
744 if self._ctx is None: 745 raise AttributeError, name 746 else: 747 return getattr(self._ctx, name)
748
749 - def setup(self, seq):
750 self._push() 751 return self._ctx.setup(seq)
752
753 - def _push(self):
754 self._ctx = ForLoopContext(self, self._ctx)
755
756 - def _pop(self):
757 self._ctx = self._ctx.parent
758
759 -class ForLoopContext:
760 """Stackable context for ForLoop to support nested for loops. 761 """
762 - def __init__(self, forloop, parent):
763 self._forloop = forloop 764 self.parent = parent
765
766 - def setup(self, seq):
767 try: 768 self.length = len(seq) 769 except: 770 self.length = 0 771 772 self.index = 0 773 for a in seq: 774 self.index += 1 775 yield a 776 self._forloop._pop()
777 778 index0 = property(lambda self: self.index-1) 779 first = property(lambda self: self.index == 1) 780 last = property(lambda self: self.index == self.length) 781 odd = property(lambda self: self.index % 2 == 1) 782 even = property(lambda self: self.index % 2 == 0) 783 parity = property(lambda self: ['odd', 'even'][self.even]) 784 revindex0 = property(lambda self: self.length - self.index) 785 revindex = property(lambda self: self.length - self.index + 1)
786
787 -class BaseTemplate:
788 - def __init__(self, code, filename, filter, globals, builtins):
789 self.filename = filename 790 self.filter = filter 791 self._globals = globals 792 self._builtins = builtins 793 if code: 794 self.t = self._compile(code) 795 else: 796 self.t = lambda: ''
797
798 - def _compile(self, code):
799 env = self.make_env(self._globals or {}, self._builtins) 800 exec(code, env) 801 return env['__template__']
802
803 - def __call__(self, *a, **kw):
804 __hidetraceback__ = True 805 return self.t(*a, **kw)
806
807 - def make_env(self, globals, builtins):
808 return dict(globals, 809 __builtins__=builtins, 810 ForLoop=ForLoop, 811 TemplateResult=TemplateResult, 812 escape_=self._escape, 813 join_=self._join 814 )
815 - def _join(self, *items):
816 return u"".join(items)
817
818 - def _escape(self, value, escape=False):
819 if value is None: 820 value = '' 821 822 value = safeunicode(value) 823 if escape and self.filter: 824 value = self.filter(value) 825 return value
826 827 _htmlquote_re = re.compile(r'[&<>"\']') 828 _htmlquote_d = { 829 u"&": u"&amp;", 830 u"<": u"&lt;", 831 u">": u"&gt;", 832 u"'": u"&#39;", 833 u'"': u"&quot;", 834 } 835
836 -def websafe(text):
837 r""" 838 Encodes `text` for raw use in HTML. 839 840 >>> websafe(u"<'&\">") 841 u'&lt;&#39;&amp;&quot;&gt;' 842 843 Unlike the websafe function in utils.py, this works with unicode text. 844 """ 845 return _htmlquote_re.sub(lambda m: _htmlquote_d[m.group(0)], text)
846 847
848 -class Template(BaseTemplate):
849 CONTENT_TYPES = { 850 '.html' : 'text/html; charset=utf-8', 851 '.xhtml' : 'application/xhtml+xml; charset=utf-8', 852 '.txt' : 'text/plain', 853 } 854 FILTERS = { 855 '.html': websafe, 856 '.xhtml': websafe, 857 '.xml': websafe 858 } 859 globals = {} 860
861 - def __init__(self, text, filename='<template>', filter=None, globals=None, builtins=None, extensions=None):
862 self.extensions = extensions or [] 863 text = Template.normalize_text(text) 864 code = self.compile_template(text, filename) 865 866 _, ext = os.path.splitext(filename) 867 filter = filter or self.FILTERS.get(ext, None) 868 self.content_type = self.CONTENT_TYPES.get(ext, None) 869 870 if globals is None: 871 globals = self.globals 872 if builtins is None: 873 builtins = TEMPLATE_BUILTINS 874 875 BaseTemplate.__init__(self, code=code, filename=filename, filter=filter, globals=globals, builtins=builtins)
876
877 - def normalize_text(text):
878 """Normalizes template text by correcting \r\n, tabs and BOM chars.""" 879 text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs() 880 if not text.endswith('\n'): 881 text += '\n' 882 883 # ignore BOM chars at the begining of template 884 BOM = '\xef\xbb\xbf' 885 if isinstance(text, str) and text.startswith(BOM): 886 text = text[len(BOM):] 887 888 # support fort \$ for backward-compatibility 889 text = text.replace(r'\$', '$$') 890 return text
891 normalize_text = staticmethod(normalize_text) 892
893 - def __call__(self, *a, **kw):
894 __hidetraceback__ = True 895 import webapi as web 896 if 'headers' in web.ctx and self.content_type: 897 web.header('Content-Type', self.content_type, unique=True) 898 899 return BaseTemplate.__call__(self, *a, **kw)
900
901 - def generate_code(text, filename, parser=None):
902 # parse the text 903 parser = parser or Parser() 904 rootnode = parser.parse(text, filename) 905 906 # generate python code from the parse tree 907 code = rootnode.emit(indent="").strip() 908 return safestr(code)
909 910 generate_code = staticmethod(generate_code) 911
912 - def create_parser(self):
913 p = Parser() 914 for ext in self.extensions: 915 p = ext(p) 916 return p
917
918 - def compile_template(self, template_string, filename):
919 code = Template.generate_code(template_string, filename, parser=self.create_parser()) 920 921 def get_source_line(filename, lineno): 922 try: 923 lines = open(filename).read().splitlines() 924 return lines[lineno] 925 except: 926 return None
927 928 try: 929 # compile the code first to report the errors, if any, with the filename 930 compiled_code = compile(code, filename, 'exec') 931 except SyntaxError, e: 932 # display template line that caused the error along with the traceback. 933 try: 934 e.msg += '\n\nTemplate traceback:\n File %s, line %s\n %s' % \ 935 (repr(e.filename), e.lineno, get_source_line(e.filename, e.lineno-1)) 936 except: 937 pass 938 raise 939 940 # make sure code is safe 941 import compiler 942 ast = compiler.parse(code) 943 SafeVisitor().walk(ast, filename) 944 945 return compiled_code
946
947 -class CompiledTemplate(Template):
948 - def __init__(self, f, filename):
949 Template.__init__(self, '', filename) 950 self.t = f
951
952 - def compile_template(self, *a):
953 return None
954
955 - def _compile(self, *a):
956 return None
957
958 -class Render:
959 """The most preferred way of using templates. 960 961 render = web.template.render('templates') 962 print render.foo() 963 964 Optional parameter can be `base` can be used to pass output of 965 every template through the base template. 966 967 render = web.template.render('templates', base='layout') 968 """
969 - def __init__(self, loc='templates', cache=None, base=None, **keywords):
970 self._loc = loc 971 self._keywords = keywords 972 973 if cache is None: 974 cache = not config.get('debug', False) 975 976 if cache: 977 self._cache = {} 978 else: 979 self._cache = None 980 981 if base and not hasattr(base, '__call__'): 982 # make base a function, so that it can be passed to sub-renders 983 self._base = lambda page: self._template(base)(page) 984 else: 985 self._base = base
986
987 - def _add_global(self, obj, name=None):
988 """Add a global to this rendering instance.""" 989 if 'globals' not in self._keywords: self._keywords['globals'] = {} 990 if not name: 991 name = obj.__name__ 992 self._keywords['globals'][name] = obj
993
994 - def _lookup(self, name):
995 path = os.path.join(self._loc, name) 996 if os.path.isdir(path): 997 return 'dir', path 998 else: 999 path = self._findfile(path) 1000 if path: 1001 return 'file', path 1002 else: 1003 return 'none', None
1004
1005 - def _load_template(self, name):
1006 kind, path = self._lookup(name) 1007 1008 if kind == 'dir': 1009 return Render(path, cache=self._cache is not None, base=self._base, **self._keywords) 1010 elif kind == 'file': 1011 return Template(open(path).read(), filename=path, **self._keywords) 1012 else: 1013 raise AttributeError, "No template named " + name
1014
1015 - def _findfile(self, path_prefix):
1016 p = [f for f in glob.glob(path_prefix + '.*') if not f.endswith('~')] # skip backup files 1017 p.sort() # sort the matches for deterministic order 1018 return p and p[0]
1019
1020 - def _template(self, name):
1021 if self._cache is not None: 1022 if name not in self._cache: 1023 self._cache[name] = self._load_template(name) 1024 return self._cache[name] 1025 else: 1026 return self._load_template(name)
1027
1028 - def __getattr__(self, name):
1029 t = self._template(name) 1030 if self._base and isinstance(t, Template): 1031 def template(*a, **kw): 1032 return self._base(t(*a, **kw))
1033 return template 1034 else: 1035 return self._template(name) 1036
1037 -class GAE_Render(Render):
1038 # Render gets over-written. make a copy here. 1039 super = Render
1040 - def __init__(self, loc, *a, **kw):
1041 GAE_Render.super.__init__(self, loc, *a, **kw) 1042 1043 import types 1044 if isinstance(loc, types.ModuleType): 1045 self.mod = loc 1046 else: 1047 name = loc.rstrip('/').replace('/', '.') 1048 self.mod = __import__(name, None, None, ['x']) 1049 1050 self.mod.__dict__.update(kw.get('builtins', TEMPLATE_BUILTINS)) 1051 self.mod.__dict__.update(Template.globals) 1052 self.mod.__dict__.update(kw.get('globals', {}))
1053
1054 - def _load_template(self, name):
1055 t = getattr(self.mod, name) 1056 import types 1057 if isinstance(t, types.ModuleType): 1058 return GAE_Render(t, cache=self._cache is not None, base=self._base, **self._keywords) 1059 else: 1060 return t
1061 1062 render = Render 1063 # setup render for Google App Engine. 1064 try: 1065 from google import appengine 1066 render = Render = GAE_Render 1067 except ImportError: 1068 pass 1069
1070 -def frender(path, **keywords):
1071 """Creates a template from the given file path. 1072 """ 1073 return Template(open(path).read(), filename=path, **keywords)
1074
1075 -def compile_templates(root):
1076 """Compiles templates to python code.""" 1077 re_start = re_compile('^', re.M) 1078 1079 for dirpath, dirnames, filenames in os.walk(root): 1080 filenames = [f for f in filenames if not f.startswith('.') and not f.endswith('~') and not f.startswith('__init__.py')] 1081 1082 for d in dirnames[:]: 1083 if d.startswith('.'): 1084 dirnames.remove(d) # don't visit this dir 1085 1086 out = open(os.path.join(dirpath, '__init__.py'), 'w') 1087 out.write('from web.template import CompiledTemplate, ForLoop, TemplateResult\n\n') 1088 if dirnames: 1089 out.write("import " + ", ".join(dirnames)) 1090 1091 out.write("_dummy = CompiledTemplate(lambda: None, 'dummy')\n") 1092 out.write("join_ = _dummy._join\n") 1093 out.write("escape_ = _dummy._escape\n") 1094 out.write("\n") 1095 1096 for f in filenames: 1097 path = os.path.join(dirpath, f) 1098 1099 if '.' in f: 1100 name, _ = f.split('.', 1) 1101 else: 1102 name = f 1103 1104 text = open(path).read() 1105 text = Template.normalize_text(text) 1106 code = Template.generate_code(text, path) 1107 1108 code = code.replace("__template__", name, 1) 1109 1110 out.write(code) 1111 1112 out.write('\n\n') 1113 out.write('%s = CompiledTemplate(%s, %s)\n\n' % (name, name, repr(path))) 1114 1115 # create template to make sure it compiles 1116 t = Template(open(path).read(), path) 1117 out.close()
1118
1119 -class ParseError(Exception):
1120 pass
1121
1122 -class SecurityError(Exception):
1123 """The template seems to be trying to do something naughty.""" 1124 pass
1125 1126 # Enumerate all the allowed AST nodes 1127 ALLOWED_AST_NODES = [ 1128 "Add", "And", 1129 # "AssAttr", 1130 "AssList", "AssName", "AssTuple", 1131 # "Assert", 1132 "Assign", "AugAssign", 1133 # "Backquote", 1134 "Bitand", "Bitor", "Bitxor", "Break", 1135 "CallFunc","Class", "Compare", "Const", "Continue", 1136 "Decorators", "Dict", "Discard", "Div", 1137 "Ellipsis", "EmptyNode", 1138 # "Exec", 1139 "Expression", "FloorDiv", "For", 1140 # "From", 1141 "Function", 1142 "GenExpr", "GenExprFor", "GenExprIf", "GenExprInner", 1143 "Getattr", 1144 # "Global", 1145 "If", "IfExp", 1146 # "Import", 1147 "Invert", "Keyword", "Lambda", "LeftShift", 1148 "List", "ListComp", "ListCompFor", "ListCompIf", "Mod", 1149 "Module", 1150 "Mul", "Name", "Not", "Or", "Pass", "Power", 1151 # "Print", "Printnl", "Raise", 1152 "Return", "RightShift", "Slice", "Sliceobj", 1153 "Stmt", "Sub", "Subscript", 1154 # "TryExcept", "TryFinally", 1155 "Tuple", "UnaryAdd", "UnarySub", 1156 "While", "With", "Yield", 1157 ] 1158
1159 -class SafeVisitor(object):
1160 """ 1161 Make sure code is safe by walking through the AST. 1162 1163 Code considered unsafe if: 1164 * it has restricted AST nodes 1165 * it is trying to access resricted attributes 1166 1167 Adopted from http://www.zafar.se/bkz/uploads/safe.txt (public domain, Babar K. Zafar) 1168 """
1169 - def __init__(self):
1170 "Initialize visitor by generating callbacks for all AST node types." 1171 self.errors = []
1172
1173 - def walk(self, ast, filename):
1174 "Validate each node in AST and raise SecurityError if the code is not safe." 1175 self.filename = filename 1176 self.visit(ast) 1177 1178 if self.errors: 1179 raise SecurityError, '\n'.join([str(err) for err in self.errors])
1180
1181 - def visit(self, node, *args):
1182 "Recursively validate node and all of its children." 1183 def classname(obj): 1184 return obj.__class__.__name__
1185 nodename = classname(node) 1186 fn = getattr(self, 'visit' + nodename, None) 1187 1188 if fn: 1189 fn(node, *args) 1190 else: 1191 if nodename not in ALLOWED_AST_NODES: 1192 self.fail(node, *args) 1193 1194 for child in node.getChildNodes(): 1195 self.visit(child, *args)
1196
1197 - def visitName(self, node, *args):
1198 "Disallow any attempts to access a restricted attr." 1199 #self.assert_attr(node.getChildren()[0], node) 1200 pass
1201
1202 - def visitGetattr(self, node, *args):
1203 "Disallow any attempts to access a restricted attribute." 1204 self.assert_attr(node.attrname, node)
1205
1206 - def assert_attr(self, attrname, node):
1207 if self.is_unallowed_attr(attrname): 1208 lineno = self.get_node_lineno(node) 1209 e = SecurityError("%s:%d - access to attribute '%s' is denied" % (self.filename, lineno, attrname)) 1210 self.errors.append(e)
1211
1212 - def is_unallowed_attr(self, name):
1213 return name.startswith('_') \ 1214 or name.startswith('func_') \ 1215 or name.startswith('im_')
1216
1217 - def get_node_lineno(self, node):
1218 return (node.lineno) and node.lineno or 0
1219
1220 - def fail(self, node, *args):
1221 "Default callback for unallowed AST nodes." 1222 lineno = self.get_node_lineno(node) 1223 nodename = node.__class__.__name__ 1224 e = SecurityError("%s:%d - execution of '%s' statements is denied" % (self.filename, lineno, nodename)) 1225 self.errors.append(e)
1226
1227 -class TemplateResult(storage, DictMixin):
1228 """Dictionary like object for storing template output. 1229 1230 A template can specify key-value pairs in the output using 1231 `var` statements. Each `var` statement adds a new key to the 1232 template output and the main output is stored with key 1233 __body__. 1234 1235 >>> d = TemplateResult(__body__='hello, world', x='foo') 1236 >>> d 1237 <TemplateResult: {'__body__': 'hello, world', 'x': 'foo'}> 1238 >>> print d 1239 hello, world 1240 >>> d = TemplateResult() 1241 >>> d.extend([u'hello', u'world']) 1242 >>> d 1243 <TemplateResult: {'__body__': u'helloworld'}> 1244 """
1245 - def __init__(self, *a, **kw):
1246 storage.__init__(self, *a, **kw) 1247 self.setdefault("__body__", None) 1248 1249 # avoiding self._data because it adds as item instead of attr. 1250 self.__dict__["_data"] = [] 1251 self.__dict__["extend"] = self._data.extend
1252
1253 - def __getitem__(self, name):
1254 if name == "__body__" and storage.__getitem__(self, '__body__') is None: 1255 self["__body__"] = u"".join(self._data) 1256 return storage.__getitem__(self, name)
1257
1258 - def __unicode__(self):
1259 return self["__body__"]
1260
1261 - def __str__(self):
1262 return self["__body__"].encode('utf-8')
1263
1264 - def __repr__(self):
1265 self["__body__"] # initialize __body__ if not already initialized 1266 return "<TemplateResult: %s>" % dict.__repr__(self)
1267
1268 -def test():
1269 r"""Doctest for testing template module. 1270 1271 Define a utility function to run template test. 1272 1273 >>> class TestResult: 1274 ... def __init__(self, t): self.t = t 1275 ... def __getattr__(self, name): return getattr(self.t, name) 1276 ... def __repr__(self): return repr(unicode(self)) 1277 ... 1278 >>> def t(code, **keywords): 1279 ... tmpl = Template(code, **keywords) 1280 ... return lambda *a, **kw: TestResult(tmpl(*a, **kw)) 1281 ... 1282 1283 Simple tests. 1284 1285 >>> t('1')() 1286 u'1\n' 1287 >>> t('$def with ()\n1')() 1288 u'1\n' 1289 >>> t('$def with (a)\n$a')(1) 1290 u'1\n' 1291 >>> t('$def with (a=0)\n$a')(1) 1292 u'1\n' 1293 >>> t('$def with (a=0)\n$a')(a=1) 1294 u'1\n' 1295 1296 Test complicated expressions. 1297 1298 >>> t('$def with (x)\n$x.upper()')('hello') 1299 u'HELLO\n' 1300 >>> t('$(2 * 3 + 4 * 5)')() 1301 u'26\n' 1302 >>> t('${2 * 3 + 4 * 5}')() 1303 u'26\n' 1304 >>> t('$def with (limit)\nkeep $(limit)ing.')('go') 1305 u'keep going.\n' 1306 >>> t('$def with (a)\n$a.b[0]')(storage(b=[1])) 1307 u'1\n' 1308 1309 Test html escaping. 1310 1311 >>> t('$def with (x)\n$x', filename='a.html')('<html>') 1312 u'&lt;html&gt;\n' 1313 >>> t('$def with (x)\n$x', filename='a.txt')('<html>') 1314 u'<html>\n' 1315 1316 Test if, for and while. 1317 1318 >>> t('$if 1: 1')() 1319 u'1\n' 1320 >>> t('$if 1:\n 1')() 1321 u'1\n' 1322 >>> t('$if 1:\n 1\\')() 1323 u'1' 1324 >>> t('$if 0: 0\n$elif 1: 1')() 1325 u'1\n' 1326 >>> t('$if 0: 0\n$elif None: 0\n$else: 1')() 1327 u'1\n' 1328 >>> t('$if 0 < 1 and 1 < 2: 1')() 1329 u'1\n' 1330 >>> t('$for x in [1, 2, 3]: $x')() 1331 u'1\n2\n3\n' 1332 >>> t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1}) 1333 u'1\n' 1334 >>> t('$for x in [1, 2, 3]:\n\t$x')() 1335 u' 1\n 2\n 3\n' 1336 >>> t('$def with (a)\n$while a and a.pop():1')([1, 2, 3]) 1337 u'1\n1\n1\n' 1338 1339 The space after : must be ignored. 1340 1341 >>> t('$if True: foo')() 1342 u'foo\n' 1343 1344 Test loop.xxx. 1345 1346 >>> t("$for i in range(5):$loop.index, $loop.parity")() 1347 u'1, odd\n2, even\n3, odd\n4, even\n5, odd\n' 1348 >>> t("$for i in range(2):\n $for j in range(2):$loop.parent.parity $loop.parity")() 1349 u'odd odd\nodd even\neven odd\neven even\n' 1350 1351 Test assignment. 1352 1353 >>> t('$ a = 1\n$a')() 1354 u'1\n' 1355 >>> t('$ a = [1]\n$a[0]')() 1356 u'1\n' 1357 >>> t('$ a = {1: 1}\n$a.keys()[0]')() 1358 u'1\n' 1359 >>> t('$ a = []\n$if not a: 1')() 1360 u'1\n' 1361 >>> t('$ a = {}\n$if not a: 1')() 1362 u'1\n' 1363 >>> t('$ a = -1\n$a')() 1364 u'-1\n' 1365 >>> t('$ a = "1"\n$a')() 1366 u'1\n' 1367 1368 Test comments. 1369 1370 >>> t('$# 0')() 1371 u'\n' 1372 >>> t('hello$#comment1\nhello$#comment2')() 1373 u'hello\nhello\n' 1374 >>> t('$#comment0\nhello$#comment1\nhello$#comment2')() 1375 u'\nhello\nhello\n' 1376 1377 Test unicode. 1378 1379 >>> t('$def with (a)\n$a')(u'\u203d') 1380 u'\u203d\n' 1381 >>> t('$def with (a)\n$a')(u'\u203d'.encode('utf-8')) 1382 u'\u203d\n' 1383 >>> t(u'$def with (a)\n$a $:a')(u'\u203d') 1384 u'\u203d \u203d\n' 1385 >>> t(u'$def with ()\nfoo')() 1386 u'foo\n' 1387 >>> def f(x): return x 1388 ... 1389 >>> t(u'$def with (f)\n$:f("x")')(f) 1390 u'x\n' 1391 >>> t('$def with (f)\n$:f("x")')(f) 1392 u'x\n' 1393 1394 Test dollar escaping. 1395 1396 >>> t("Stop, $$money isn't evaluated.")() 1397 u"Stop, $money isn't evaluated.\n" 1398 >>> t("Stop, \$money isn't evaluated.")() 1399 u"Stop, $money isn't evaluated.\n" 1400 1401 Test space sensitivity. 1402 1403 >>> t('$def with (x)\n$x')(1) 1404 u'1\n' 1405 >>> t('$def with(x ,y)\n$x')(1, 1) 1406 u'1\n' 1407 >>> t('$(1 + 2*3 + 4)')() 1408 u'11\n' 1409 1410 Make sure globals are working. 1411 1412 >>> t('$x')() 1413 Traceback (most recent call last): 1414 ... 1415 NameError: global name 'x' is not defined 1416 >>> t('$x', globals={'x': 1})() 1417 u'1\n' 1418 1419 Can't change globals. 1420 1421 >>> t('$ x = 2\n$x', globals={'x': 1})() 1422 u'2\n' 1423 >>> t('$ x = x + 1\n$x', globals={'x': 1})() 1424 Traceback (most recent call last): 1425 ... 1426 UnboundLocalError: local variable 'x' referenced before assignment 1427 1428 Make sure builtins are customizable. 1429 1430 >>> t('$min(1, 2)')() 1431 u'1\n' 1432 >>> t('$min(1, 2)', builtins={})() 1433 Traceback (most recent call last): 1434 ... 1435 NameError: global name 'min' is not defined 1436 1437 Test vars. 1438 1439 >>> x = t('$var x: 1')() 1440 >>> x.x 1441 u'1' 1442 >>> x = t('$var x = 1')() 1443 >>> x.x 1444 1 1445 >>> x = t('$var x: \n foo\n bar')() 1446 >>> x.x 1447 u'foo\nbar\n' 1448 1449 Test BOM chars. 1450 1451 >>> t('\xef\xbb\xbf$def with(x)\n$x')('foo') 1452 u'foo\n' 1453 1454 Test for with weird cases. 1455 1456 >>> t('$for i in range(10)[1:5]:\n $i')() 1457 u'1\n2\n3\n4\n' 1458 >>> t("$for k, v in {'a': 1, 'b': 2}.items():\n $k $v")() 1459 u'a 1\nb 2\n' 1460 >>> t("$for k, v in ({'a': 1, 'b': 2}.items():\n $k $v")() 1461 Traceback (most recent call last): 1462 ... 1463 SyntaxError: invalid syntax 1464 1465 Test datetime. 1466 1467 >>> import datetime 1468 >>> t("$def with (date)\n$date.strftime('%m %Y')")(datetime.datetime(2009, 1, 1)) 1469 u'01 2009\n' 1470 """ 1471 pass
1472 1473 if __name__ == "__main__": 1474 import sys 1475 if '--compile' in sys.argv: 1476 compile_templates(sys.argv[2]) 1477 else: 1478 import doctest 1479 doctest.testmod() 1480