## # $Release: 1.1.1 $ # $Copyright: copyright(c) 2007-2012 kuwata-lab.com all rights reserved. $ # $License: MIT License $ ## # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: ## # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. ## # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## """Very fast and light-weight template engine based embedded Python. See User's Guide and examples for details. http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html http://www.kuwata-lab.com/tenjin/pytenjin-examples.html """ __version__ = "$Release: 1.1.1 $"[10:-2] __license__ = "$License: MIT License $"[10:-2] __all__ = ('Template', 'Engine', ) from os.path import isfile as _isfile from os.path import getmtime as _getmtime from time import time as _time import sys import os import re import time import marshal random = pickle = unquote = None # lazy import python3 = sys.version_info[0] == 3 python2 = sys.version_info[0] == 2 logger = None ## # utilities ## def _write_binary_file(filename, content): global random if random is None: from random import random tmpfile = filename + str(random())[1:] f = open(tmpfile, 'w+b') # on windows, 'w+b' is preffered than 'wb' try: f.write(content) finally: f.close() if os.path.exists(tmpfile): try: os.rename(tmpfile, filename) except: # on windows, existing file should be removed before renaming os.remove(filename) os.rename(tmpfile, filename) def _read_binary_file(filename): f = open(filename, 'rb') try: return f.read() finally: f.close() codecs = None # lazy import def _read_text_file(filename, encoding=None): global codecs if not codecs: import codecs f = codecs.open(filename, encoding=(encoding or 'utf-8')) try: return f.read() finally: f.close() def _read_template_file(filename, encoding=None): s = _read_binary_file(filename) # binary(=str) if encoding: s = s.decode(encoding) # binary(=str) to unicode return s _basestring = basestring _unicode = unicode _bytes = str def _ignore_not_found_error(f, default=None): try: return f() except OSError, ex: if ex.errno == 2: # error: No such file or directory return default raise def create_module(module_name, dummy_func=None, **kwargs): """ex. mod = create_module('tenjin.util')""" try: mod = type(sys)(module_name) except: # The module creation above does not work for Jython 2.5.2 import imp mod = imp.new_module(module_name) mod.__file__ = __file__ mod.__dict__.update(kwargs) sys.modules[module_name] = mod if dummy_func: exec(dummy_func.func_code, mod.__dict__) return mod def _raise(exception_class, *args): raise exception_class(*args) ## # helper method's module ## def _dummy(): global unquote unquote = None global to_str, escape, echo, new_cycle, generate_tostrfunc global start_capture, stop_capture, capture_as, captured_as, CaptureContext global _p, _P, _decode_params def generate_tostrfunc(encode=None, decode=None): """Generate 'to_str' function with encode or decode encoding. ex. generate to_str() function which encodes unicode into binary(=str). to_str = tenjin.generate_tostrfunc(encode='utf-8') repr(to_str(u'hoge')) #=> 'hoge' (str) ex. generate to_str() function which decodes binary(=str) into unicode. to_str = tenjin.generate_tostrfunc(decode='utf-8') repr(to_str('hoge')) #=> u'hoge' (unicode) """ if encode: if decode: raise ValueError( "can't specify both encode and decode encoding.") else: def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _encode=encode): """Convert val into string or return '' if None. Unicode will be encoded into binary(=str).""" if _isa(val, _str): return val if val is None: return '' # if _isa(val, _unicode): return val.encode(_encode) # unicode to binary(=str) if _isa(val, _unicode): return val.encode(_encode) # unicode to binary(=str) return _str(val) else: if decode: def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _decode=decode): """Convert val into string or return '' if None. Binary(=str) will be decoded into unicode.""" # if _isa(val, _str): return val.decode(_decode) # binary(=str) to unicode if _isa(val, _str): return val.decode(_decode) if val is None: return '' if _isa(val, _unicode): return val return _unicode(val) else: def to_str(val, _str=str, _unicode=unicode, _isa=isinstance): """Convert val into string or return '' if None. Both binary(=str) and unicode will be retruned as-is.""" if _isa(val, _str): return val if val is None: return '' if _isa(val, _unicode): return val return _str(val) return to_str to_str = generate_tostrfunc(encode='utf-8') # or encode=None? def echo(string): """add string value into _buf. this is equivarent to '#{string}'.""" lvars = sys._getframe(1).f_locals # local variables lvars['_buf'].append(string) def new_cycle(*values): """Generate cycle object. ex. cycle = new_cycle('odd', 'even') print(cycle()) #=> 'odd' print(cycle()) #=> 'even' print(cycle()) #=> 'odd' print(cycle()) #=> 'even' """ def gen(values): i, n = 0, len(values) while True: yield values[i] i = (i + 1) % n return gen(values).next class CaptureContext(object): def __init__(self, name, store_to_context=True, lvars=None): self.name = name self.store_to_context = store_to_context self.lvars = lvars or sys._getframe(1).f_locals def __enter__(self): lvars = self.lvars self._buf_orig = lvars['_buf'] lvars['_buf'] = _buf = [] lvars['_extend'] = _buf.extend return self def __exit__(self, *args): lvars = self.lvars _buf = lvars['_buf'] lvars['_buf'] = self._buf_orig lvars['_extend'] = self._buf_orig.extend lvars[self.name] = self.captured = ''.join(_buf) if self.store_to_context and '_context' in lvars: lvars['_context'][self.name] = self.captured def __iter__(self): self.__enter__() yield self self.__exit__() def start_capture(varname=None, _depth=1): """(obsolete) start capturing with name.""" lvars = sys._getframe(_depth).f_locals capture_context = CaptureContext(varname, None, lvars) lvars['_capture_context'] = capture_context capture_context.__enter__() def stop_capture(store_to_context=True, _depth=1): """(obsolete) stop capturing and return the result of capturing. if store_to_context is True then the result is stored into _context[varname]. """ lvars = sys._getframe(_depth).f_locals capture_context = lvars.pop('_capture_context', None) if not capture_context: raise Exception( 'stop_capture(): start_capture() is not called before.') capture_context.store_to_context = store_to_context capture_context.__exit__() return capture_context.captured def capture_as(name, store_to_context=True): """capture partial of template.""" return CaptureContext(name, store_to_context, sys._getframe(1).f_locals) def captured_as(name, _depth=1): """helper method for layout template. if captured string is found then append it to _buf and return True, else return False. """ lvars = sys._getframe(_depth).f_locals # local variables if name in lvars: _buf = lvars['_buf'] _buf.append(lvars[name]) return True return False def _p(arg): """ex. '/show/'+_p("item['id']") => "/show/#{item['id']}" """ return '<`#%s#`>' % arg # decoded into #{...} by preprocessor def _P(arg): """ex. '%s' % _P("item['id']") => "${item['id']}" """ return '<`$%s$`>' % arg # decoded into ${...} by preprocessor def _decode_params(s): """decode <`#...#`> and <`$...$`> into #{...} and ${...}""" global unquote if unquote is None: from urllib import unquote dct = {'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', '#039': "'", } def unescape(s): # return s.replace('<', '<').replace('>', '>').replace('"', '"').replace(''', "'").replace('&', '&') return re.sub(r'&(lt|gt|quot|amp|#039);', lambda m: dct[m.group(1)], s) s = to_str(s) s = re.sub(r'%3C%60%23(.*?)%23%60%3E', lambda m: '#{%s}' % unquote(m.group(1)), s) s = re.sub(r'%3C%60%24(.*?)%24%60%3E', lambda m: '${%s}' % unquote(m.group(1)), s) s = re.sub(r'<`#(.*?)#`>', lambda m: '#{%s}' % unescape(m.group(1)), s) s = re.sub(r'<`\$(.*?)\$`>', lambda m: '${%s}' % unescape(m.group(1)), s) s = re.sub(r'<`#(.*?)#`>', r'#{\1}', s) s = re.sub(r'<`\$(.*?)\$`>', r'${\1}', s) return s helpers = create_module('tenjin.helpers', _dummy, sys=sys, re=re) helpers.__all__ = ['to_str', 'escape', 'echo', 'new_cycle', 'generate_tostrfunc', 'start_capture', 'stop_capture', 'capture_as', 'captured_as', 'not_cached', 'echo_cached', 'cache_as', '_p', '_P', '_decode_params', ] generate_tostrfunc = helpers.generate_tostrfunc ## # escaped module ## def _dummy(): global is_escaped, as_escaped, to_escaped global Escaped, EscapedStr, EscapedUnicode global __all__ # 'Escaped', 'EscapedStr', __all__ = ('is_escaped', 'as_escaped', 'to_escaped', ) class Escaped(object): """marking class that object is already escaped.""" pass def is_escaped(value): """return True if value is marked as escaped, else return False.""" return isinstance(value, Escaped) class EscapedStr(str, Escaped): """string class which is marked as escaped.""" pass class EscapedUnicode(unicode, Escaped): """unicode class which is marked as escaped.""" pass def as_escaped(s): """mark string as escaped, without escaping.""" if isinstance(s, str): return EscapedStr(s) if isinstance(s, unicode): return EscapedUnicode(s) raise TypeError("as_escaped(%r): expected str or unicode." % (s, )) def to_escaped(value): """convert any value into string and escape it. if value is already marked as escaped, don't escape it.""" if hasattr(value, '__html__'): value = value.__html__() if is_escaped(value): # return value # EscapedUnicode should be convered into EscapedStr return as_escaped(_helpers.to_str(value)) # if isinstance(value, _basestring): # return as_escaped(_helpers.escape(value)) return as_escaped(_helpers.escape(_helpers.to_str(value))) escaped = create_module('tenjin.escaped', _dummy, _helpers=helpers) ## # module for html ## def _dummy(): global escape_html, escape_xml, escape, tagattr, tagattrs, _normalize_attrs global checked, selected, disabled, nl2br, text2html, nv, js_link # _escape_table = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } #_escape_pattern = re.compile(r'[&<>"]') ##_escape_callable = lambda m: _escape_table[m.group(0)] ##_escape_callable = lambda m: _escape_table.__get__(m.group(0)) #_escape_get = _escape_table.__getitem__ #_escape_callable = lambda m: _escape_get(m.group(0)) #_escape_sub = _escape_pattern.sub # def escape_html(s): # return s # 3.02 # def escape_html(s): # return _escape_pattern.sub(_escape_callable, s) # 6.31 # def escape_html(s): # return _escape_sub(_escape_callable, s) # 6.01 # def escape_html(s, _p=_escape_pattern, _f=_escape_callable): # return _p.sub(_f, s) # 6.27 # def escape_html(s, _sub=_escape_pattern.sub, _callable=_escape_callable): # return _sub(_callable, s) # 6.04 # def escape_html(s): # s = s.replace('&', '&') # s = s.replace('<', '<') # s = s.replace('>', '>') # s = s.replace('"', '"') # return s # 5.83 def escape_html(s): """Escape '&', '<', '>', '"' into '&', '<', '>', '"'.""" return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') # 5.72 escape_xml = escape_html # for backward compatibility def tagattr(name, expr, value=None, escape=True): """(experimental) Return ' name="value"' if expr is true value, else '' (empty string). If value is not specified, expr is used as value instead.""" if not expr and expr != 0: return _escaped.as_escaped('') if value is None: value = expr if escape: value = _escaped.to_escaped(value) return _escaped.as_escaped(' %s="%s"' % (name, value)) def tagattrs(**kwargs): """(experimental) built html tag attribtes. ex. >>> tagattrs(klass='main', size=20) ' class="main" size="20"' >>> tagattrs(klass='', size=0) '' """ kwargs = _normalize_attrs(kwargs) esc = _escaped.to_escaped s = ''.join([' %s="%s"' % (k, esc(v)) for k, v in kwargs.iteritems() if v or v == 0]) return _escaped.as_escaped(s) def _normalize_attrs(kwargs): if 'klass' in kwargs: kwargs['class'] = kwargs.pop('klass') if 'checked' in kwargs: kwargs['checked'] = kwargs.pop('checked') and 'checked' or None if 'selected' in kwargs: kwargs['selected'] = kwargs.pop('selected') and 'selected' or None if 'disabled' in kwargs: kwargs['disabled'] = kwargs.pop('disabled') and 'disabled' or None return kwargs def checked(expr): """return ' checked="checked"' if expr is true.""" return _escaped.as_escaped(expr and ' checked="checked"' or '') def selected(expr): """return ' selected="selected"' if expr is true.""" return _escaped.as_escaped(expr and ' selected="selected"' or '') def disabled(expr): """return ' disabled="disabled"' if expr is true.""" return _escaped.as_escaped(expr and ' disabled="disabled"' or '') def nl2br(text): """replace "\n" to "
\n" and return it.""" if not text: return _escaped.as_escaped('') return _escaped.as_escaped(text.replace('\n', '
\n')) def text2html(text, use_nbsp=True): """(experimental) escape xml characters, replace "\n" to "
\n", and return it.""" if not text: return _escaped.as_escaped('') s = _escaped.to_escaped(text) if use_nbsp: s = s.replace(' ', '  ') # return nl2br(s) s = s.replace('\n', '
\n') return _escaped.as_escaped(s) def nv(name, value, sep=None, **kwargs): """(experimental) Build name and value attributes. ex. >>> nv('rank', 'A') 'name="rank" value="A"' >>> nv('rank', 'A', '.') 'name="rank" value="A" id="rank.A"' >>> nv('rank', 'A', '.', checked=True) 'name="rank" value="A" id="rank.A" checked="checked"' >>> nv('rank', 'A', '.', klass='error', style='color:red') 'name="rank" value="A" id="rank.A" class="error" style="color:red"' """ name = _escaped.to_escaped(name) value = _escaped.to_escaped(value) s = sep and 'name="%s" value="%s" id="%s"' % (name, value, name+sep+value) \ or 'name="%s" value="%s"' % (name, value) html = kwargs and s + tagattrs(**kwargs) or s return _escaped.as_escaped(html) def js_link(label, onclick, **kwargs): s = kwargs and tagattrs(**kwargs) or '' html = '%s' % \ (_escaped.to_escaped(onclick), s, _escaped.to_escaped(label)) return _escaped.as_escaped(html) html = create_module('tenjin.html', _dummy, helpers=helpers, _escaped=escaped) helpers.escape = html.escape_html helpers.html = html # for backward compatibility sys.modules['tenjin.helpers.html'] = html ## # utility function to set default encoding of template files ## _template_encoding = (None, 'utf-8') # encodings for decode and encode def set_template_encoding(decode=None, encode=None): """Set default encoding of template files. This should be called before importing helper functions. ex. ## I like template files to be unicode-base like Django. import tenjin tenjin.set_template_encoding('utf-8') # should be called before importing helpers from tenjin.helpers import * """ global _template_encoding if _template_encoding == (decode, encode): return if decode and encode: raise ValueError( "set_template_encoding(): cannot specify both decode and encode.") if not decode and not encode: raise ValueError( "set_template_encoding(): decode or encode should be specified.") if decode: Template.encoding = decode # unicode base template helpers.to_str = helpers.generate_tostrfunc(decode=decode) else: Template.encoding = None # binary base template helpers.to_str = helpers.generate_tostrfunc(encode=encode) _template_encoding = (decode, encode) ## # Template class ## class TemplateSyntaxError(SyntaxError): def build_error_message(self): ex = self if not ex.text: return self.args[0] return ''.join([ "%s:%s:%s: %s\n" % (ex.filename, ex.lineno, ex.offset, ex.msg, ), "%4d: %s\n" % (ex.lineno, ex.text.rstrip(), ), " %s^\n" % (' ' * ex.offset, ), ]) class Template(object): """Convert and evaluate embedded python string. See User's Guide and examples for details. http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html http://www.kuwata-lab.com/tenjin/pytenjin-examples.html """ # default value of attributes filename = None encoding = None escapefunc = 'escape' tostrfunc = 'to_str' indent = 4 preamble = None # "_buf = []; _expand = _buf.expand; _to_str = to_str; _escape = escape" postamble = None # "print ''.join(_buf)" smarttrim = None args = None timestamp = None trace = False # if True then '' and '' are printed def __init__(self, filename=None, encoding=None, input=None, escapefunc=None, tostrfunc=None, indent=None, preamble=None, postamble=None, smarttrim=None, trace=None): """Initailizer of Template class. filename:str (=None) Filename to convert (optional). If None, no convert. encoding:str (=None) Encoding name. If specified, template string is converted into unicode object internally. Template.render() returns str object if encoding is None, else returns unicode object if encoding name is specified. input:str (=None) Input string. In other words, content of template file. Template file will not be read if this argument is specified. escapefunc:str (='escape') Escape function name. tostrfunc:str (='to_str') 'to_str' function name. indent:int (=4) Indent width. preamble:str or bool (=None) Preamble string which is inserted into python code. If true, '_buf = []; ' is used insated. postamble:str or bool (=None) Postamble string which is appended to python code. If true, 'print("".join(_buf))' is used instead. smarttrim:bool (=None) If True then "
\\n#{_context}\\n
" is parsed as "
\\n#{_context}
". """ if encoding is not None: self.encoding = encoding if escapefunc is not None: self.escapefunc = escapefunc if tostrfunc is not None: self.tostrfunc = tostrfunc if indent is not None: self.indent = indent if preamble is not None: self.preamble = preamble if postamble is not None: self.postamble = postamble if smarttrim is not None: self.smarttrim = smarttrim if trace is not None: self.trace = trace # if preamble is True: self.preamble = "_buf = []" if postamble is True: self.postamble = "print(''.join(_buf))" if input: self.convert(input, filename) # False means 'file not exist' (= Engine should not check timestamp of file) self.timestamp = False elif filename: self.convert_file(filename) else: self._reset() def _reset(self, input=None, filename=None): self.script = None self.bytecode = None self.input = input self.filename = filename if input != None: i = input.find("\n") if i < 0: self.newline = "\n" # or None elif len(input) >= 2 and input[i-1] == "\r": self.newline = "\r\n" else: self.newline = "\n" self._localvars_assignments_added = False def _localvars_assignments(self): return "_extend=_buf.extend;_to_str=%s;_escape=%s; " % (self.tostrfunc, self.escapefunc) def before_convert(self, buf): if self.preamble: eol = self.input.startswith('([ \t]*\r?\n)?', re.S) def stmt_pattern(self): pat = self.STMT_PATTERN if isinstance(pat, tuple): pat = self.__class__.STMT_PATTERN = re.compile(*pat) return pat def parse_stmts(self, buf, input): if not input: return rexp = self.stmt_pattern() is_bol = True index = 0 for m in rexp.finditer(input): mspace, code, rspace = m.groups() #mspace, close, rspace = m.groups() #code = input[m.start()+4+len(mspace):m.end()-len(close)-(rspace and len(rspace) or 0)] text = input[index:m.start()] index = m.end() # detect spaces at beginning of line lspace = None if text == '': if is_bol: lspace = '' elif text[-1] == '\n': lspace = '' else: rindex = text.rfind('\n') if rindex < 0: if is_bol and text.isspace(): lspace, text = text, '' else: s = text[rindex+1:] if s.isspace(): lspace, text = s, text[:rindex+1] #is_bol = rspace is not None # add text, spaces, and statement self.parse_exprs(buf, text, is_bol) is_bol = rspace is not None # if mspace == "\n": if mspace and mspace.endswith("\n"): code = "\n" + (code or "") # if rspace == "\n": if rspace and rspace.endswith("\n"): code = (code or "") + "\n" if code: code = self.statement_hook(code) m = self._match_to_args_declaration(code) if m: self._add_args_declaration(buf, m) else: self.add_stmt(buf, code) rest = input[index:] if rest: self.parse_exprs(buf, rest) self._arrange_indent(buf) def statement_hook(self, stmt): """expand macros and parse '#@ARGS' in a statement.""" return stmt.replace("\r\n", "\n") # Python can't handle "\r\n" in code def _match_to_args_declaration(self, stmt): if self.args is not None: return None args_pattern = r'^ *#@ARGS(?:[ \t]+(.*?))?$' return re.match(args_pattern, stmt) def _add_args_declaration(self, buf, m): arr = (m.group(1) or '').split(',') args = [] declares = [] for s in arr: arg = s.strip() if not s: continue if not re.match('^[a-zA-Z_]\w*$', arg): raise ValueError("%r: invalid template argument." % arg) args.append(arg) declares.append("%s = _context.get('%s'); " % (arg, arg)) self.args = args #nl = stmt[m.end():] #if nl: declares.append(nl) buf.append(''.join(declares) + "\n") s = '(?:\{.*?\}.*?)*' EXPR_PATTERN = ( r'#\{(.*?'+s+r')\}|\$\{(.*?'+s+r')\}|\{=(?:=(.*?)=|(.*?))=\}', re.S) del s def expr_pattern(self): pat = self.EXPR_PATTERN if isinstance(pat, tuple): self.__class__.EXPR_PATTERN = pat = re.compile(*pat) return pat def get_expr_and_flags(self, match): expr1, expr2, expr3, expr4 = match.groups() if expr1 is not None: return expr1, (False, True) # not escape, call to_str if expr2 is not None: return expr2, (True, True) # call escape, call to_str if expr3 is not None: return expr3, (False, True) # not escape, call to_str if expr4 is not None: return expr4, (True, True) # call escape, call to_str def parse_exprs(self, buf, input, is_bol=False): buf2 = [] self._parse_exprs(buf2, input, is_bol) if buf2: buf.append(''.join(buf2)) def _parse_exprs(self, buf, input, is_bol=False): if not input: return self.start_text_part(buf) rexp = self.expr_pattern() smarttrim = self.smarttrim nl = self.newline nl_len = len(nl) pos = 0 for m in rexp.finditer(input): start = m.start() text = input[pos:start] pos = m.end() expr, flags = self.get_expr_and_flags(m) # if text: self.add_text(buf, text) self.add_expr(buf, expr, *flags) # if smarttrim: flag_bol = text.endswith( nl) or not text and (start > 0 or is_bol) if flag_bol and not flags[0] and input[pos:pos+nl_len] == nl: pos += nl_len buf.append("\n") if smarttrim: if buf and buf[-1] == "\n": buf.pop() rest = input[pos:] if rest: self.add_text(buf, rest, True) self.stop_text_part(buf) if input[-1] == '\n': buf.append("\n") def start_text_part(self, buf): self._add_localvars_assignments_to_text(buf) # buf.append("_buf.extend((") buf.append("_extend((") def _add_localvars_assignments_to_text(self, buf): if not self._localvars_assignments_added: self._localvars_assignments_added = True buf.append(self._localvars_assignments()) def stop_text_part(self, buf): buf.append("));") def _quote_text(self, text): text = re.sub(r"(['\\\\])", r"\\\1", text) text = text.replace("\r\n", "\\r\n") return text def add_text(self, buf, text, encode_newline=False): if not text: return use_unicode = self.encoding and python2 buf.append(use_unicode and "u'''" or "'''") text = self._quote_text(text) if not encode_newline: buf.extend((text, "''', ")) elif text.endswith("\r\n"): buf.extend((text[0:-2], "\\r\\n''', ")) elif text.endswith("\n"): buf.extend((text[0:-1], "\\n''', ")) else: buf.extend((text, "''', ")) _add_text = add_text def add_expr(self, buf, code, *flags): if not code or code.isspace(): return flag_escape, flag_tostr = flags if not self.tostrfunc: flag_tostr = False if not self.escapefunc: flag_escape = False if flag_tostr and flag_escape: s1, s2 = "_escape(_to_str(", ")), " elif flag_tostr: s1, s2 = "_to_str(", "), " elif flag_escape: s1, s2 = "_escape(", "), " else: s1, s2 = "(", "), " buf.extend((s1, code, s2, )) def add_stmt(self, buf, code): if not code: return lines = code.splitlines(True) # keep "\n" if lines[-1][-1] != "\n": lines[-1] = lines[-1] + "\n" buf.extend(lines) self._add_localvars_assignments_to_stmts(buf) def _add_localvars_assignments_to_stmts(self, buf): if self._localvars_assignments_added: return for index, stmt in enumerate(buf): if not re.match(r'^[ \t]*(?:\#|_buf ?= ?\[\]|from __future__)', stmt): break else: return self._localvars_assignments_added = True if re.match(r'^[ \t]*(if|for|while|def|with|class)\b', stmt): buf.insert(index, self._localvars_assignments() + "\n") else: buf[index] = self._localvars_assignments() + buf[index] _START_WORDS = dict.fromkeys( ('for', 'if', 'while', 'def', 'try:', 'with', 'class'), True) _END_WORDS = dict.fromkeys(('#end', '#endfor', '#endif', '#endwhile', '#enddef', '#endtry', '#endwith', '#endclass'), True) _CONT_WORDS = dict.fromkeys( ('elif', 'else:', 'except', 'except:', 'finally:'), True) _WORD_REXP = re.compile(r'\S+') depth = -1 ## # ex. # input = r""" # if items: ## _buf.extend(('\n', )) # endif # """[1:] ## lines = input.splitlines(True) ## block = self.parse_lines(lines) # => [ "if items:\n", # [ "_buf.extend(('\n', ))\n", # ], # "#endif\n", # ] def parse_lines(self, lines): block = [] try: self._parse_lines(lines.__iter__(), False, block, 0) except StopIteration: if self.depth > 0: fname, linenum, colnum, linetext = self.filename, len( lines), None, None raise TemplateSyntaxError( "unexpected EOF.", (fname, linenum, colnum, linetext)) else: pass return block def _parse_lines(self, lines_iter, end_block, block, linenum): if block is None: block = [] _START_WORDS = self._START_WORDS _END_WORDS = self._END_WORDS _CONT_WORDS = self._CONT_WORDS _WORD_REXP = self._WORD_REXP get_line = lines_iter.next while True: line = get_line() linenum += line.count("\n") m = _WORD_REXP.search(line) if not m: block.append(line) continue word = m.group(0) if word in _END_WORDS: if word != end_block and word != '#end': if end_block is False: msg = "'%s' found but corresponding statement is missing." % ( word, ) else: msg = "'%s' expected but got '%s'." % (end_block, word) colnum = m.start() + 1 raise TemplateSyntaxError( msg, (self.filename, linenum, colnum, line)) return block, line, None, linenum elif line.endswith(':\n') or line.endswith(':\r\n'): if word in _CONT_WORDS: return block, line, word, linenum elif word in _START_WORDS: block.append(line) self.depth += 1 cont_word = None try: child_block, line, cont_word, linenum = \ self._parse_lines( lines_iter, '#end'+word, [], linenum) block.extend((child_block, line, )) while cont_word: # 'elif' or 'else:' child_block, line, cont_word, linenum = \ self._parse_lines( lines_iter, '#end'+word, [], linenum) block.extend((child_block, line, )) except StopIteration: msg = "'%s' is not closed." % (cont_word or word) colnum = m.start() + 1 raise TemplateSyntaxError( msg, (self.filename, linenum, colnum, line)) self.depth -= 1 else: block.append(line) else: block.append(line) assert "unreachable" def _join_block(self, block, buf, depth): indent = ' ' * (self.indent * depth) for line in block: if isinstance(line, list): self._join_block(line, buf, depth+1) elif line.isspace(): buf.append(line) else: buf.append(indent + line.lstrip()) def _arrange_indent(self, buf): """arrange indentation of statements in buf""" block = self.parse_lines(buf) buf[:] = [] self._join_block(block, buf, 0) def render(self, context=None, globals=None, _buf=None): """Evaluate python code with context dictionary. If _buf is None then return the result of evaluation as str, else return None. context:dict (=None) Context object to evaluate. If None then new dict is created. globals:dict (=None) Global object. If None then globals() is used. _buf:list (=None) If None then new list is created. """ if context is None: locals = context = {} elif self.args is None: locals = context.copy() else: locals = {} if '_engine' in context: context.get('_engine').hook_context(locals) locals['_context'] = context if globals is None: globals = sys._getframe(1).f_globals bufarg = _buf if _buf is None: _buf = [] locals['_buf'] = _buf if not self.bytecode: self.compile() if self.trace: _buf.append("\n" % self.filename) exec(self.bytecode, globals, locals) _buf.append("\n" % self.filename) else: exec(self.bytecode, globals, locals) if bufarg is not None: return bufarg elif not logger: return ''.join(_buf) else: try: return ''.join(_buf) except UnicodeDecodeError, ex: logger.error("[tenjin.Template] " + str(ex)) logger.error("[tenjin.Template] (_buf=%r)" % (_buf, )) raise def compile(self): """compile self.script into self.bytecode""" self.bytecode = compile( self.script, self.filename or '(tenjin)', 'exec') ## # preprocessor class ## class Preprocessor(Template): """Template class for preprocessing.""" STMT_PATTERN = (r'<\?PY( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?', re.S) EXPR_PATTERN = ( r'#\{\{(.*?)\}\}|\$\{\{(.*?)\}\}|\{#=(?:=(.*?)=|(.*?))=#\}', re.S) def add_expr(self, buf, code, *flags): if not code or code.isspace(): return code = "_decode_params(%s)" % code Template.add_expr(self, buf, code, *flags) class TemplatePreprocessor(object): factory = Preprocessor def __init__(self, factory=None): if factory is not None: self.factory = factory self.globals = sys._getframe(1).f_globals def __call__(self, input, **kwargs): filename = kwargs.get('filename') context = kwargs.get('context') or {} globals = kwargs.get('globals') or self.globals template = self.factory() template.convert(input, filename) return template.render(context, globals=globals) class TrimPreprocessor(object): _rexp = re.compile(r'^[ \t]+<', re.M) _rexp_all = re.compile(r'^[ \t]+', re.M) def __init__(self, all=False): self.all = all def __call__(self, input, **kwargs): if self.all: return self._rexp_all.sub('', input) else: return self._rexp.sub('<', input) class PrefixedLinePreprocessor(object): def __init__(self, prefix='::(?=[ \t]|$)'): self.prefix = prefix self.regexp = re.compile(r'^([ \t]*)' + prefix + r'(.*)', re.M) def convert_prefixed_lines(self, text): def fn(m): return "%s" % (m.group(1), m.group(2)) return self.regexp.sub(fn, text) STMT_REXP = re.compile(r'<\?py\s.*?\?>', re.S) def __call__(self, input, **kwargs): buf = [] append = buf.append pos = 0 for m in self.STMT_REXP.finditer(input): text = input[pos:m.start()] stmt = m.group(0) pos = m.end() if text: append(self.convert_prefixed_lines(text)) append(stmt) rest = input[pos:] if rest: append(self.convert_prefixed_lines(rest)) return "".join(buf) class ParseError(Exception): pass class JavaScriptPreprocessor(object): def __init__(self, **attrs): self._attrs = attrs def __call__(self, input, **kwargs): return self.parse(input, kwargs.get('filename')) def parse(self, input, filename=None): buf = [] self._parse_chunks(input, buf, filename) return ''.join(buf) CHUNK_REXP = re.compile( r'(?:^( *)<|<)!-- *#(?:JS: (\$?\w+(?:\.\w+)*\(.*?\))|/JS:?) *-->([ \t]*\r?\n)?', re.M) def _scan_chunks(self, input, filename): rexp = self.CHUNK_REXP pos = 0 curr_funcdecl = None for m in rexp.finditer(input): lspace, funcdecl, rspace = m.groups() text = input[pos:m.start()] pos = m.end() if funcdecl: if curr_funcdecl: raise ParseError("%s is nested in %s. (file: %s, line: %s)" % (funcdecl, curr_funcdecl, filename, _linenum(input, m.start()), )) curr_funcdecl = funcdecl else: if not curr_funcdecl: raise ParseError("unexpected ''. (file: %s, line: %s)" % (filename, _linenum(input, m.start()), )) curr_funcdecl = None yield text, lspace, funcdecl, rspace, False if curr_funcdecl: raise ParseError("%s is not closed by ''. (file: %s, line: %s)" % (curr_funcdecl, filename, _linenum(input, m.start()), )) rest = input[pos:] yield rest, None, None, None, True def _parse_chunks(self, input, buf, filename=None): if not input: return stag = '([ \t]*\r?\n)?', re.M | re.S) def _scan_stmts(self, input): rexp = self.STMT_REXP pos = 0 for m in rexp.finditer(input): lspace, code, rspace = m.groups() text = input[pos:m.start()] pos = m.end() yield text, lspace, code, rspace, False rest = input[pos:] yield rest, None, None, None, True def _parse_stmts(self, input, buf): if not input: return for text, lspace, code, rspace, end_p in self._scan_stmts(input): if end_p: break if lspace is not None and rspace is not None: self._parse_exprs(text, buf) buf.extend((lspace, code, rspace)) else: if lspace: text += lspace self._parse_exprs(text, buf) buf.append(code) if rspace: self._parse_exprs(rspace, buf) if text: self._parse_exprs(text, buf) s = r'(?:\{[^{}]*?\}[^{}]*?)*' EXPR_REXP = re.compile(r'\{=(.*?)=\}|([$#])\{(.*?' + s + r')\}', re.S) del s def _get_expr(self, m): code1, ch, code2 = m.groups() if ch: code = code2 escape_p = ch == '$' elif code1[0] == code1[-1] == '=': code = code1[1:-1] escape_p = False else: code = code1 escape_p = True return code, escape_p def _scan_exprs(self, input): rexp = self.EXPR_REXP pos = 0 for m in rexp.finditer(input): text = input[pos:m.start()] pos = m.end() code, escape_p = self._get_expr(m) yield text, code, escape_p, False rest = input[pos:] yield rest, None, None, True def _parse_exprs(self, input, buf): if not input: return buf.append("_buf+=") extend = buf.extend op = '' for text, code, escape_p, end_p in self._scan_exprs(input): if end_p: break if text: extend((op, self._escape_text(text))) op = '+' if code: extend((op, escape_p and '_E(' or '_S(', code, ')')) op = '+' rest = text if rest: extend((op, self._escape_text(rest))) if input.endswith("\n"): buf.append(";\n") else: buf.append(";") def _escape_text(self, text): lines = text.splitlines(True) fn = self._escape_str s = "\\\n".join(fn(line) for line in lines) return "".join(("'", s, "'")) def _escape_str(self, string): return string.replace("\\", "\\\\").replace("'", "\\'").replace("\n", r"\n") def _linenum(input, pos): return input[0:pos].count("\n") + 1 JS_FUNC = r""" function _S(x){return x==null?'':x;} function _E(x){return x==null?'':typeof(x)!=='string'?x:x.replace(/[&<>"']/g,_EF);} var _ET={'&':"&",'<':"<",'>':">",'"':""","'":"'"}; function _EF(c){return _ET[c];}; """[1:-1] JS_FUNC = escaped.EscapedStr(JS_FUNC) ## # cache storages ## class CacheStorage(object): """[abstract] Template object cache class (in memory and/or file)""" def __init__(self): self.items = {} # key: full path, value: template object def get(self, cachepath, create_template): """get template object. if not found, load attributes from cache file and restore template object.""" template = self.items.get(cachepath) if not template: dct = self._load(cachepath) if dct: template = create_template() for k in dct: setattr(template, k, dct[k]) self.items[cachepath] = template return template def set(self, cachepath, template): """set template object and save template attributes into cache file.""" self.items[cachepath] = template dct = self._save_data_of(template) return self._store(cachepath, dct) def _save_data_of(self, template): return {'args': template.args, 'bytecode': template.bytecode, 'script': template.script, 'timestamp': template.timestamp} def unset(self, cachepath): """remove template object from dict and cache file.""" self.items.pop(cachepath, None) return self._delete(cachepath) def clear(self): """remove all template objects and attributes from dict and cache file.""" d, self.items = self.items, {} for k in d.iterkeys(): self._delete(k) d.clear() def _load(self, cachepath): """(abstract) load dict object which represents template object attributes from cache file.""" raise NotImplementedError.new( "%s#_load(): not implemented yet." % self.__class__.__name__) def _store(self, cachepath, template): """(abstract) load dict object which represents template object attributes from cache file.""" raise NotImplementedError.new( "%s#_store(): not implemented yet." % self.__class__.__name__) def _delete(self, cachepath): """(abstract) remove template object from cache file.""" raise NotImplementedError.new( "%s#_delete(): not implemented yet." % self.__class__.__name__) class MemoryCacheStorage(CacheStorage): def _load(self, cachepath): return None def _store(self, cachepath, template): pass def _delete(self, cachepath): pass class FileCacheStorage(CacheStorage): def _load(self, cachepath): if not _isfile(cachepath): return None if logger: logger.info("[tenjin.%s] load cache (file=%r)" % (self.__class__.__name__, cachepath)) data = _read_binary_file(cachepath) return self._restore(data) def _store(self, cachepath, dct): if logger: logger.info("[tenjin.%s] store cache (file=%r)" % (self.__class__.__name__, cachepath)) data = self._dump(dct) _write_binary_file(cachepath, data) def _restore(self, data): raise NotImplementedError( "%s._restore(): not implemented yet." % self.__class__.__name__) def _dump(self, dct): raise NotImplementedError( "%s._dump(): not implemented yet." % self.__class__.__name__) def _delete(self, cachepath): _ignore_not_found_error(lambda: os.unlink(cachepath)) class MarshalCacheStorage(FileCacheStorage): def _restore(self, data): return marshal.loads(data) def _dump(self, dct): return marshal.dumps(dct) class PickleCacheStorage(FileCacheStorage): def __init__(self, *args, **kwargs): global pickle if pickle is None: import cPickle as pickle FileCacheStorage.__init__(self, *args, **kwargs) def _restore(self, data): return pickle.loads(data) def _dump(self, dct): dct.pop('bytecode', None) return pickle.dumps(dct) class TextCacheStorage(FileCacheStorage): def _restore(self, data): header, script = data.split("\n\n", 1) timestamp = encoding = args = None for line in header.split("\n"): key, val = line.split(": ", 1) if key == 'timestamp': timestamp = float(val) elif key == 'encoding': encoding = val elif key == 'args': args = val.split(', ') if encoding: script = script.decode(encoding) # binary(=str) to unicode return {'args': args, 'script': script, 'timestamp': timestamp} def _dump(self, dct): s = dct['script'] if dct.get('encoding') and isinstance(s, unicode): s = s.encode(dct['encoding']) # unicode to binary(=str) sb = [] sb.append("timestamp: %s\n" % dct['timestamp']) if dct.get('encoding'): sb.append("encoding: %s\n" % dct['encoding']) if dct.get('args') is not None: sb.append("args: %s\n" % ', '.join(dct['args'])) sb.append("\n") sb.append(s) s = ''.join(sb) if python3: if isinstance(s, str): # unicode(=str) to binary s = s.encode(dct.get('encoding') or 'utf-8') return s def _save_data_of(self, template): dct = FileCacheStorage._save_data_of(self, template) dct['encoding'] = template.encoding return dct ## # abstract class for data cache ## class KeyValueStore(object): def get(self, key, *options): raise NotImplementedError( "%s.get(): not implemented yet." % self.__class__.__name__) def set(self, key, value, *options): raise NotImplementedError( "%s.set(): not implemented yet." % self.__class__.__name__) def delete(self, key, *options): raise NotImplementedError( "%s.del(): not implemented yet." % self.__class__.__name__) def has(self, key, *options): raise NotImplementedError( "%s.has(): not implemented yet." % self.__class__.__name__) ## # memory base data cache ## class MemoryBaseStore(KeyValueStore): def __init__(self): self.values = {} def get(self, key, original_timestamp=None): tupl = self.values.get(key) if not tupl: return None value, created_at, expires_at = tupl if original_timestamp is not None and created_at < original_timestamp: self.delete(key) return None if expires_at < _time(): self.delete(key) return None return value def set(self, key, value, lifetime=0): created_at = _time() expires_at = lifetime and created_at + lifetime or 0 self.values[key] = (value, created_at, expires_at) return True def delete(self, key): try: del self.values[key] return True except KeyError: return False def has(self, key): pair = self.values.get(key) if not pair: return False value, created_at, expires_at = pair if expires_at and expires_at < _time(): self.delete(key) return False return True ## # file base data cache ## class FileBaseStore(KeyValueStore): lifetime = 604800 # = 60*60*24*7 def __init__(self, root_path, encoding=None): if not os.path.isdir(root_path): raise ValueError("%r: directory not found." % (root_path, )) self.root_path = root_path if encoding is None and python3: encoding = 'utf-8' self.encoding = encoding _pat = re.compile(r'[^-.\/\w]') def filepath(self, key, _pat1=_pat): return os.path.join(self.root_path, _pat1.sub('_', key)) def get(self, key, original_timestamp=None): fpath = self.filepath(key) # if not _isfile(fpath): return None stat = _ignore_not_found_error(lambda: os.stat(fpath), None) if stat is None: return None created_at = stat.st_ctime expires_at = stat.st_mtime if original_timestamp is not None and created_at < original_timestamp: self.delete(key) return None if expires_at < _time(): self.delete(key) return None if self.encoding: def f(): return _read_text_file(fpath, self.encoding) else: def f(): return _read_binary_file(fpath) return _ignore_not_found_error(f, None) def set(self, key, value, lifetime=0): fpath = self.filepath(key) dirname = os.path.dirname(fpath) if not os.path.isdir(dirname): os.makedirs(dirname) now = _time() if isinstance(value, _unicode): value = value.encode(self.encoding or 'utf-8') _write_binary_file(fpath, value) expires_at = now + (lifetime or self.lifetime) # timestamp os.utime(fpath, (expires_at, expires_at)) return True def delete(self, key): fpath = self.filepath(key) ret = _ignore_not_found_error(lambda: os.unlink(fpath), False) return ret != False def has(self, key): fpath = self.filepath(key) if not _isfile(fpath): return False if _getmtime(fpath) < _time(): self.delete(key) return False return True ## # html fragment cache helper class ## class FragmentCacheHelper(object): """html fragment cache helper class.""" lifetime = 60 # 1 minute prefix = None def __init__(self, store, lifetime=None, prefix=None): self.store = store if lifetime is not None: self.lifetime = lifetime if prefix is not None: self.prefix = prefix def not_cached(self, cache_key, lifetime=None): """(obsolete. use cache_as() instead of this.) html fragment cache helper. see document of FragmentCacheHelper class.""" context = sys._getframe(1).f_locals['_context'] context['_cache_key'] = cache_key key = self.prefix and self.prefix + cache_key or cache_key value = self.store.get(key) if value: # cached if logger: logger.debug('[tenjin.not_cached] %r: cached.' % (cache_key, )) context[key] = value return False else: # not cached if logger: logger.debug( '[tenjin.not_cached]: %r: not cached.' % (cache_key, )) if key in context: del context[key] if lifetime is None: lifetime = self.lifetime context['_cache_lifetime'] = lifetime helpers.start_capture(cache_key, _depth=2) return True def echo_cached(self): """(obsolete. use cache_as() instead of this.) html fragment cache helper. see document of FragmentCacheHelper class.""" f_locals = sys._getframe(1).f_locals context = f_locals['_context'] cache_key = context.pop('_cache_key') key = self.prefix and self.prefix + cache_key or cache_key if key in context: # cached value = context.pop(key) else: # not cached value = helpers.stop_capture(False, _depth=2) lifetime = context.pop('_cache_lifetime') self.store.set(key, value, lifetime) f_locals['_buf'].append(value) def functions(self): """(obsolete. use cache_as() instead of this.)""" return (self.not_cached, self.echo_cached) def cache_as(self, cache_key, lifetime=None): key = self.prefix and self.prefix + cache_key or cache_key _buf = sys._getframe(1).f_locals['_buf'] value = self.store.get(key) if value: if logger: logger.debug('[tenjin.cache_as] %r: cache found.' % (cache_key, )) _buf.append(value) else: if logger: logger.debug( '[tenjin.cache_as] %r: expired or not cached yet.' % (cache_key, )) _buf_len = len(_buf) yield None value = ''.join(_buf[_buf_len:]) self.store.set(key, value, lifetime) # you can change default store by 'tenjin.helpers.fragment_cache.store = ...' helpers.fragment_cache = FragmentCacheHelper(MemoryBaseStore()) helpers.not_cached = helpers.fragment_cache.not_cached helpers.echo_cached = helpers.fragment_cache.echo_cached helpers.cache_as = helpers.fragment_cache.cache_as helpers.__all__.extend(('not_cached', 'echo_cached', 'cache_as')) ## # helper class to find and read template ## class Loader(object): def exists(self, filepath): raise NotImplementedError( "%s.exists(): not implemented yet." % self.__class__.__name__) def find(self, filename, dirs=None): #: if dirs provided then search template file from it. if dirs: for dirname in dirs: filepath = os.path.join(dirname, filename) if self.exists(filepath): return filepath #: if dirs not provided then just return filename if file exists. else: if self.exists(filename): return filename #: if file not found then return None. return None def abspath(self, filename): raise NotImplementedError( "%s.abspath(): not implemented yet." % self.__class__.__name__) def timestamp(self, filepath): raise NotImplementedError( "%s.timestamp(): not implemented yet." % self.__class__.__name__) def load(self, filepath): raise NotImplementedError( "%s.timestamp(): not implemented yet." % self.__class__.__name__) ## # helper class to find and read files ## class FileSystemLoader(Loader): def exists(self, filepath): #: return True if filepath exists as a file. return os.path.isfile(filepath) def abspath(self, filepath): #: return full-path of filepath return os.path.abspath(filepath) def timestamp(self, filepath): #: return mtime of file return _getmtime(filepath) def load(self, filepath): #: if file exists, return file content and mtime def f(): mtime = _getmtime(filepath) input = _read_template_file(filepath) mtime2 = _getmtime(filepath) if mtime != mtime2: mtime = mtime2 input = _read_template_file(filepath) mtime2 = _getmtime(filepath) if mtime != mtime2: if logger: logger.warn( "[tenjin] %s.load(): timestamp is changed while reading file." % self.__class__.__name__) return input, mtime #: if file not exist, return None return _ignore_not_found_error(f) ## ## ## class TemplateNotFoundError(Exception): pass ## # template engine class ## class Engine(object): """Template Engine class. See User's Guide and examples for details. http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html http://www.kuwata-lab.com/tenjin/pytenjin-examples.html """ # default value of attributes prefix = '' postfix = '' layout = None templateclass = Template path = None cache = TextCacheStorage() # save converted Python code into text file lang = None loader = FileSystemLoader() preprocess = False preprocessorclass = Preprocessor timestamp_interval = 1 # seconds def __init__(self, prefix=None, postfix=None, layout=None, path=None, cache=True, preprocess=None, templateclass=None, preprocessorclass=None, lang=None, loader=None, pp=None, **kwargs): """Initializer of Engine class. prefix:str (='') Prefix string used to convert template short name to template filename. postfix:str (='') Postfix string used to convert template short name to template filename. layout:str (=None) Default layout template name. path:list of str(=None) List of directory names which contain template files. cache:bool or CacheStorage instance (=True) Cache storage object to store converted python code. If True, default cache storage (=Engine.cache) is used (if it is None then create MarshalCacheStorage object for each engine object). If False, no cache storage is used nor no cache files are created. preprocess:bool(=False) Activate preprocessing or not. templateclass:class (=Template) Template class which engine creates automatically. lang:str (=None) Language name such as 'en', 'fr', 'ja', and so on. If you specify this, cache file path will be 'inex.html.en.cache' for example. pp:list (=None) List of preprocessor object which is callable and manipulates template content. kwargs:dict Options for Template class constructor. See document of Template.__init__() for details. """ if prefix: self.prefix = prefix if postfix: self.postfix = postfix if layout: self.layout = layout if templateclass: self.templateclass = templateclass if preprocessorclass: self.preprocessorclass = preprocessorclass if path is not None: self.path = path if lang is not None: self.lang = lang if loader is not None: self.loader = loader if preprocess is not None: self.preprocess = preprocess if pp is None: pp = [] elif isinstance(pp, list): pass elif isinstance(pp, tuple): pp = list(pp) else: raise TypeError("'pp' expected to be a list but got %r." % (pp,)) self.pp = pp if preprocess: self.pp.append(TemplatePreprocessor(self.preprocessorclass)) self.kwargs = kwargs self.encoding = kwargs.get('encoding') self._filepaths = {} # template_name => relative path and absolute path self._added_templates = {} # templates added by add_template() #self.cache = cache self._set_cache_storage(cache) def _set_cache_storage(self, cache): if cache is True: if not self.cache: self.cache = MarshalCacheStorage() elif cache is None: pass elif cache is False: self.cache = None elif isinstance(cache, CacheStorage): self.cache = cache else: raise ValueError("%r: invalid cache object." % (cache, )) def cachename(self, filepath): #: if lang is provided then add it to cache filename. if self.lang: return '%s.%s.cache' % (filepath, self.lang) #: return cache file name. else: return filepath + '.cache' def to_filename(self, template_name): """Convert template short name into filename. ex. >>> engine = tenjin.Engine(prefix='user_', postfix='.pyhtml') >>> engine.to_filename(':list') 'user_list.pyhtml' >>> engine.to_filename('list') 'list' """ #: if template_name starts with ':', add prefix and postfix to it. if template_name[0] == ':': return self.prefix + template_name[1:] + self.postfix #: if template_name doesn't start with ':', just return it. return template_name def _create_template(self, input=None, filepath=None, _context=None, _globals=None): #: if input is not specified then just create empty template object. template = self.templateclass(None, **self.kwargs) #: if input is specified then create template object and return it. if input: template.convert(input, filepath) return template def _preprocess(self, input, filepath, _context, _globals): #if _context is None: _context = {} #if _globals is None: _globals = sys._getframe(3).f_globals #: preprocess template and return result #preprocessor = self.preprocessorclass(filepath, input=input) # return preprocessor.render(_context, globals=_globals) #: preprocesses input with _context and returns result. if '_engine' not in _context: self.hook_context(_context) for pp in self.pp: input = pp.__call__(input, filename=filepath, context=_context, globals=_globals) return input def add_template(self, template): self._added_templates[template.filename] = template def _get_template_from_cache(self, cachepath, filepath): #: if template not found in cache, return None template = self.cache.get(cachepath, self.templateclass) if not template: return None assert template.timestamp is not None #: if checked within a sec, skip timestamp check. now = _time() last_checked = getattr(template, '_last_checked_at', None) if last_checked and now < last_checked + self.timestamp_interval: # if logger: logger.trace('[tenjin.%s] timestamp check skipped (%f < %f + %f)' % \ # (self.__class__.__name__, now, template._last_checked_at, self.timestamp_interval)) return template #: if timestamp of template objectis same as file, return it. if template.timestamp == self.loader.timestamp(filepath): template._last_checked_at = now return template #: if timestamp of template object is different from file, clear it # cache._delete(cachepath) if logger: logger.info("[tenjin.%s] cache expired (filepath=%r)" % (self.__class__.__name__, filepath)) return None def get_template(self, template_name, _context=None, _globals=None): """Return template object. If template object has not registered, template engine creates and registers template object automatically. """ #: accept template_name such as ':index'. filename = self.to_filename(template_name) #: if template object is added by add_template(), return it. if filename in self._added_templates: return self._added_templates[filename] #: get filepath and fullpath of template pair = self._filepaths.get(filename) if pair: filepath, fullpath = pair else: #: if template file is not found then raise TemplateNotFoundError. filepath = self.loader.find(filename, self.path) if not filepath: raise TemplateNotFoundError( '%s: filename not found (path=%r).' % (filename, self.path)) # fullpath = self.loader.abspath(filepath) self._filepaths[filename] = (filepath, fullpath) #: use full path as base of cache file path cachepath = self.cachename(fullpath) #: get template object from cache cache = self.cache template = cache and self._get_template_from_cache( cachepath, filepath) or None #: if template object is not found in cache or is expired... if not template: ret = self.loader.load(filepath) if not ret: raise TemplateNotFoundError( "%r: template not found." % filepath) input, timestamp = ret if self.pp: # required for preprocessing if _context is None: _context = {} if _globals is None: _globals = sys._getframe(1).f_globals input = self._preprocess(input, filepath, _context, _globals) #: create template object. template = self._create_template( input, filepath, _context, _globals) #: set timestamp and filename of template object. template.timestamp = timestamp template._last_checked_at = _time() #: save template object into cache. if cache: if not template.bytecode: #: ignores syntax error when compiling. try: template.compile() except SyntaxError: pass cache.set(cachepath, template) # else: # template.compile() #: template.filename = filepath return template def include(self, template_name, append_to_buf=True, **kwargs): """Evaluate template using current local variables as context. template_name:str Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template. append_to_buf:boolean (=True) If True then append output into _buf and return None, else return stirng output. ex. #{include('file.pyhtml', False)} """ #: get local and global vars of caller. frame = sys._getframe(1) locals = frame.f_locals globals = frame.f_globals #: get _context from caller's local vars. assert '_context' in locals context = locals['_context'] #: if kwargs specified then add them into context. if kwargs: context.update(kwargs) #: get template object with context data and global vars. # (context and globals are passed to get_template() only for preprocessing.) template = self.get_template(template_name, context, globals) #: if append_to_buf is true then add output to _buf. #: if append_to_buf is false then don't add output to _buf. if append_to_buf: _buf = locals['_buf'] else: _buf = None #: render template and return output. s = template.render(context, globals, _buf=_buf) #: kwargs are removed from context data. if kwargs: for k in kwargs: del context[k] return s def render(self, template_name, context=None, globals=None, layout=True): """Evaluate template with layout file and return result of evaluation. template_name:str Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template. context:dict (=None) Context object to evaluate. If None then new dict is used. globals:dict (=None) Global context to evaluate. If None then globals() is used. layout:str or Bool(=True) If True, the default layout name specified in constructor is used. If False, no layout template is used. If str, it is regarded as layout template name. If temlate object related with the 'template_name' argument is not exist, engine generates a template object and register it automatically. """ if context is None: context = {} if globals is None: globals = sys._getframe(1).f_globals self.hook_context(context) while True: # context and globals are passed to get_template() only for preprocessing template = self.get_template(template_name, context, globals) content = template.render(context, globals) layout = context.pop('_layout', layout) if layout is True or layout is None: layout = self.layout if not layout: break template_name = layout layout = False context['_content'] = content context.pop('_content', None) return content def hook_context(self, context): #: add engine itself into context data. context['_engine'] = self #context['render'] = self.render #: add include() method into context data. context['include'] = self.include ## # safe template and engine ## class SafeTemplate(Template): """Uses 'to_escaped()' instead of 'escape()'. '#{...}' is not allowed with this class. Use '[==...==]' instead. """ tostrfunc = 'to_str' escapefunc = 'to_escaped' def get_expr_and_flags(self, match): return _get_expr_and_flags(match, "#{%s}: '#{}' is not allowed with SafeTemplate.") class SafePreprocessor(Preprocessor): tostrfunc = 'to_str' escapefunc = 'to_escaped' def get_expr_and_flags(self, match): return _get_expr_and_flags(match, "#{{%s}}: '#{{}}' is not allowed with SafePreprocessor.") def _get_expr_and_flags(match, errmsg): expr1, expr2, expr3, expr4 = match.groups() if expr1 is not None: raise TemplateSyntaxError(errmsg % match.group(1)) if expr2 is not None: return expr2, (True, False) # #{...} : call escape, not to_str if expr3 is not None: return expr3, (False, True) # [==...==] : not escape, call to_str if expr4 is not None: return expr4, (True, False) # [=...=] : call escape, not to_str class SafeEngine(Engine): templateclass = SafeTemplate preprocessorclass = SafePreprocessor ## # for Google App Engine # (should separate into individual file or module?) ## def _dummy(): global memcache, _tenjin memcache = _tenjin = None # lazy import of google.appengine.api.memcache global GaeMemcacheCacheStorage, GaeMemcacheStore, init class GaeMemcacheCacheStorage(CacheStorage): lifetime = 0 # 0 means unlimited def __init__(self, lifetime=None, namespace=None): CacheStorage.__init__(self) if lifetime is not None: self.lifetime = lifetime self.namespace = namespace def _load(self, cachepath): key = cachepath if _tenjin.logger: _tenjin.logger.info( "[tenjin.gae.GaeMemcacheCacheStorage] load cache (key=%r)" % (key, )) return memcache.get(key, namespace=self.namespace) def _store(self, cachepath, dct): dct.pop('bytecode', None) key = cachepath if _tenjin.logger: _tenjin.logger.info( "[tenjin.gae.GaeMemcacheCacheStorage] store cache (key=%r)" % (key, )) ret = memcache.set(key, dct, self.lifetime, namespace=self.namespace) if not ret: if _tenjin.logger: _tenjin.logger.info( "[tenjin.gae.GaeMemcacheCacheStorage] failed to store cache (key=%r)" % (key, )) def _delete(self, cachepath): key = cachepath memcache.delete(key, namespace=self.namespace) class GaeMemcacheStore(KeyValueStore): lifetime = 0 def __init__(self, lifetime=None, namespace=None): if lifetime is not None: self.lifetime = lifetime self.namespace = namespace def get(self, key): return memcache.get(key, namespace=self.namespace) def set(self, key, value, lifetime=None): if lifetime is None: lifetime = self.lifetime if memcache.set(key, value, lifetime, namespace=self.namespace): return True else: if _tenjin.logger: _tenjin.logger.info( "[tenjin.gae.GaeMemcacheStore] failed to set (key=%r)" % (key, )) return False def delete(self, key): return memcache.delete(key, namespace=self.namespace) def has(self, key): if memcache.add(key, 'dummy', namespace=self.namespace): memcache.delete(key, namespace=self.namespace) return False else: return True def init(): global memcache, _tenjin if not memcache: from google.appengine.api import memcache if not _tenjin: import tenjin as _tenjin # avoid cache confliction between versions ver = os.environ.get('CURRENT_VERSION_ID', '1.1') # .split('.')[0] Engine.cache = GaeMemcacheCacheStorage(namespace=ver) # set fragment cache store helpers.fragment_cache.store = GaeMemcacheStore(namespace=ver) helpers.fragment_cache.lifetime = 60 # 1 minute helpers.fragment_cache.prefix = 'fragment.' gae = create_module('tenjin.gae', _dummy, os=os, helpers=helpers, Engine=Engine, CacheStorage=CacheStorage, KeyValueStore=KeyValueStore) del _dummy