Back to index

moin  1.9.0~rc2
templates.py
Go to the documentation of this file.
00001 # -*- coding: utf-8 -*-
00002 r"""
00003     werkzeug.templates
00004     ~~~~~~~~~~~~~~~~~~
00005 
00006     A minimal template engine.
00007 
00008     :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
00009     :license: BSD License.
00010 """
00011 import sys
00012 import re
00013 import __builtin__ as builtins
00014 from compiler import ast, parse
00015 from compiler.consts import SC_LOCAL, SC_GLOBAL, SC_FREE, SC_CELL
00016 from compiler.pycodegen import ModuleCodeGenerator
00017 from tokenize import PseudoToken
00018 from werkzeug import utils
00019 from werkzeug._internal import _decode_unicode
00020 
00021 
00022 # Copyright notice: The `parse_data` method uses the string interpolation
00023 # algorithm by Ka-Ping Yee which originally was part of `ltpl20.py`_
00024 #
00025 # .. _ltipl20.py: http://lfw.org/python/Itpl20.py
00026 
00027 
00028 token_re = re.compile('%s|%s(?s)' % (
00029     r'[uU]?[rR]?("""|\'\'\')((?<!\\)\\\1|.)*?\1',
00030     PseudoToken
00031 ))
00032 directive_re = re.compile(r'(?<!\\)<%(?:(#)|(py(?:thon)?\b)|'
00033                           r'(?:\s*(\w+))\s*)(.*?)\s*%>\n?(?s)')
00034 escape_re = re.compile(r'\\\n|\\(\\|<%)')
00035 namestart_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
00036 undefined = type('UndefinedType', (object,), {
00037     '__iter__': lambda x: iter(()),
00038     '__repr__': lambda x: 'Undefined',
00039     '__str__':  lambda x: ''
00040 })()
00041 runtime_vars = dict.fromkeys(('Undefined', '__to_unicode', '__context',
00042                               '__write', '__write_many'))
00043 
00044 
00045 def call_stmt(func, args, lineno):
00046     return ast.CallFunc(ast.Name(func, lineno=lineno),
00047                         args, lineno=lineno)
00048 
00049 
00050 def tokenize(source, filename):
00051     escape = escape_re.sub
00052     escape_repl = lambda m: m.group(1) or ''
00053     lineno = 1
00054     pos = 0
00055 
00056     for match in directive_re.finditer(source):
00057         start, end = match.span()
00058         if start > pos:
00059             data = source[pos:start]
00060             yield lineno, 'data', escape(escape_repl, data)
00061             lineno += data.count('\n')
00062         is_comment, is_code, cmd, args = match.groups()
00063         if is_code:
00064             yield lineno, 'code', args
00065         elif not is_comment:
00066             yield lineno, 'cmd', (cmd, args)
00067         lineno += source[start:end].count('\n')
00068         pos = end
00069 
00070     if pos < len(source):
00071         yield lineno, 'data', escape(escape_repl, source[pos:])
00072 
00073 
00074 def transform(node, filename):
00075     root = ast.Module(None, node, lineno=1)
00076     nodes = [root]
00077     while nodes:
00078         node = nodes.pop()
00079         node.filename = filename
00080         if node.__class__ in (ast.Printnl, ast.Print):
00081             node.dest = ast.Name('__context')
00082         elif node.__class__ is ast.Const and isinstance(node.value, str):
00083             try:
00084                 node.value.decode('ascii')
00085             except UnicodeError:
00086                 node.value = node.value.decode('utf-8')
00087         nodes.extend(node.getChildNodes())
00088     return root
00089 
00090 
00091 class TemplateSyntaxError(SyntaxError):
00092 
00093     def __init__(self, msg, filename, lineno):
00094         from linecache import getline
00095         l = getline(filename, lineno)
00096         SyntaxError.__init__(self, msg, (filename, lineno, len(l) or 1, l))
00097 
00098 
00099 class Parser(object):
00100 
00101     def __init__(self, gen, filename):
00102         self.gen = gen
00103         self.filename = filename
00104         self.lineno = 1
00105 
00106     def fail(self, msg):
00107         raise TemplateSyntaxError(msg, self.filename, self.lineno)
00108 
00109     def parse_python(self, expr, type='exec'):
00110         if isinstance(expr, unicode):
00111             expr = '\xef\xbb\xbf' + expr.encode('utf-8')
00112         try:
00113             node = parse(expr, type)
00114         except SyntaxError, e:
00115             raise TemplateSyntaxError(str(e), self.filename,
00116                                       self.lineno + e.lineno - 1)
00117         nodes = [node]
00118         while nodes:
00119             n = nodes.pop()
00120             if hasattr(n, 'lineno'):
00121                 n.lineno = (n.lineno or 1) + self.lineno - 1
00122             nodes.extend(n.getChildNodes())
00123         return node.node
00124 
00125     def parse(self, needle=()):
00126         start_lineno = self.lineno
00127         result = []
00128         add = result.append
00129         for self.lineno, token, value in self.gen:
00130             if token == 'data':
00131                 add(self.parse_data(value))
00132             elif token == 'code':
00133                 add(self.parse_code(value.splitlines()))
00134             elif token == 'cmd':
00135                 name, args = value
00136                 if name in needle:
00137                     return name, args, ast.Stmt(result, lineno=start_lineno)
00138                 if name in ('for', 'while'):
00139                     add(self.parse_loop(args, name))
00140                 elif name == 'if':
00141                     add(self.parse_if(args))
00142                 else:
00143                     self.fail('unknown directive %s' % name)
00144         if needle:
00145             self.fail('unexpected end of template')
00146         return ast.Stmt(result, lineno=start_lineno)
00147 
00148     def parse_loop(self, args, type):
00149         rv = self.parse_python('%s %s: pass' % (type, args), 'exec').nodes[0]
00150         tag, value, rv.body = self.parse(('end' + type, 'else'))
00151         if value:
00152             self.fail('unexpected data after ' + tag)
00153         if tag == 'else':
00154             tag, value, rv.else_ = self.parse(('end' + type,))
00155             if value:
00156                 self.fail('unexpected data after else')
00157         return rv
00158 
00159     def parse_if(self, args):
00160         cond = self.parse_python('if %s: pass' % args).nodes[0]
00161         tag, value, body = self.parse(('else', 'elif', 'endif'))
00162         cond.tests[0] = (cond.tests[0][0], body)
00163         while 1:
00164             if tag == 'else':
00165                 if value:
00166                     self.fail('unexpected data after else')
00167                 tag, value, cond.else_ = self.parse(('endif',))
00168             elif tag == 'elif':
00169                 expr = self.parse_python(value, 'eval')
00170                 tag, value, body = self.parse(('else', 'elif', 'endif'))
00171                 cond.tests.append((expr, body))
00172                 continue
00173             break
00174         if value:
00175             self.fail('unexpected data after endif')
00176         return cond
00177 
00178     def parse_code(self, lines):
00179         margin = sys.maxint
00180         for line in lines[1:]:
00181             content = len(line.lstrip())
00182             if content:
00183                 indent = len(line) - content
00184                 margin = min(margin, indent)
00185         if lines:
00186             lines[0] = lines[0].lstrip()
00187         if margin < sys.maxint:
00188             for i in xrange(1, len(lines)):
00189                 lines[i] = lines[i][margin:]
00190         while lines and not lines[-1]:
00191             lines.pop()
00192         while lines and not lines[0]:
00193             lines.pop(0)
00194         return self.parse_python('\n'.join(lines))
00195 
00196     def parse_data(self, text):
00197         start_lineno = lineno = self.lineno
00198         pos = 0
00199         end = len(text)
00200         nodes = []
00201 
00202         def match_or_fail(pos):
00203             match = token_re.match(text, pos)
00204             if match is None:
00205                 self.fail('invalid syntax')
00206             return match.group().strip(), match.end()
00207 
00208         def write_expr(code):
00209             node = self.parse_python(code, 'eval')
00210             nodes.append(call_stmt('__to_unicode', [node], lineno))
00211             return code.count('\n')
00212 
00213         def write_data(value):
00214             if value:
00215                 nodes.append(ast.Const(value, lineno=lineno))
00216                 return value.count('\n')
00217             return 0
00218 
00219         while 1:
00220             offset = text.find('$', pos)
00221             if offset < 0:
00222                 break
00223             next = text[offset + 1]
00224 
00225             if next == '{':
00226                 lineno += write_data(text[pos:offset])
00227                 pos = offset + 2
00228                 level = 1
00229                 while level:
00230                     token, pos = match_or_fail(pos)
00231                     if token in ('{', '}'):
00232                         level += token == '{' and 1 or -1
00233                 lineno += write_expr(text[offset + 2:pos - 1])
00234             elif next in namestart_chars:
00235                 lineno += write_data(text[pos:offset])
00236                 token, pos = match_or_fail(offset + 1)
00237                 while pos < end:
00238                     if text[pos] == '.' and pos + 1 < end and \
00239                        text[pos + 1] in namestart_chars:
00240                         token, pos = match_or_fail(pos + 1)
00241                     elif text[pos] in '([':
00242                         pos += 1
00243                         level = 1
00244                         while level:
00245                             token, pos = match_or_fail(pos)
00246                             if token in ('(', ')', '[', ']'):
00247                                 level += token in '([' and 1 or -1
00248                     else:
00249                         break
00250                 lineno += write_expr(text[offset + 1:pos])
00251             else:
00252                 lineno += write_data(text[pos:offset + 1])
00253                 pos = offset + 1 + (next == '$')
00254         write_data(text[pos:])
00255 
00256         return ast.Discard(call_stmt(len(nodes) == 1 and '__write' or
00257                            '__write_many', nodes, start_lineno),
00258                            lineno=start_lineno)
00259 
00260 
00261 class Context(object):
00262 
00263     def __init__(self, namespace, charset, errors):
00264         self.charset = charset
00265         self.errors = errors
00266         self._namespace = namespace
00267         self._buffer = []
00268         self._write = self._buffer.append
00269         _extend = self._buffer.extend
00270         self.runtime = dict(
00271             Undefined=undefined,
00272             __to_unicode=self.to_unicode,
00273             __context=self,
00274             __write=self._write,
00275             __write_many=lambda *a: _extend(a)
00276         )
00277 
00278     def write(self, value):
00279         self._write(self.to_unicode(value))
00280 
00281     def to_unicode(self, value):
00282         if isinstance(value, str):
00283             return _decode_unicode(value, self.charset, self.errors)
00284         return unicode(value)
00285 
00286     def get_value(self, as_unicode=True):
00287         rv = u''.join(self._buffer)
00288         if not as_unicode:
00289             return rv.encode(self.charset, self.errors)
00290         return rv
00291 
00292     def __getitem__(self, key, default=undefined):
00293         try:
00294             return self._namespace[key]
00295         except KeyError:
00296             return getattr(builtins, key, default)
00297 
00298     def get(self, key, default=None):
00299         return self.__getitem__(key, default)
00300 
00301     def __setitem__(self, key, value):
00302         self._namespace[key] = value
00303 
00304     def __delitem__(self, key):
00305         del self._namespace[key]
00306 
00307 
00308 class TemplateCodeGenerator(ModuleCodeGenerator):
00309 
00310     def __init__(self, node, filename):
00311         ModuleCodeGenerator.__init__(self, transform(node, filename))
00312 
00313     def _nameOp(self, prefix, name):
00314         if name in runtime_vars:
00315             return self.emit(prefix + '_GLOBAL', name)
00316         return ModuleCodeGenerator._nameOp(self, prefix, name)
00317 
00318 
00319 class Template(object):
00320     """Represents a simple text based template.  It's a good idea to load such
00321     templates from files on the file system to get better debug output.
00322     """
00323     default_context = {
00324         'escape':           utils.escape,
00325         'url_quote':        utils.url_quote,
00326         'url_quote_plus':   utils.url_quote_plus,
00327         'url_encode':       utils.url_encode
00328     }
00329 
00330     def __init__(self, source, filename='<template>', charset='utf-8',
00331                  errors='strict', unicode_mode=True):
00332         if isinstance(source, str):
00333             source = _decode_unicode(source, charset, errors)
00334         if isinstance(filename, unicode):
00335             filename = filename.encode('utf-8')
00336         node = Parser(tokenize(u'\n'.join(source.splitlines()),
00337                                filename), filename).parse()
00338         self.code = TemplateCodeGenerator(node, filename).getCode()
00339         self.filename = filename
00340         self.charset = charset
00341         self.errors = errors
00342         self.unicode_mode = unicode_mode
00343 
00344     @classmethod
00345     def from_file(cls, file, charset='utf-8', errors='strict',
00346                   unicode_mode=True, encoding=None):
00347         """Load a template from a file.
00348 
00349         .. versionchanged:: 0.5
00350             The encoding parameter was renamed to charset.
00351 
00352         :param file: a filename or file object to load the template from.
00353         :param charset: the charset of the template to load.
00354         :param errors: the error behavior of the charset decoding.
00355         :param unicode_mode: set to `False` to disable unicode mode.
00356         :return: a template
00357         """
00358         if encoding is not None:
00359             from warnings import warn
00360             warn(DeprecationWarning('the encoding parameter is deprecated. '
00361                                     'use charset instead.'), stacklevel=2)
00362             charset = encoding
00363         close = False
00364         if isinstance(file, basestring):
00365             f = open(file, 'r')
00366             close = True
00367         try:
00368             data = _decode_unicode(f.read(), charset, errors)
00369         finally:
00370             if close:
00371                 f.close()
00372         return cls(data, getattr(f, 'name', '<template>'), charset,
00373                    errors, unicode_mode)
00374 
00375     def render(self, *args, **kwargs):
00376         """This function accepts either a dict or some keyword arguments which
00377         will then be the context the template is evaluated in.  The return
00378         value will be the rendered template.
00379 
00380         :param context: the function accepts the same arguments as the
00381                         :class:`dict` constructor.
00382         :return: the rendered template as string
00383         """
00384         ns = self.default_context.copy()
00385         if len(args) == 1 and isinstance(args[0], utils.MultiDict):
00386             ns.update(args[0].to_dict(flat=True))
00387         else:
00388             ns.update(dict(*args))
00389         if kwargs:
00390             ns.update(kwargs)
00391         context = Context(ns, self.charset, self.errors)
00392         exec self.code in context.runtime, context
00393         return context.get_value(self.unicode_mode)
00394 
00395     def substitute(self, *args, **kwargs):
00396         """For API compatibility with `string.Template`."""
00397         return self.render(*args, **kwargs)