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