From 95dfe14528663923ca2a88ec928f1d8d9df2402b Mon Sep 17 00:00:00 2001 From: bai Date: Fri, 29 Mar 2019 02:14:43 +0000 Subject: Init --- cgi/tenjin.py | 2118 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2118 insertions(+) create mode 100644 cgi/tenjin.py (limited to 'cgi/tenjin.py') diff --git a/cgi/tenjin.py b/cgi/tenjin.py new file mode 100644 index 0000000..db8cdde --- /dev/null +++ b/cgi/tenjin.py @@ -0,0 +1,2118 @@ +## +## $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', ) + + +import sys, os, re, time, marshal +from time import time as _time +from os.path import getmtime as _getmtime +from os.path import isfile as _isfile +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: + os.remove(filename) # on windows, existing file should be removed before renaming + 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__ + __all__ = ('is_escaped', 'as_escaped', 'to_escaped', ) #'Escaped', 'EscapedStr', + + 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) + self.timestamp = False # False means 'file not exist' (= Engine should not check timestamp of file) + 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): + fn = lambda m: "%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): + s = s.encode(dct.get('encoding') or 'utf-8') ## unicode(=str) to binary + 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: + f = lambda: _read_text_file(fpath, self.encoding) + else: + f = lambda: _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 -- cgit v1.2.1-18-gbd029