blob: db8cddee77ed9ab6e09505a415b76016c1032655 [file] [log] [blame]
##
## $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