diff options
Diffstat (limited to 'cgi/tenjin.py')
-rw-r--r-- | cgi/tenjin.py | 2293 |
1 files changed, 0 insertions, 2293 deletions
diff --git a/cgi/tenjin.py b/cgi/tenjin.py deleted file mode 100644 index ddc12bb..0000000 --- a/cgi/tenjin.py +++ /dev/null @@ -1,2293 +0,0 @@ -## -# $Release: 1.1.1 $ -# $Copyright: copyright(c) 2007-2012 kuwata-lab.com all rights reserved. $ -# $License: MIT License $ -## -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -## -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -## -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## - -"""Very fast and light-weight template engine based embedded Python. - See User's Guide and examples for details. - http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html - http://www.kuwata-lab.com/tenjin/pytenjin-examples.html -""" - -__version__ = "$Release: 1.1.1 $"[10:-2] -__license__ = "$License: MIT License $"[10:-2] -__all__ = ('Template', 'Engine', ) - - -from os.path import isfile as _isfile -from os.path import getmtime as _getmtime -from time import time as _time -import sys -import os -import re -import time -import marshal -random = pickle = unquote = None # lazy import -python3 = sys.version_info[0] == 3 -python2 = sys.version_info[0] == 2 - -logger = None - - -## -# utilities -## - -def _write_binary_file(filename, content): - global random - if random is None: - from random import random - tmpfile = filename + str(random())[1:] - f = open(tmpfile, 'w+b') # on windows, 'w+b' is preffered than 'wb' - try: - f.write(content) - finally: - f.close() - if os.path.exists(tmpfile): - try: - os.rename(tmpfile, filename) - except: - # on windows, existing file should be removed before renaming - os.remove(filename) - os.rename(tmpfile, filename) - - -def _read_binary_file(filename): - f = open(filename, 'rb') - try: - return f.read() - finally: - f.close() - - -codecs = None # lazy import - - -def _read_text_file(filename, encoding=None): - global codecs - if not codecs: - import codecs - f = codecs.open(filename, encoding=(encoding or 'utf-8')) - try: - return f.read() - finally: - f.close() - - -def _read_template_file(filename, encoding=None): - s = _read_binary_file(filename) # binary(=str) - if encoding: - s = s.decode(encoding) # binary(=str) to unicode - return s - - -_basestring = basestring -_unicode = unicode -_bytes = str - - -def _ignore_not_found_error(f, default=None): - try: - return f() - except OSError, ex: - if ex.errno == 2: # error: No such file or directory - return default - raise - - -def create_module(module_name, dummy_func=None, **kwargs): - """ex. mod = create_module('tenjin.util')""" - try: - mod = type(sys)(module_name) - except: - # The module creation above does not work for Jython 2.5.2 - import imp - mod = imp.new_module(module_name) - - mod.__file__ = __file__ - mod.__dict__.update(kwargs) - sys.modules[module_name] = mod - if dummy_func: - exec(dummy_func.func_code, mod.__dict__) - return mod - - -def _raise(exception_class, *args): - raise exception_class(*args) - - -## -# helper method's module -## - -def _dummy(): - global unquote - unquote = None - global to_str, escape, echo, new_cycle, generate_tostrfunc - global start_capture, stop_capture, capture_as, captured_as, CaptureContext - global _p, _P, _decode_params - - def generate_tostrfunc(encode=None, decode=None): - """Generate 'to_str' function with encode or decode encoding. - ex. generate to_str() function which encodes unicode into binary(=str). - to_str = tenjin.generate_tostrfunc(encode='utf-8') - repr(to_str(u'hoge')) #=> 'hoge' (str) - ex. generate to_str() function which decodes binary(=str) into unicode. - to_str = tenjin.generate_tostrfunc(decode='utf-8') - repr(to_str('hoge')) #=> u'hoge' (unicode) - """ - if encode: - if decode: - raise ValueError( - "can't specify both encode and decode encoding.") - else: - def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _encode=encode): - """Convert val into string or return '' if None. Unicode will be encoded into binary(=str).""" - if _isa(val, _str): - return val - if val is None: - return '' - # if _isa(val, _unicode): return val.encode(_encode) # unicode to binary(=str) - if _isa(val, _unicode): - return val.encode(_encode) # unicode to binary(=str) - return _str(val) - else: - if decode: - def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _decode=decode): - """Convert val into string or return '' if None. Binary(=str) will be decoded into unicode.""" - # if _isa(val, _str): return val.decode(_decode) # binary(=str) to unicode - if _isa(val, _str): - return val.decode(_decode) - if val is None: - return '' - if _isa(val, _unicode): - return val - return _unicode(val) - else: - def to_str(val, _str=str, _unicode=unicode, _isa=isinstance): - """Convert val into string or return '' if None. Both binary(=str) and unicode will be retruned as-is.""" - if _isa(val, _str): - return val - if val is None: - return '' - if _isa(val, _unicode): - return val - return _str(val) - return to_str - - to_str = generate_tostrfunc(encode='utf-8') # or encode=None? - - def echo(string): - """add string value into _buf. this is equivarent to '#{string}'.""" - lvars = sys._getframe(1).f_locals # local variables - lvars['_buf'].append(string) - - def new_cycle(*values): - """Generate cycle object. - ex. - cycle = new_cycle('odd', 'even') - print(cycle()) #=> 'odd' - print(cycle()) #=> 'even' - print(cycle()) #=> 'odd' - print(cycle()) #=> 'even' - """ - def gen(values): - i, n = 0, len(values) - while True: - yield values[i] - i = (i + 1) % n - return gen(values).next - - class CaptureContext(object): - - def __init__(self, name, store_to_context=True, lvars=None): - self.name = name - self.store_to_context = store_to_context - self.lvars = lvars or sys._getframe(1).f_locals - - def __enter__(self): - lvars = self.lvars - self._buf_orig = lvars['_buf'] - lvars['_buf'] = _buf = [] - lvars['_extend'] = _buf.extend - return self - - def __exit__(self, *args): - lvars = self.lvars - _buf = lvars['_buf'] - lvars['_buf'] = self._buf_orig - lvars['_extend'] = self._buf_orig.extend - lvars[self.name] = self.captured = ''.join(_buf) - if self.store_to_context and '_context' in lvars: - lvars['_context'][self.name] = self.captured - - def __iter__(self): - self.__enter__() - yield self - self.__exit__() - - def start_capture(varname=None, _depth=1): - """(obsolete) start capturing with name.""" - lvars = sys._getframe(_depth).f_locals - capture_context = CaptureContext(varname, None, lvars) - lvars['_capture_context'] = capture_context - capture_context.__enter__() - - def stop_capture(store_to_context=True, _depth=1): - """(obsolete) stop capturing and return the result of capturing. - if store_to_context is True then the result is stored into _context[varname]. - """ - lvars = sys._getframe(_depth).f_locals - capture_context = lvars.pop('_capture_context', None) - if not capture_context: - raise Exception( - 'stop_capture(): start_capture() is not called before.') - capture_context.store_to_context = store_to_context - capture_context.__exit__() - return capture_context.captured - - def capture_as(name, store_to_context=True): - """capture partial of template.""" - return CaptureContext(name, store_to_context, sys._getframe(1).f_locals) - - def captured_as(name, _depth=1): - """helper method for layout template. - if captured string is found then append it to _buf and return True, - else return False. - """ - lvars = sys._getframe(_depth).f_locals # local variables - if name in lvars: - _buf = lvars['_buf'] - _buf.append(lvars[name]) - return True - return False - - def _p(arg): - """ex. '/show/'+_p("item['id']") => "/show/#{item['id']}" """ - return '<`#%s#`>' % arg # decoded into #{...} by preprocessor - - def _P(arg): - """ex. '<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('<', '<').replace('>', '>').replace('"', '"').replace(''', "'").replace('&', '&') - return re.sub(r'&(lt|gt|quot|amp|#039);', lambda m: dct[m.group(1)], s) - s = to_str(s) - s = re.sub(r'%3C%60%23(.*?)%23%60%3E', - lambda m: '#{%s}' % unquote(m.group(1)), s) - s = re.sub(r'%3C%60%24(.*?)%24%60%3E', - lambda m: '${%s}' % unquote(m.group(1)), s) - s = re.sub(r'<`#(.*?)#`>', - lambda m: '#{%s}' % unescape(m.group(1)), s) - s = re.sub(r'<`\$(.*?)\$`>', - lambda m: '${%s}' % unescape(m.group(1)), s) - s = re.sub(r'<`#(.*?)#`>', r'#{\1}', s) - s = re.sub(r'<`\$(.*?)\$`>', r'${\1}', s) - return s - - -helpers = create_module('tenjin.helpers', _dummy, sys=sys, re=re) -helpers.__all__ = ['to_str', 'escape', 'echo', 'new_cycle', 'generate_tostrfunc', - 'start_capture', 'stop_capture', 'capture_as', 'captured_as', - 'not_cached', 'echo_cached', 'cache_as', - '_p', '_P', '_decode_params', - ] -generate_tostrfunc = helpers.generate_tostrfunc - - -## -# escaped module -## -def _dummy(): - global is_escaped, as_escaped, to_escaped - global Escaped, EscapedStr, EscapedUnicode - global __all__ - # 'Escaped', 'EscapedStr', - __all__ = ('is_escaped', 'as_escaped', 'to_escaped', ) - - class Escaped(object): - """marking class that object is already escaped.""" - pass - - def is_escaped(value): - """return True if value is marked as escaped, else return False.""" - return isinstance(value, Escaped) - - class EscapedStr(str, Escaped): - """string class which is marked as escaped.""" - pass - - class EscapedUnicode(unicode, Escaped): - """unicode class which is marked as escaped.""" - pass - - def as_escaped(s): - """mark string as escaped, without escaping.""" - if isinstance(s, str): - return EscapedStr(s) - if isinstance(s, unicode): - return EscapedUnicode(s) - raise TypeError("as_escaped(%r): expected str or unicode." % (s, )) - - def to_escaped(value): - """convert any value into string and escape it. - if value is already marked as escaped, don't escape it.""" - if hasattr(value, '__html__'): - value = value.__html__() - if is_escaped(value): - # return value # EscapedUnicode should be convered into EscapedStr - return as_escaped(_helpers.to_str(value)) - # if isinstance(value, _basestring): - # return as_escaped(_helpers.escape(value)) - return as_escaped(_helpers.escape(_helpers.to_str(value))) - - -escaped = create_module('tenjin.escaped', _dummy, _helpers=helpers) - - -## -# module for html -## -def _dummy(): - global escape_html, escape_xml, escape, tagattr, tagattrs, _normalize_attrs - global checked, selected, disabled, nl2br, text2html, nv, js_link - - # _escape_table = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } - #_escape_pattern = re.compile(r'[&<>"]') - ##_escape_callable = lambda m: _escape_table[m.group(0)] - ##_escape_callable = lambda m: _escape_table.__get__(m.group(0)) - #_escape_get = _escape_table.__getitem__ - #_escape_callable = lambda m: _escape_get(m.group(0)) - #_escape_sub = _escape_pattern.sub - - # def escape_html(s): - # return s # 3.02 - - # def escape_html(s): - # return _escape_pattern.sub(_escape_callable, s) # 6.31 - - # def escape_html(s): - # return _escape_sub(_escape_callable, s) # 6.01 - - # def escape_html(s, _p=_escape_pattern, _f=_escape_callable): - # return _p.sub(_f, s) # 6.27 - - # def escape_html(s, _sub=_escape_pattern.sub, _callable=_escape_callable): - # return _sub(_callable, s) # 6.04 - - # def escape_html(s): - # s = s.replace('&', '&') - # s = s.replace('<', '<') - # s = s.replace('>', '>') - # s = s.replace('"', '"') - # return s # 5.83 - - def escape_html(s): - """Escape '&', '<', '>', '"' into '&', '<', '>', '"'.""" - return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') # 5.72 - - escape_xml = escape_html # for backward compatibility - - def tagattr(name, expr, value=None, escape=True): - """(experimental) Return ' name="value"' if expr is true value, else '' (empty string). - If value is not specified, expr is used as value instead.""" - if not expr and expr != 0: - return _escaped.as_escaped('') - if value is None: - value = expr - if escape: - value = _escaped.to_escaped(value) - return _escaped.as_escaped(' %s="%s"' % (name, value)) - - def tagattrs(**kwargs): - """(experimental) built html tag attribtes. - ex. - >>> tagattrs(klass='main', size=20) - ' class="main" size="20"' - >>> tagattrs(klass='', size=0) - '' - """ - kwargs = _normalize_attrs(kwargs) - esc = _escaped.to_escaped - s = ''.join([' %s="%s"' % (k, esc(v)) - for k, v in kwargs.iteritems() if v or v == 0]) - return _escaped.as_escaped(s) - - def _normalize_attrs(kwargs): - if 'klass' in kwargs: - kwargs['class'] = kwargs.pop('klass') - if 'checked' in kwargs: - kwargs['checked'] = kwargs.pop('checked') and 'checked' or None - if 'selected' in kwargs: - kwargs['selected'] = kwargs.pop('selected') and 'selected' or None - if 'disabled' in kwargs: - kwargs['disabled'] = kwargs.pop('disabled') and 'disabled' or None - return kwargs - - def checked(expr): - """return ' checked="checked"' if expr is true.""" - return _escaped.as_escaped(expr and ' checked="checked"' or '') - - def selected(expr): - """return ' selected="selected"' if expr is true.""" - return _escaped.as_escaped(expr and ' selected="selected"' or '') - - def disabled(expr): - """return ' disabled="disabled"' if expr is true.""" - return _escaped.as_escaped(expr and ' disabled="disabled"' or '') - - def nl2br(text): - """replace "\n" to "<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(' ', ' ') - # 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) - # False means 'file not exist' (= Engine should not check timestamp of file) - self.timestamp = False - elif filename: - self.convert_file(filename) - else: - self._reset() - - def _reset(self, input=None, filename=None): - self.script = None - self.bytecode = None - self.input = input - self.filename = filename - if input != None: - i = input.find("\n") - if i < 0: - self.newline = "\n" # or None - elif len(input) >= 2 and input[i-1] == "\r": - self.newline = "\r\n" - else: - self.newline = "\n" - self._localvars_assignments_added = False - - def _localvars_assignments(self): - return "_extend=_buf.extend;_to_str=%s;_escape=%s; " % (self.tostrfunc, self.escapefunc) - - def before_convert(self, buf): - if self.preamble: - eol = self.input.startswith('<?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): - def fn(m): return "%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={'&':"&",'<':"<",'>':">",'"':""","'":"'"}; -function _EF(c){return _ET[c];}; -"""[1:-1] -JS_FUNC = escaped.EscapedStr(JS_FUNC) - - -## -# cache storages -## - -class CacheStorage(object): - """[abstract] Template object cache class (in memory and/or file)""" - - def __init__(self): - self.items = {} # key: full path, value: template object - - def get(self, cachepath, create_template): - """get template object. if not found, load attributes from cache file and restore template object.""" - template = self.items.get(cachepath) - if not template: - dct = self._load(cachepath) - if dct: - template = create_template() - for k in dct: - setattr(template, k, dct[k]) - self.items[cachepath] = template - return template - - def set(self, cachepath, template): - """set template object and save template attributes into cache file.""" - self.items[cachepath] = template - dct = self._save_data_of(template) - return self._store(cachepath, dct) - - def _save_data_of(self, template): - return {'args': template.args, 'bytecode': template.bytecode, - 'script': template.script, 'timestamp': template.timestamp} - - def unset(self, cachepath): - """remove template object from dict and cache file.""" - self.items.pop(cachepath, None) - return self._delete(cachepath) - - def clear(self): - """remove all template objects and attributes from dict and cache file.""" - d, self.items = self.items, {} - for k in d.iterkeys(): - self._delete(k) - d.clear() - - def _load(self, cachepath): - """(abstract) load dict object which represents template object attributes from cache file.""" - raise NotImplementedError.new( - "%s#_load(): not implemented yet." % self.__class__.__name__) - - def _store(self, cachepath, template): - """(abstract) load dict object which represents template object attributes from cache file.""" - raise NotImplementedError.new( - "%s#_store(): not implemented yet." % self.__class__.__name__) - - def _delete(self, cachepath): - """(abstract) remove template object from cache file.""" - raise NotImplementedError.new( - "%s#_delete(): not implemented yet." % self.__class__.__name__) - - -class MemoryCacheStorage(CacheStorage): - - def _load(self, cachepath): - return None - - def _store(self, cachepath, template): - pass - - def _delete(self, cachepath): - pass - - -class FileCacheStorage(CacheStorage): - - def _load(self, cachepath): - if not _isfile(cachepath): - return None - if logger: - logger.info("[tenjin.%s] load cache (file=%r)" % - (self.__class__.__name__, cachepath)) - data = _read_binary_file(cachepath) - return self._restore(data) - - def _store(self, cachepath, dct): - if logger: - logger.info("[tenjin.%s] store cache (file=%r)" % - (self.__class__.__name__, cachepath)) - data = self._dump(dct) - _write_binary_file(cachepath, data) - - def _restore(self, data): - raise NotImplementedError( - "%s._restore(): not implemented yet." % self.__class__.__name__) - - def _dump(self, dct): - raise NotImplementedError( - "%s._dump(): not implemented yet." % self.__class__.__name__) - - def _delete(self, cachepath): - _ignore_not_found_error(lambda: os.unlink(cachepath)) - - -class MarshalCacheStorage(FileCacheStorage): - - def _restore(self, data): - return marshal.loads(data) - - def _dump(self, dct): - return marshal.dumps(dct) - - -class PickleCacheStorage(FileCacheStorage): - - def __init__(self, *args, **kwargs): - global pickle - if pickle is None: - import cPickle as pickle - FileCacheStorage.__init__(self, *args, **kwargs) - - def _restore(self, data): - return pickle.loads(data) - - def _dump(self, dct): - dct.pop('bytecode', None) - return pickle.dumps(dct) - - -class TextCacheStorage(FileCacheStorage): - - def _restore(self, data): - header, script = data.split("\n\n", 1) - timestamp = encoding = args = None - for line in header.split("\n"): - key, val = line.split(": ", 1) - if key == 'timestamp': - timestamp = float(val) - elif key == 'encoding': - encoding = val - elif key == 'args': - args = val.split(', ') - if encoding: - script = script.decode(encoding) # binary(=str) to unicode - return {'args': args, 'script': script, 'timestamp': timestamp} - - def _dump(self, dct): - s = dct['script'] - if dct.get('encoding') and isinstance(s, unicode): - s = s.encode(dct['encoding']) # unicode to binary(=str) - sb = [] - sb.append("timestamp: %s\n" % dct['timestamp']) - if dct.get('encoding'): - sb.append("encoding: %s\n" % dct['encoding']) - if dct.get('args') is not None: - sb.append("args: %s\n" % ', '.join(dct['args'])) - sb.append("\n") - sb.append(s) - s = ''.join(sb) - if python3: - if isinstance(s, str): - # unicode(=str) to binary - s = s.encode(dct.get('encoding') or 'utf-8') - return s - - def _save_data_of(self, template): - dct = FileCacheStorage._save_data_of(self, template) - dct['encoding'] = template.encoding - return dct - - -## -# abstract class for data cache -## -class KeyValueStore(object): - - def get(self, key, *options): - raise NotImplementedError( - "%s.get(): not implemented yet." % self.__class__.__name__) - - def set(self, key, value, *options): - raise NotImplementedError( - "%s.set(): not implemented yet." % self.__class__.__name__) - - def delete(self, key, *options): - raise NotImplementedError( - "%s.del(): not implemented yet." % self.__class__.__name__) - - def has(self, key, *options): - raise NotImplementedError( - "%s.has(): not implemented yet." % self.__class__.__name__) - - -## -# memory base data cache -## -class MemoryBaseStore(KeyValueStore): - - def __init__(self): - self.values = {} - - def get(self, key, original_timestamp=None): - tupl = self.values.get(key) - if not tupl: - return None - value, created_at, expires_at = tupl - if original_timestamp is not None and created_at < original_timestamp: - self.delete(key) - return None - if expires_at < _time(): - self.delete(key) - return None - return value - - def set(self, key, value, lifetime=0): - created_at = _time() - expires_at = lifetime and created_at + lifetime or 0 - self.values[key] = (value, created_at, expires_at) - return True - - def delete(self, key): - try: - del self.values[key] - return True - except KeyError: - return False - - def has(self, key): - pair = self.values.get(key) - if not pair: - return False - value, created_at, expires_at = pair - if expires_at and expires_at < _time(): - self.delete(key) - return False - return True - - -## -# file base data cache -## -class FileBaseStore(KeyValueStore): - - lifetime = 604800 # = 60*60*24*7 - - def __init__(self, root_path, encoding=None): - if not os.path.isdir(root_path): - raise ValueError("%r: directory not found." % (root_path, )) - self.root_path = root_path - if encoding is None and python3: - encoding = 'utf-8' - self.encoding = encoding - - _pat = re.compile(r'[^-.\/\w]') - - def filepath(self, key, _pat1=_pat): - return os.path.join(self.root_path, _pat1.sub('_', key)) - - def get(self, key, original_timestamp=None): - fpath = self.filepath(key) - # if not _isfile(fpath): return None - stat = _ignore_not_found_error(lambda: os.stat(fpath), None) - if stat is None: - return None - created_at = stat.st_ctime - expires_at = stat.st_mtime - if original_timestamp is not None and created_at < original_timestamp: - self.delete(key) - return None - if expires_at < _time(): - self.delete(key) - return None - if self.encoding: - def f(): return _read_text_file(fpath, self.encoding) - else: - def f(): return _read_binary_file(fpath) - return _ignore_not_found_error(f, None) - - def set(self, key, value, lifetime=0): - fpath = self.filepath(key) - dirname = os.path.dirname(fpath) - if not os.path.isdir(dirname): - os.makedirs(dirname) - now = _time() - if isinstance(value, _unicode): - value = value.encode(self.encoding or 'utf-8') - _write_binary_file(fpath, value) - expires_at = now + (lifetime or self.lifetime) # timestamp - os.utime(fpath, (expires_at, expires_at)) - return True - - def delete(self, key): - fpath = self.filepath(key) - ret = _ignore_not_found_error(lambda: os.unlink(fpath), False) - return ret != False - - def has(self, key): - fpath = self.filepath(key) - if not _isfile(fpath): - return False - if _getmtime(fpath) < _time(): - self.delete(key) - return False - return True - - -## -# html fragment cache helper class -## -class FragmentCacheHelper(object): - """html fragment cache helper class.""" - - lifetime = 60 # 1 minute - prefix = None - - def __init__(self, store, lifetime=None, prefix=None): - self.store = store - if lifetime is not None: - self.lifetime = lifetime - if prefix is not None: - self.prefix = prefix - - def not_cached(self, cache_key, lifetime=None): - """(obsolete. use cache_as() instead of this.) - html fragment cache helper. see document of FragmentCacheHelper class.""" - context = sys._getframe(1).f_locals['_context'] - context['_cache_key'] = cache_key - key = self.prefix and self.prefix + cache_key or cache_key - value = self.store.get(key) - if value: # cached - if logger: - logger.debug('[tenjin.not_cached] %r: cached.' % (cache_key, )) - context[key] = value - return False - else: # not cached - if logger: - logger.debug( - '[tenjin.not_cached]: %r: not cached.' % (cache_key, )) - if key in context: - del context[key] - if lifetime is None: - lifetime = self.lifetime - context['_cache_lifetime'] = lifetime - helpers.start_capture(cache_key, _depth=2) - return True - - def echo_cached(self): - """(obsolete. use cache_as() instead of this.) - html fragment cache helper. see document of FragmentCacheHelper class.""" - f_locals = sys._getframe(1).f_locals - context = f_locals['_context'] - cache_key = context.pop('_cache_key') - key = self.prefix and self.prefix + cache_key or cache_key - if key in context: # cached - value = context.pop(key) - else: # not cached - value = helpers.stop_capture(False, _depth=2) - lifetime = context.pop('_cache_lifetime') - self.store.set(key, value, lifetime) - f_locals['_buf'].append(value) - - def functions(self): - """(obsolete. use cache_as() instead of this.)""" - return (self.not_cached, self.echo_cached) - - def cache_as(self, cache_key, lifetime=None): - key = self.prefix and self.prefix + cache_key or cache_key - _buf = sys._getframe(1).f_locals['_buf'] - value = self.store.get(key) - if value: - if logger: - logger.debug('[tenjin.cache_as] %r: cache found.' % - (cache_key, )) - _buf.append(value) - else: - if logger: - logger.debug( - '[tenjin.cache_as] %r: expired or not cached yet.' % (cache_key, )) - _buf_len = len(_buf) - yield None - value = ''.join(_buf[_buf_len:]) - self.store.set(key, value, lifetime) - - -# you can change default store by 'tenjin.helpers.fragment_cache.store = ...' -helpers.fragment_cache = FragmentCacheHelper(MemoryBaseStore()) -helpers.not_cached = helpers.fragment_cache.not_cached -helpers.echo_cached = helpers.fragment_cache.echo_cached -helpers.cache_as = helpers.fragment_cache.cache_as -helpers.__all__.extend(('not_cached', 'echo_cached', 'cache_as')) - - -## -# helper class to find and read template -## -class Loader(object): - - def exists(self, filepath): - raise NotImplementedError( - "%s.exists(): not implemented yet." % self.__class__.__name__) - - def find(self, filename, dirs=None): - #: if dirs provided then search template file from it. - if dirs: - for dirname in dirs: - filepath = os.path.join(dirname, filename) - if self.exists(filepath): - return filepath - #: if dirs not provided then just return filename if file exists. - else: - if self.exists(filename): - return filename - #: if file not found then return None. - return None - - def abspath(self, filename): - raise NotImplementedError( - "%s.abspath(): not implemented yet." % self.__class__.__name__) - - def timestamp(self, filepath): - raise NotImplementedError( - "%s.timestamp(): not implemented yet." % self.__class__.__name__) - - def load(self, filepath): - raise NotImplementedError( - "%s.timestamp(): not implemented yet." % self.__class__.__name__) - - -## -# helper class to find and read files -## -class FileSystemLoader(Loader): - - def exists(self, filepath): - #: return True if filepath exists as a file. - return os.path.isfile(filepath) - - def abspath(self, filepath): - #: return full-path of filepath - return os.path.abspath(filepath) - - def timestamp(self, filepath): - #: return mtime of file - return _getmtime(filepath) - - def load(self, filepath): - #: if file exists, return file content and mtime - def f(): - mtime = _getmtime(filepath) - input = _read_template_file(filepath) - mtime2 = _getmtime(filepath) - if mtime != mtime2: - mtime = mtime2 - input = _read_template_file(filepath) - mtime2 = _getmtime(filepath) - if mtime != mtime2: - if logger: - logger.warn( - "[tenjin] %s.load(): timestamp is changed while reading file." % self.__class__.__name__) - return input, mtime - #: if file not exist, return None - return _ignore_not_found_error(f) - - -## -## -## -class TemplateNotFoundError(Exception): - pass - - -## -# template engine class -## - -class Engine(object): - """Template Engine class. - See User's Guide and examples for details. - http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html - http://www.kuwata-lab.com/tenjin/pytenjin-examples.html - """ - - # default value of attributes - prefix = '' - postfix = '' - layout = None - templateclass = Template - path = None - cache = TextCacheStorage() # save converted Python code into text file - lang = None - loader = FileSystemLoader() - preprocess = False - preprocessorclass = Preprocessor - timestamp_interval = 1 # seconds - - def __init__(self, prefix=None, postfix=None, layout=None, path=None, cache=True, preprocess=None, templateclass=None, preprocessorclass=None, lang=None, loader=None, pp=None, **kwargs): - """Initializer of Engine class. - - prefix:str (='') - Prefix string used to convert template short name to template filename. - postfix:str (='') - Postfix string used to convert template short name to template filename. - layout:str (=None) - Default layout template name. - path:list of str(=None) - List of directory names which contain template files. - cache:bool or CacheStorage instance (=True) - Cache storage object to store converted python code. - If True, default cache storage (=Engine.cache) is used (if it is None - then create MarshalCacheStorage object for each engine object). - If False, no cache storage is used nor no cache files are created. - preprocess:bool(=False) - Activate preprocessing or not. - templateclass:class (=Template) - Template class which engine creates automatically. - lang:str (=None) - Language name such as 'en', 'fr', 'ja', and so on. If you specify - this, cache file path will be 'inex.html.en.cache' for example. - pp:list (=None) - List of preprocessor object which is callable and manipulates template content. - kwargs:dict - Options for Template class constructor. - See document of Template.__init__() for details. - """ - if prefix: - self.prefix = prefix - if postfix: - self.postfix = postfix - if layout: - self.layout = layout - if templateclass: - self.templateclass = templateclass - if preprocessorclass: - self.preprocessorclass = preprocessorclass - if path is not None: - self.path = path - if lang is not None: - self.lang = lang - if loader is not None: - self.loader = loader - if preprocess is not None: - self.preprocess = preprocess - if pp is None: - pp = [] - elif isinstance(pp, list): - pass - elif isinstance(pp, tuple): - pp = list(pp) - else: - raise TypeError("'pp' expected to be a list but got %r." % (pp,)) - self.pp = pp - if preprocess: - self.pp.append(TemplatePreprocessor(self.preprocessorclass)) - self.kwargs = kwargs - self.encoding = kwargs.get('encoding') - self._filepaths = {} # template_name => relative path and absolute path - self._added_templates = {} # templates added by add_template() - #self.cache = cache - self._set_cache_storage(cache) - - def _set_cache_storage(self, cache): - if cache is True: - if not self.cache: - self.cache = MarshalCacheStorage() - elif cache is None: - pass - elif cache is False: - self.cache = None - elif isinstance(cache, CacheStorage): - self.cache = cache - else: - raise ValueError("%r: invalid cache object." % (cache, )) - - def cachename(self, filepath): - #: if lang is provided then add it to cache filename. - if self.lang: - return '%s.%s.cache' % (filepath, self.lang) - #: return cache file name. - else: - return filepath + '.cache' - - def to_filename(self, template_name): - """Convert template short name into filename. - ex. - >>> engine = tenjin.Engine(prefix='user_', postfix='.pyhtml') - >>> engine.to_filename(':list') - 'user_list.pyhtml' - >>> engine.to_filename('list') - 'list' - """ - #: if template_name starts with ':', add prefix and postfix to it. - if template_name[0] == ':': - return self.prefix + template_name[1:] + self.postfix - #: if template_name doesn't start with ':', just return it. - return template_name - - def _create_template(self, input=None, filepath=None, _context=None, _globals=None): - #: if input is not specified then just create empty template object. - template = self.templateclass(None, **self.kwargs) - #: if input is specified then create template object and return it. - if input: - template.convert(input, filepath) - return template - - def _preprocess(self, input, filepath, _context, _globals): - #if _context is None: _context = {} - #if _globals is None: _globals = sys._getframe(3).f_globals - #: preprocess template and return result - #preprocessor = self.preprocessorclass(filepath, input=input) - # return preprocessor.render(_context, globals=_globals) - #: preprocesses input with _context and returns result. - if '_engine' not in _context: - self.hook_context(_context) - for pp in self.pp: - input = pp.__call__(input, filename=filepath, - context=_context, globals=_globals) - return input - - def add_template(self, template): - self._added_templates[template.filename] = template - - def _get_template_from_cache(self, cachepath, filepath): - #: if template not found in cache, return None - template = self.cache.get(cachepath, self.templateclass) - if not template: - return None - assert template.timestamp is not None - #: if checked within a sec, skip timestamp check. - now = _time() - last_checked = getattr(template, '_last_checked_at', None) - if last_checked and now < last_checked + self.timestamp_interval: - # if logger: logger.trace('[tenjin.%s] timestamp check skipped (%f < %f + %f)' % \ - # (self.__class__.__name__, now, template._last_checked_at, self.timestamp_interval)) - return template - #: if timestamp of template objectis same as file, return it. - if template.timestamp == self.loader.timestamp(filepath): - template._last_checked_at = now - return template - #: if timestamp of template object is different from file, clear it - # cache._delete(cachepath) - if logger: - logger.info("[tenjin.%s] cache expired (filepath=%r)" % - (self.__class__.__name__, filepath)) - return None - - def get_template(self, template_name, _context=None, _globals=None): - """Return template object. - If template object has not registered, template engine creates - and registers template object automatically. - """ - #: accept template_name such as ':index'. - filename = self.to_filename(template_name) - #: if template object is added by add_template(), return it. - if filename in self._added_templates: - return self._added_templates[filename] - #: get filepath and fullpath of template - pair = self._filepaths.get(filename) - if pair: - filepath, fullpath = pair - else: - #: if template file is not found then raise TemplateNotFoundError. - filepath = self.loader.find(filename, self.path) - if not filepath: - raise TemplateNotFoundError( - '%s: filename not found (path=%r).' % (filename, self.path)) - # - fullpath = self.loader.abspath(filepath) - self._filepaths[filename] = (filepath, fullpath) - #: use full path as base of cache file path - cachepath = self.cachename(fullpath) - #: get template object from cache - cache = self.cache - template = cache and self._get_template_from_cache( - cachepath, filepath) or None - #: if template object is not found in cache or is expired... - if not template: - ret = self.loader.load(filepath) - if not ret: - raise TemplateNotFoundError( - "%r: template not found." % filepath) - input, timestamp = ret - if self.pp: # required for preprocessing - if _context is None: - _context = {} - if _globals is None: - _globals = sys._getframe(1).f_globals - input = self._preprocess(input, filepath, _context, _globals) - #: create template object. - template = self._create_template( - input, filepath, _context, _globals) - #: set timestamp and filename of template object. - template.timestamp = timestamp - template._last_checked_at = _time() - #: save template object into cache. - if cache: - if not template.bytecode: - #: ignores syntax error when compiling. - try: - template.compile() - except SyntaxError: - pass - cache.set(cachepath, template) - # else: - # template.compile() - #: - template.filename = filepath - return template - - def include(self, template_name, append_to_buf=True, **kwargs): - """Evaluate template using current local variables as context. - - template_name:str - Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template. - append_to_buf:boolean (=True) - If True then append output into _buf and return None, - else return stirng output. - - ex. - <?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 |