2 simple, elegant templating
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.
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.
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.)
19 template -> defwith sections
20 defwith -> '$def with (' arguments ')' | ''
22 section -> block | assignment | line
24 assignment -> '$ ' <assignment expression>
26 text -> <any characters other than $>
27 expr -> '$' pyexpr | '$(' pyexpr ')' | '${' pyexpr '}'
28 pyexpr -> <python expression>
33 "Render", "render", "frender",
34 "ParseError", "SecurityError",
43 from UserDict import DictMixin
46 from utils import storage, safeunicode, safestr, re_compile
47 from webapi import config
48 from net import websafe
52 Splits the given text at newline.
54 >>> splitline('foo\nbar')
61 index = text.find('\n') + 1
63 return text[:index], text[index:]
71 self.statement_nodes = STATEMENT_NODES
72 self.keywords = KEYWORDS
74 def parse(self, text, name="<template>"):
78 defwith, text = self.read_defwith(text)
79 suite = self.read_suite(text)
80 return DefwithNode(defwith, suite)
82 def read_defwith(self, text):
83 if text.startswith('$def with'):
84 defwith, text = splitline(text)
85 defwith = defwith[1:].strip() # strip $ and spaces
90 def read_section(self, text):
91 r"""Reads one section from the given text.
93 section -> block | assignment | line
95 >>> read_section = Parser().read_section
96 >>> read_section('foo\nbar\n')
97 (<line: [t'foo\n']>, 'bar\n')
98 >>> read_section('$ a = b + 1\nfoo\n')
99 (<assignment: 'a = b + 1'>, 'foo\n')
101 read_section('$for in range(10):\n hello $i\nfoo)
103 if text.lstrip(' ').startswith('$'):
104 index = text.index('$')
105 begin_indent, text2 = text[:index], text[index+1:]
106 ahead = self.python_lookahead(text2)
109 return self.read_var(text2)
110 elif ahead in self.statement_nodes:
111 return self.read_block_section(text2, begin_indent)
112 elif ahead in self.keywords:
113 return self.read_keyword(text2)
114 elif ahead.strip() == '':
115 # assignments starts with a space after $
117 return self.read_assignment(text2)
118 return self.readline(text)
120 def read_var(self, text):
121 r"""Reads a var statement.
123 >>> read_var = Parser().read_var
124 >>> read_var('var x=10\nfoo')
125 (<var: x = 10>, 'foo')
126 >>> read_var('var x: hello $name\nfoo')
127 (<var: x = join_(u'hello ', escape_(name, True))>, 'foo')
129 line, text = splitline(text)
130 tokens = self.python_tokens(line)
132 raise SyntaxError('Invalid var statement')
136 value = line.split(sep, 1)[1].strip()
139 pass # no need to process value
141 #@@ Hack for backward-compatability
142 if tokens[3] == '\n': # multi-line var statement
143 block, text = self.read_indented_block(text, ' ')
144 lines = [self.readline(x)[0] for x in block.splitlines()]
147 nodes.extend(x.nodes)
148 nodes.append(TextNode('\n'))
149 else: # single-line var statement
150 linenode, _ = self.readline(value)
151 nodes = linenode.nodes
152 parts = [node.emit('') for node in nodes]
153 value = "join_(%s)" % ", ".join(parts)
155 raise SyntaxError('Invalid var statement')
156 return VarNode(name, value), text
158 def read_suite(self, text):
159 r"""Reads section by section till end of text.
161 >>> read_suite = Parser().read_suite
162 >>> read_suite('hello $name\nfoo\n')
163 [<line: [t'hello ', $name, t'\n']>, <line: [t'foo\n']>]
167 section, text = self.read_section(text)
168 sections.append(section)
169 return SuiteNode(sections)
171 def readline(self, text):
172 r"""Reads one line from the text. Newline is supressed if the line ends with \.
174 >>> readline = Parser().readline
175 >>> readline('hello $name!\nbye!')
176 (<line: [t'hello ', $name, t'!\n']>, 'bye!')
177 >>> readline('hello $name!\\\nbye!')
178 (<line: [t'hello ', $name, t'!']>, 'bye!')
179 >>> readline('$f()\n\n')
180 (<line: [$f(), t'\n']>, '\n')
182 line, text = splitline(text)
184 # supress new line if line ends with \
185 if line.endswith('\\\n'):
190 node, line = self.read_node(line)
193 return LineNode(nodes), text
195 def read_node(self, text):
196 r"""Reads a node from the given text and returns the node and remaining text.
198 >>> read_node = Parser().read_node
199 >>> read_node('hello $name')
201 >>> read_node('$name')
204 if text.startswith('$$'):
205 return TextNode('$'), text[2:]
206 elif text.startswith('$#'): # comment
207 line, text = splitline(text)
208 return TextNode('\n'), text
209 elif text.startswith('$'):
210 text = text[1:] # strip $
211 if text.startswith(':'):
213 text = text[1:] # strip :
216 return self.read_expr(text, escape=escape)
218 return self.read_text(text)
220 def read_text(self, text):
221 r"""Reads a text node from the given text.
223 >>> read_text = Parser().read_text
224 >>> read_text('hello $name')
227 index = text.find('$')
229 return TextNode(text), ''
231 return TextNode(text[:index]), text[index:]
233 def read_keyword(self, text):
234 line, text = splitline(text)
235 return StatementNode(line.strip() + "\n"), text
237 def read_expr(self, text, escape=True):
238 """Reads a python expression from the text and returns the expression and remaining text.
240 expr -> simple_expr | paren_expr
241 simple_expr -> id extended_expr
242 extended_expr -> attr_access | paren_expr extended_expr | ''
243 attr_access -> dot id extended_expr
244 paren_expr -> [ tokens ] | ( tokens ) | { tokens }
246 >>> read_expr = Parser().read_expr
247 >>> read_expr("name")
249 >>> read_expr("a.b and c")
251 >>> read_expr("a. b")
253 >>> read_expr("name</h1>")
255 >>> read_expr("(limit)ing")
257 >>> read_expr('a[1, 2][:3].f(1+2, "weird string[).", 3 + 4) done.')
258 ($a[1, 2][:3].f(1+2, "weird string[).", 3 + 4), ' done.')
268 lookahead = tokens.lookahead()
269 if lookahead is None:
271 elif lookahead.value == '.':
273 elif lookahead.value in parens:
280 from token import NAME # python token constants
281 dot = tokens.lookahead()
282 if tokens.lookahead2().type == NAME:
283 tokens.next() # consume dot
288 begin = tokens.next().value
291 if tokens.lookahead().value in parens:
305 def get_tokens(text):
306 """tokenize text using python tokenizer.
307 Python tokenizer ignores spaces, but they might be important in some cases.
308 This function introduces dummy space tokens when it identifies any ignored space.
309 Each token is a storage object containing type, value, begin and end.
311 readline = iter([text]).next
313 for t in tokenize.generate_tokens(readline):
314 t = storage(type=t[0], value=t[1], begin=t[2], end=t[3])
315 if end is not None and end != t.begin:
318 yield storage(type=-1, value=text[x1:x2], begin=end, end=t.begin)
323 """Iterator like object with 2 support for 2 look aheads."""
324 def __init__(self, items):
325 self.iteritems = iter(items)
328 self.current_item = None
331 if len(self.items) <= self.position:
332 self.items.append(self._next())
333 return self.items[self.position]
337 return self.iteritems.next()
338 except StopIteration:
341 def lookahead2(self):
342 if len(self.items) <= self.position+1:
343 self.items.append(self._next())
344 return self.items[self.position+1]
347 self.current_item = self.lookahead()
349 return self.current_item
351 tokens = BetterIter(get_tokens(text))
353 if tokens.lookahead().value in parens:
357 row, col = tokens.current_item.end
358 return ExpressionNode(text[:col], escape=escape), text[col:]
360 def read_assignment(self, text):
361 r"""Reads assignment statement from text.
363 >>> read_assignment = Parser().read_assignment
364 >>> read_assignment('a = b + 1\nfoo')
365 (<assignment: 'a = b + 1'>, 'foo')
367 line, text = splitline(text)
368 return AssignmentNode(line.strip()), text
370 def python_lookahead(self, text):
371 """Returns the first python token from the given text.
373 >>> python_lookahead = Parser().python_lookahead
374 >>> python_lookahead('for i in range(10):')
376 >>> python_lookahead('else:')
378 >>> python_lookahead(' x = 1')
381 readline = iter([text]).next
382 tokens = tokenize.generate_tokens(readline)
383 return tokens.next()[1]
385 def python_tokens(self, text):
386 readline = iter([text]).next
387 tokens = tokenize.generate_tokens(readline)
388 return [t[1] for t in tokens]
390 def read_indented_block(self, text, indent):
391 r"""Read a block of text. A block is what typically follows a for or it statement.
392 It can be in the same line as that of the statement or an indented block.
394 >>> read_indented_block = Parser().read_indented_block
395 >>> read_indented_block(' a\n b\nc', ' ')
397 >>> read_indented_block(' a\n b\n c\nd', ' ')
399 >>> read_indented_block(' a\n\n b\nc', ' ')
407 line, text2 = splitline(text)
408 if line.strip() == "":
410 elif line.startswith(indent):
411 block += line[len(indent):]
417 def read_statement(self, text):
418 r"""Reads a python statement.
420 >>> read_statement = Parser().read_statement
421 >>> read_statement('for i in range(10): hello $name')
422 ('for i in range(10):', ' hello $name')
424 tok = PythonTokenizer(text)
425 tok.consume_till(':')
426 return text[:tok.index], text[tok.index:]
428 def read_block_section(self, text, begin_indent=''):
430 >>> read_block_section = Parser().read_block_section
431 >>> read_block_section('for i in range(10): hello $i\nfoo')
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\n foo', begin_indent=' ')
434 (<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, ' foo')
435 >>> read_block_section('for i in range(10):\n hello $i\nfoo')
436 (<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, 'foo')
438 line, text = splitline(text)
439 stmt, line = self.read_statement(line)
440 keyword = self.python_lookahead(stmt)
442 # if there is some thing left in the line
444 block = line.lstrip()
446 def find_indent(text):
447 rx = re_compile(' +')
448 match = rx.match(text)
449 first_indent = match and match.group(0)
450 return first_indent or ""
452 # find the indentation of the block by looking at the first line
453 first_indent = find_indent(text)[len(begin_indent):]
455 #TODO: fix this special case
456 if keyword == "code":
457 indent = begin_indent + first_indent
459 indent = begin_indent + min(first_indent, INDENT)
461 block, text = self.read_indented_block(text, indent)
463 return self.create_block_node(keyword, stmt, block, begin_indent), text
465 def create_block_node(self, keyword, stmt, block, begin_indent):
466 if keyword in self.statement_nodes:
467 return self.statement_nodes[keyword](stmt, block, begin_indent)
469 raise ParseError, 'Unknown statement: %s' % repr(keyword)
471 class PythonTokenizer:
472 """Utility wrapper over python tokenizer."""
473 def __init__(self, text):
475 readline = iter([text]).next
476 self.tokens = tokenize.generate_tokens(readline)
479 def consume_till(self, delim):
480 """Consumes tokens till colon.
482 >>> tok = PythonTokenizer('for i in range(10): hello $i')
483 >>> tok.consume_till(':')
484 >>> tok.text[:tok.index]
485 'for i in range(10):'
486 >>> tok.text[tok.index:]
495 self.consume_till(')')
497 self.consume_till(']')
499 self.consume_till('}')
501 # if end of line is found, it is an exception.
502 # Since there is no easy way to report the line number,
503 # leave the error reporting to the python parser later
504 #@@ This should be fixed.
508 #raise ParseError, "Expected %s, found end of line." % repr(delim)
510 # raising ParseError doesn't show the line number.
511 # if this error is ignored, then it will be caught when compiling the python code.
515 type, t, begin, end, line = self.tokens.next()
518 return storage(type=type, value=t, begin=begin, end=end)
521 def __init__(self, defwith, suite):
523 self.defwith = defwith.replace('with', '__template__') + ':'
524 # offset 4 lines. for encoding, __lineoffset__, loop and self.
525 self.defwith += "\n __lineoffset__ = -4"
527 self.defwith = 'def __template__():'
528 # offset 4 lines for encoding, __template__, __lineoffset__, loop and self.
529 self.defwith += "\n __lineoffset__ = -5"
531 self.defwith += "\n loop = ForLoop()"
532 self.defwith += "\n self = TemplateResult(); extend_ = self.extend"
534 self.end = "\n return self"
536 def emit(self, indent):
537 encoding = "# coding: utf-8\n"
538 return encoding + self.defwith + self.suite.emit(indent + INDENT) + self.end
541 return "<defwith: %s, %s>" % (self.defwith, self.suite)
544 def __init__(self, value):
547 def emit(self, indent, begin_indent=''):
548 return repr(safeunicode(self.value))
551 return 't' + repr(self.value)
553 class ExpressionNode:
554 def __init__(self, value, escape=True):
555 self.value = value.strip()
557 # convert ${...} to $(...)
558 if value.startswith('{') and value.endswith('}'):
559 self.value = '(' + self.value[1:-1] + ')'
563 def emit(self, indent, begin_indent=''):
564 return 'escape_(%s, %s)' % (self.value, bool(self.escape))
571 return "$%s%s" % (escape, self.value)
573 class AssignmentNode:
574 def __init__(self, code):
577 def emit(self, indent, begin_indent=''):
578 return indent + self.code + "\n"
581 return "<assignment: %s>" % repr(self.code)
584 def __init__(self, nodes):
587 def emit(self, indent, text_indent='', name=''):
588 text = [node.emit('') for node in self.nodes]
590 text = [repr(text_indent)] + text
592 return indent + "extend_([%s])\n" % ", ".join(text)
595 return "<line: %s>" % repr(self.nodes)
597 INDENT = ' ' # 4 spaces
600 def __init__(self, stmt, block, begin_indent=''):
602 self.suite = Parser().read_suite(block)
603 self.begin_indent = begin_indent
605 def emit(self, indent, text_indent=''):
606 text_indent = self.begin_indent + text_indent
607 out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent)
611 return "<block: %s, %s>" % (repr(self.stmt), repr(self.suite))
613 class ForNode(BlockNode):
614 def __init__(self, stmt, block, begin_indent=''):
615 self.original_stmt = stmt
616 tok = PythonTokenizer(stmt)
617 tok.consume_till('in')
618 a = stmt[:tok.index] # for i in
619 b = stmt[tok.index:-1] # rest of for stmt excluding :
620 stmt = a + ' loop.setup(' + b.strip() + '):'
621 BlockNode.__init__(self, stmt, block, begin_indent)
624 return "<block: %s, %s>" % (repr(self.original_stmt), repr(self.suite))
627 def __init__(self, stmt, block, begin_indent=''):
628 # compensate one line for $code:
629 self.code = "\n" + block
631 def emit(self, indent, text_indent=''):
633 rx = re.compile('^', re.M)
634 return rx.sub(indent, self.code).rstrip(' ')
637 return "<code: %s>" % repr(self.code)
640 def __init__(self, stmt):
643 def emit(self, indent, begin_indent=''):
644 return indent + self.stmt
647 return "<stmt: %s>" % repr(self.stmt)
649 class IfNode(BlockNode):
652 class ElseNode(BlockNode):
655 class ElifNode(BlockNode):
658 class DefNode(BlockNode):
659 def __init__(self, *a, **kw):
660 BlockNode.__init__(self, *a, **kw)
662 code = CodeNode("", "")
663 code.code = "self = TemplateResult(); extend_ = self.extend\n"
664 self.suite.sections.insert(0, code)
666 code = CodeNode("", "")
667 code.code = "return self\n"
668 self.suite.sections.append(code)
670 def emit(self, indent, text_indent=''):
671 text_indent = self.begin_indent + text_indent
672 out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent)
673 return indent + "__lineoffset__ -= 3\n" + out
676 def __init__(self, name, value):
680 def emit(self, indent, text_indent):
681 return indent + "self[%s] = %s\n" % (repr(self.name), self.value)
684 return "<var: %s = %s>" % (self.name, self.value)
687 """Suite is a list of sections."""
688 def __init__(self, sections):
689 self.sections = sections
691 def emit(self, indent, text_indent=''):
692 return "\n" + "".join([s.emit(indent, text_indent) for s in self.sections])
695 return repr(self.sections)
714 TEMPLATE_BUILTIN_NAMES = [
715 "dict", "enumerate", "float", "int", "bool", "list", "long", "reversed",
716 "set", "slice", "tuple", "xrange",
717 "abs", "all", "any", "callable", "chr", "cmp", "divmod", "filter", "hex",
718 "id", "isinstance", "iter", "len", "max", "min", "oct", "ord", "pow", "range",
721 "__import__", # some c-libraries like datetime requires __import__ to present in the namespace
725 TEMPLATE_BUILTINS = dict([(name, getattr(__builtin__, name)) for name in TEMPLATE_BUILTIN_NAMES if name in __builtin__.__dict__])
729 Wrapper for expression in for stament to support loop.xxx helpers.
732 >>> for x in loop.setup(['a', 'b', 'c']):
733 ... print loop.index, loop.revindex, loop.parity, x
739 Traceback (most recent call last):
741 AttributeError: index
746 def __getattr__(self, name):
747 if self._ctx is None:
748 raise AttributeError, name
750 return getattr(self._ctx, name)
752 def setup(self, seq):
754 return self._ctx.setup(seq)
757 self._ctx = ForLoopContext(self, self._ctx)
760 self._ctx = self._ctx.parent
762 class ForLoopContext:
763 """Stackable context for ForLoop to support nested for loops.
765 def __init__(self, forloop, parent):
766 self._forloop = forloop
769 def setup(self, seq):
771 self.length = len(seq)
781 index0 = property(lambda self: self.index-1)
782 first = property(lambda self: self.index == 1)
783 last = property(lambda self: self.index == self.length)
784 odd = property(lambda self: self.index % 2 == 1)
785 even = property(lambda self: self.index % 2 == 0)
786 parity = property(lambda self: ['odd', 'even'][self.even])
787 revindex0 = property(lambda self: self.length - self.index)
788 revindex = property(lambda self: self.length - self.index + 1)
791 def __init__(self, code, filename, filter, globals, builtins):
792 self.filename = filename
794 self._globals = globals
795 self._builtins = builtins
797 self.t = self._compile(code)
801 def _compile(self, code):
802 env = self.make_env(self._globals or {}, self._builtins)
804 return env['__template__']
806 def __call__(self, *a, **kw):
807 __hidetraceback__ = True
808 return self.t(*a, **kw)
810 def make_env(self, globals, builtins):
812 __builtins__=builtins,
814 TemplateResult=TemplateResult,
815 escape_=self._escape,
818 def _join(self, *items):
819 return u"".join(items)
821 def _escape(self, value, escape=False):
825 value = safeunicode(value)
826 if escape and self.filter:
827 value = self.filter(value)
830 class Template(BaseTemplate):
832 '.html' : 'text/html; charset=utf-8',
833 '.xhtml' : 'application/xhtml+xml; charset=utf-8',
834 '.txt' : 'text/plain',
843 def __init__(self, text, filename='<template>', filter=None, globals=None, builtins=None, extensions=None):
844 self.extensions = extensions or []
845 text = Template.normalize_text(text)
846 code = self.compile_template(text, filename)
848 _, ext = os.path.splitext(filename)
849 filter = filter or self.FILTERS.get(ext, None)
850 self.content_type = self.CONTENT_TYPES.get(ext, None)
853 globals = self.globals
855 builtins = TEMPLATE_BUILTINS
857 BaseTemplate.__init__(self, code=code, filename=filename, filter=filter, globals=globals, builtins=builtins)
859 def normalize_text(text):
860 """Normalizes template text by correcting \r\n, tabs and BOM chars."""
861 text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs()
862 if not text.endswith('\n'):
865 # ignore BOM chars at the begining of template
867 if isinstance(text, str) and text.startswith(BOM):
868 text = text[len(BOM):]
870 # support fort \$ for backward-compatibility
871 text = text.replace(r'\$', '$$')
873 normalize_text = staticmethod(normalize_text)
875 def __call__(self, *a, **kw):
876 __hidetraceback__ = True
878 if 'headers' in web.ctx and self.content_type:
879 web.header('Content-Type', self.content_type, unique=True)
881 return BaseTemplate.__call__(self, *a, **kw)
883 def generate_code(text, filename, parser=None):
885 parser = parser or Parser()
886 rootnode = parser.parse(text, filename)
888 # generate python code from the parse tree
889 code = rootnode.emit(indent="").strip()
892 generate_code = staticmethod(generate_code)
894 def create_parser(self):
896 for ext in self.extensions:
900 def compile_template(self, template_string, filename):
901 code = Template.generate_code(template_string, filename, parser=self.create_parser())
903 def get_source_line(filename, lineno):
905 lines = open(filename).read().splitlines()
911 # compile the code first to report the errors, if any, with the filename
912 compiled_code = compile(code, filename, 'exec')
913 except SyntaxError, e:
914 # display template line that caused the error along with the traceback.
916 e.msg += '\n\nTemplate traceback:\n File %s, line %s\n %s' % \
917 (repr(e.filename), e.lineno, get_source_line(e.filename, e.lineno-1))
922 # make sure code is safe - but not with jython, it doesn't have a working compiler module
923 if not sys.platform.startswith('java'):
926 ast = compiler.parse(code)
927 SafeVisitor().walk(ast, filename)
929 warnings.warn("Unabled to import compiler module. Unable to check templates for safety.")
931 warnings.warn("SECURITY ISSUE: You are using Jython, which does not support checking templates for safety. Your templates can execute arbitrary code.")
935 class CompiledTemplate(Template):
936 def __init__(self, f, filename):
937 Template.__init__(self, '', filename)
940 def compile_template(self, *a):
943 def _compile(self, *a):
947 """The most preferred way of using templates.
949 render = web.template.render('templates')
952 Optional parameter can be `base` can be used to pass output of
953 every template through the base template.
955 render = web.template.render('templates', base='layout')
957 def __init__(self, loc='templates', cache=None, base=None, **keywords):
959 self._keywords = keywords
962 cache = not config.get('debug', False)
969 if base and not hasattr(base, '__call__'):
970 # make base a function, so that it can be passed to sub-renders
971 self._base = lambda page: self._template(base)(page)
975 def _add_global(self, obj, name=None):
976 """Add a global to this rendering instance."""
977 if 'globals' not in self._keywords: self._keywords['globals'] = {}
980 self._keywords['globals'][name] = obj
982 def _lookup(self, name):
983 path = os.path.join(self._loc, name)
984 if os.path.isdir(path):
987 path = self._findfile(path)
993 def _load_template(self, name):
994 kind, path = self._lookup(name)
997 return Render(path, cache=self._cache is not None, base=self._base, **self._keywords)
999 return Template(open(path).read(), filename=path, **self._keywords)
1001 raise AttributeError, "No template named " + name
1003 def _findfile(self, path_prefix):
1004 p = [f for f in glob.glob(path_prefix + '.*') if not f.endswith('~')] # skip backup files
1005 p.sort() # sort the matches for deterministic order
1008 def _template(self, name):
1009 if self._cache is not None:
1010 if name not in self._cache:
1011 self._cache[name] = self._load_template(name)
1012 return self._cache[name]
1014 return self._load_template(name)
1016 def __getattr__(self, name):
1017 t = self._template(name)
1018 if self._base and isinstance(t, Template):
1019 def template(*a, **kw):
1020 return self._base(t(*a, **kw))
1023 return self._template(name)
1025 class GAE_Render(Render):
1026 # Render gets over-written. make a copy here.
1028 def __init__(self, loc, *a, **kw):
1029 GAE_Render.super.__init__(self, loc, *a, **kw)
1032 if isinstance(loc, types.ModuleType):
1035 name = loc.rstrip('/').replace('/', '.')
1036 self.mod = __import__(name, None, None, ['x'])
1038 self.mod.__dict__.update(kw.get('builtins', TEMPLATE_BUILTINS))
1039 self.mod.__dict__.update(Template.globals)
1040 self.mod.__dict__.update(kw.get('globals', {}))
1042 def _load_template(self, name):
1043 t = getattr(self.mod, name)
1045 if isinstance(t, types.ModuleType):
1046 return GAE_Render(t, cache=self._cache is not None, base=self._base, **self._keywords)
1051 # setup render for Google App Engine.
1053 from google import appengine
1054 render = Render = GAE_Render
1058 def frender(path, **keywords):
1059 """Creates a template from the given file path.
1061 return Template(open(path).read(), filename=path, **keywords)
1063 def compile_templates(root):
1064 """Compiles templates to python code."""
1065 re_start = re_compile('^', re.M)
1067 for dirpath, dirnames, filenames in os.walk(root):
1068 filenames = [f for f in filenames if not f.startswith('.') and not f.endswith('~') and not f.startswith('__init__.py')]
1070 for d in dirnames[:]:
1071 if d.startswith('.'):
1072 dirnames.remove(d) # don't visit this dir
1074 out = open(os.path.join(dirpath, '__init__.py'), 'w')
1075 out.write('from web.template import CompiledTemplate, ForLoop, TemplateResult\n\n')
1077 out.write("import " + ", ".join(dirnames))
1081 path = os.path.join(dirpath, f)
1084 name, _ = f.split('.', 1)
1088 text = open(path).read()
1089 text = Template.normalize_text(text)
1090 code = Template.generate_code(text, path)
1092 code = code.replace("__template__", name, 1)
1097 out.write('%s = CompiledTemplate(%s, %s)\n' % (name, name, repr(path)))
1098 out.write("join_ = %s._join; escape_ = %s._escape\n\n" % (name, name))
1100 # create template to make sure it compiles
1101 t = Template(open(path).read(), path)
1104 class ParseError(Exception):
1107 class SecurityError(Exception):
1108 """The template seems to be trying to do something naughty."""
1111 # Enumerate all the allowed AST nodes
1112 ALLOWED_AST_NODES = [
1115 "AssList", "AssName", "AssTuple",
1117 "Assign", "AugAssign",
1119 "Bitand", "Bitor", "Bitxor", "Break",
1120 "CallFunc","Class", "Compare", "Const", "Continue",
1121 "Decorators", "Dict", "Discard", "Div",
1122 "Ellipsis", "EmptyNode",
1124 "Expression", "FloorDiv", "For",
1127 "GenExpr", "GenExprFor", "GenExprIf", "GenExprInner",
1132 "Invert", "Keyword", "Lambda", "LeftShift",
1133 "List", "ListComp", "ListCompFor", "ListCompIf", "Mod",
1135 "Mul", "Name", "Not", "Or", "Pass", "Power",
1136 # "Print", "Printnl", "Raise",
1137 "Return", "RightShift", "Slice", "Sliceobj",
1138 "Stmt", "Sub", "Subscript",
1139 # "TryExcept", "TryFinally",
1140 "Tuple", "UnaryAdd", "UnarySub",
1141 "While", "With", "Yield",
1144 class SafeVisitor(object):
1146 Make sure code is safe by walking through the AST.
1148 Code considered unsafe if:
1149 * it has restricted AST nodes
1150 * it is trying to access resricted attributes
1152 Adopted from http://www.zafar.se/bkz/uploads/safe.txt (public domain, Babar K. Zafar)
1155 "Initialize visitor by generating callbacks for all AST node types."
1158 def walk(self, ast, filename):
1159 "Validate each node in AST and raise SecurityError if the code is not safe."
1160 self.filename = filename
1164 raise SecurityError, '\n'.join([str(err) for err in self.errors])
1166 def visit(self, node, *args):
1167 "Recursively validate node and all of its children."
1169 return obj.__class__.__name__
1170 nodename = classname(node)
1171 fn = getattr(self, 'visit' + nodename, None)
1176 if nodename not in ALLOWED_AST_NODES:
1177 self.fail(node, *args)
1179 for child in node.getChildNodes():
1180 self.visit(child, *args)
1182 def visitName(self, node, *args):
1183 "Disallow any attempts to access a restricted attr."
1184 #self.assert_attr(node.getChildren()[0], node)
1187 def visitGetattr(self, node, *args):
1188 "Disallow any attempts to access a restricted attribute."
1189 self.assert_attr(node.attrname, node)
1191 def assert_attr(self, attrname, node):
1192 if self.is_unallowed_attr(attrname):
1193 lineno = self.get_node_lineno(node)
1194 e = SecurityError("%s:%d - access to attribute '%s' is denied" % (self.filename, lineno, attrname))
1195 self.errors.append(e)
1197 def is_unallowed_attr(self, name):
1198 return name.startswith('_') \
1199 or name.startswith('func_') \
1200 or name.startswith('im_')
1202 def get_node_lineno(self, node):
1203 return (node.lineno) and node.lineno or 0
1205 def fail(self, node, *args):
1206 "Default callback for unallowed AST nodes."
1207 lineno = self.get_node_lineno(node)
1208 nodename = node.__class__.__name__
1209 e = SecurityError("%s:%d - execution of '%s' statements is denied" % (self.filename, lineno, nodename))
1210 self.errors.append(e)
1212 class TemplateResult(object, DictMixin):
1213 """Dictionary like object for storing template output.
1215 The result of a template execution is usally a string, but sometimes it
1216 contains attributes set using $var. This class provides a simple
1217 dictionary like interface for storing the output of the template and the
1218 attributes. The output is stored with a special key __body__. Convering
1219 the the TemplateResult to string or unicode returns the value of __body__.
1221 When the template is in execution, the output is generated part by part
1222 and those parts are combined at the end. Parts are added to the
1223 TemplateResult by calling the `extend` method and the parts are combined
1224 seemlessly when __body__ is accessed.
1226 >>> d = TemplateResult(__body__='hello, world', x='foo')
1228 <TemplateResult: {'__body__': 'hello, world', 'x': 'foo'}>
1233 >>> d = TemplateResult()
1234 >>> d.extend([u'hello', u'world'])
1236 <TemplateResult: {'__body__': u'helloworld'}>
1238 def __init__(self, *a, **kw):
1239 self.__dict__["_d"] = dict(*a, **kw)
1240 self._d.setdefault("__body__", u'')
1242 self.__dict__['_parts'] = []
1243 self.__dict__["extend"] = self._parts.extend
1245 self._d.setdefault("__body__", None)
1248 return self._d.keys()
1250 def _prepare_body(self):
1251 """Prepare value of __body__ by joining parts.
1254 value = u"".join(self._parts)
1256 body = self._d.get('__body__')
1258 self._d['__body__'] = body + value
1260 self._d['__body__'] = value
1262 def __getitem__(self, name):
1263 if name == "__body__":
1264 self._prepare_body()
1265 return self._d[name]
1267 def __setitem__(self, name, value):
1268 if name == "__body__":
1269 self._prepare_body()
1270 return self._d.__setitem__(name, value)
1272 def __delitem__(self, name):
1273 if name == "__body__":
1274 self._prepare_body()
1275 return self._d.__delitem__(name)
1277 def __getattr__(self, key):
1281 raise AttributeError, k
1283 def __setattr__(self, key, value):
1286 def __delattr__(self, key):
1290 raise AttributeError, k
1292 def __unicode__(self):
1293 self._prepare_body()
1294 return self["__body__"]
1297 self._prepare_body()
1298 return self["__body__"].encode('utf-8')
1301 self._prepare_body()
1302 return "<TemplateResult: %s>" % self._d
1305 r"""Doctest for testing template module.
1307 Define a utility function to run template test.
1309 >>> class TestResult:
1310 ... def __init__(self, t): self.t = t
1311 ... def __getattr__(self, name): return getattr(self.t, name)
1312 ... def __repr__(self): return repr(unicode(self))
1314 >>> def t(code, **keywords):
1315 ... tmpl = Template(code, **keywords)
1316 ... return lambda *a, **kw: TestResult(tmpl(*a, **kw))
1323 >>> t('$def with ()\n1')()
1325 >>> t('$def with (a)\n$a')(1)
1327 >>> t('$def with (a=0)\n$a')(1)
1329 >>> t('$def with (a=0)\n$a')(a=1)
1332 Test complicated expressions.
1334 >>> t('$def with (x)\n$x.upper()')('hello')
1336 >>> t('$(2 * 3 + 4 * 5)')()
1338 >>> t('${2 * 3 + 4 * 5}')()
1340 >>> t('$def with (limit)\nkeep $(limit)ing.')('go')
1342 >>> t('$def with (a)\n$a.b[0]')(storage(b=[1]))
1347 >>> t('$def with (x)\n$x', filename='a.html')('<html>')
1349 >>> t('$def with (x)\n$x', filename='a.txt')('<html>')
1352 Test if, for and while.
1356 >>> t('$if 1:\n 1')()
1358 >>> t('$if 1:\n 1\\')()
1360 >>> t('$if 0: 0\n$elif 1: 1')()
1362 >>> t('$if 0: 0\n$elif None: 0\n$else: 1')()
1364 >>> t('$if 0 < 1 and 1 < 2: 1')()
1366 >>> t('$for x in [1, 2, 3]: $x')()
1368 >>> t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1})
1370 >>> t('$for x in [1, 2, 3]:\n\t$x')()
1372 >>> t('$def with (a)\n$while a and a.pop():1')([1, 2, 3])
1375 The space after : must be ignored.
1377 >>> t('$if True: foo')()
1382 >>> t("$for i in range(5):$loop.index, $loop.parity")()
1383 u'1, odd\n2, even\n3, odd\n4, even\n5, odd\n'
1384 >>> t("$for i in range(2):\n $for j in range(2):$loop.parent.parity $loop.parity")()
1385 u'odd odd\nodd even\neven odd\neven even\n'
1389 >>> t('$ a = 1\n$a')()
1391 >>> t('$ a = [1]\n$a[0]')()
1393 >>> t('$ a = {1: 1}\n$a.keys()[0]')()
1395 >>> t('$ a = []\n$if not a: 1')()
1397 >>> t('$ a = {}\n$if not a: 1')()
1399 >>> t('$ a = -1\n$a')()
1401 >>> t('$ a = "1"\n$a')()
1408 >>> t('hello$#comment1\nhello$#comment2')()
1410 >>> t('$#comment0\nhello$#comment1\nhello$#comment2')()
1415 >>> t('$def with (a)\n$a')(u'\u203d')
1417 >>> t('$def with (a)\n$a')(u'\u203d'.encode('utf-8'))
1419 >>> t(u'$def with (a)\n$a $:a')(u'\u203d')
1421 >>> t(u'$def with ()\nfoo')()
1423 >>> def f(x): return x
1425 >>> t(u'$def with (f)\n$:f("x")')(f)
1427 >>> t('$def with (f)\n$:f("x")')(f)
1430 Test dollar escaping.
1432 >>> t("Stop, $$money isn't evaluated.")()
1433 u"Stop, $money isn't evaluated.\n"
1434 >>> t("Stop, \$money isn't evaluated.")()
1435 u"Stop, $money isn't evaluated.\n"
1437 Test space sensitivity.
1439 >>> t('$def with (x)\n$x')(1)
1441 >>> t('$def with(x ,y)\n$x')(1, 1)
1443 >>> t('$(1 + 2*3 + 4)')()
1446 Make sure globals are working.
1449 Traceback (most recent call last):
1451 NameError: global name 'x' is not defined
1452 >>> t('$x', globals={'x': 1})()
1455 Can't change globals.
1457 >>> t('$ x = 2\n$x', globals={'x': 1})()
1459 >>> t('$ x = x + 1\n$x', globals={'x': 1})()
1460 Traceback (most recent call last):
1462 UnboundLocalError: local variable 'x' referenced before assignment
1464 Make sure builtins are customizable.
1466 >>> t('$min(1, 2)')()
1468 >>> t('$min(1, 2)', builtins={})()
1469 Traceback (most recent call last):
1471 NameError: global name 'min' is not defined
1475 >>> x = t('$var x: 1')()
1478 >>> x = t('$var x = 1')()
1481 >>> x = t('$var x: \n foo\n bar')()
1487 >>> t('\xef\xbb\xbf$def with(x)\n$x')('foo')
1490 Test for with weird cases.
1492 >>> t('$for i in range(10)[1:5]:\n $i')()
1494 >>> t("$for k, v in {'a': 1, 'b': 2}.items():\n $k $v")()
1496 >>> t("$for k, v in ({'a': 1, 'b': 2}.items():\n $k $v")()
1497 Traceback (most recent call last):
1499 SyntaxError: invalid syntax
1504 >>> t("$def with (date)\n$date.strftime('%m %Y')")(datetime.datetime(2009, 1, 1))
1509 if __name__ == "__main__":
1511 if '--compile' in sys.argv:
1512 compile_templates(sys.argv[2])