aboutsummaryrefslogtreecommitdiff
path: root/cgi/tenjin.py
diff options
context:
space:
mode:
Diffstat (limited to 'cgi/tenjin.py')
-rw-r--r--cgi/tenjin.py2118
1 files changed, 2118 insertions, 0 deletions
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. '<b>%s</b>' % _P("item['id']") => "<b>${item['id']}</b>" """
+ 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('&lt;', '<').replace('&gt;', '>').replace('&quot;', '"').replace('&#039;', "'").replace('&amp;', '&')
+ 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'&lt;`#(.*?)#`&gt;', lambda m: '#{%s}' % unescape(m.group(1)), s)
+ s = re.sub(r'&lt;`\$(.*?)\$`&gt;', 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
+ #_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('&', '&amp;')
+ # s = s.replace('<', '&lt;')
+ # s = s.replace('>', '&gt;')
+ # s = s.replace('"', '&quot;')
+ # return s # 5.83
+
+ def escape_html(s):
+ """Escape '&', '<', '>', '"' into '&amp;', '&lt;', '&gt;', '&quot;'."""
+ return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;') # 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 "<br />\n" and return it."""
+ if not text:
+ return _escaped.as_escaped('')
+ return _escaped.as_escaped(text.replace('\n', '<br />\n'))
+
+ def text2html(text, use_nbsp=True):
+ """(experimental) escape xml characters, replace "\n" to "<br />\n", and return it."""
+ if not text:
+ return _escaped.as_escaped('')
+ s = _escaped.to_escaped(text)
+ if use_nbsp: s = s.replace(' ', ' &nbsp;')
+ #return nl2br(s)
+ s = s.replace('\n', '<br />\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 = '<a href="javascript:undefined" onclick="%s;return false"%s>%s</a>' % \
+ (_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 '<!-- begin: file -->' and '<!-- end: file -->' 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 "<div>\\n#{_context}\\n</div>" is parsed as
+ "<div>\\n#{_context}</div>".
+ """
+ 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('<?py') and "\n" or "; "
+ buf.append(self.preamble + eol)
+
+ def after_convert(self, buf):
+ if self.postamble:
+ if buf and not buf[-1].endswith("\n"):
+ buf.append("\n")
+ buf.append(self.postamble + "\n")
+
+ def convert_file(self, filename):
+ """Convert file into python script and return it.
+ This is equivarent to convert(open(filename).read(), filename).
+ """
+ input = _read_template_file(filename)
+ return self.convert(input, filename)
+
+ def convert(self, input, filename=None):
+ """Convert string in which python code is embedded into python script and return it.
+
+ input:str
+ Input string to convert into python code.
+ filename:str (=None)
+ Filename of input. this is optional but recommended to report errors.
+ """
+ if self.encoding and isinstance(input, str):
+ input = input.decode(self.encoding)
+ self._reset(input, filename)
+ buf = []
+ self.before_convert(buf)
+ self.parse_stmts(buf, input)
+ self.after_convert(buf)
+ script = ''.join(buf)
+ self.script = script
+ return script
+
+ STMT_PATTERN = (r'<\?py( |\t|\r?\n)(.*?) ?\?>([ \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(('<ul>\n', ))
+ ## i = 0
+ ## for item in items:
+ ## i += 1
+ ## _buf.extend(('<li>', to_str(item), '</li>\n', ))
+ ## #endfor
+ ## _buf.extend(('</ul>\n', ))
+ ## #endif
+ ## """[1:]
+ ## lines = input.splitlines(True)
+ ## block = self.parse_lines(lines)
+ ## #=> [ "if items:\n",
+ ## [ "_buf.extend(('<ul>\n', ))\n",
+ ## "i = 0\n",
+ ## "for item in items:\n",
+ ## [ "i += 1\n",
+ ## "_buf.extend(('<li>', to_str(item), '</li>\n', ))\n",
+ ## ],
+ ## "#endfor\n",
+ ## "_buf.extend(('</ul>\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("<!-- ***** begin: %s ***** -->\n" % self.filename)
+ exec(self.bytecode, globals, locals)
+ _buf.append("<!-- ***** end: %s ***** -->\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<?py%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 '<!-- #/JS -->'. (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 '<!-- #/JS -->'. (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 = '<script'
+ if self._attrs:
+ for k in self._attrs:
+ stag = "".join((stag, ' ', k, '="', self._attrs[k], '"'))
+ stag += '>'
+ etag = '</script>'
+ for text, lspace, funcdecl, rspace, end_p in self._scan_chunks(input, filename):
+ if end_p: break
+ if funcdecl:
+ buf.append(text)
+ if re.match(r'^\$?\w+\(', funcdecl):
+ buf.extend((lspace or '', stag, 'function ', funcdecl, "{var _buf='';", rspace or ''))
+ else:
+ m = re.match(r'(.+?)\((.*)\)', funcdecl)
+ buf.extend((lspace or '', stag, m.group(1), '=function(', m.group(2), "){var _buf='';", rspace or ''))
+ else:
+ self._parse_stmts(text, buf)
+ buf.extend((lspace or '', "return _buf;};", etag, rspace or ''))
+ #
+ buf.append(text)
+
+ STMT_REXP = re.compile(r'(?:^( *)<|<)\?js(\s.*?) ?\?>([ \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={'&':"&amp;",'<':"&lt;",'>':"&gt;",'"':"&quot;","'":"&#039;"};
+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.
+ <?py include('file.pyhtml') ?>
+ #{include('file.pyhtml', False)}
+ <?py val = 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