Rich Lane | a06d0c3 | 2013-03-25 08:52:03 -0700 | [diff] [blame] | 1 | ## |
| 2 | ## $Release: 1.1.1 $ |
| 3 | ## $Copyright: copyright(c) 2007-2012 kuwata-lab.com all rights reserved. $ |
| 4 | ## $License: MIT License $ |
| 5 | ## |
| 6 | ## Permission is hereby granted, free of charge, to any person obtaining |
| 7 | ## a copy of this software and associated documentation files (the |
| 8 | ## "Software"), to deal in the Software without restriction, including |
| 9 | ## without limitation the rights to use, copy, modify, merge, publish, |
| 10 | ## distribute, sublicense, and/or sell copies of the Software, and to |
| 11 | ## permit persons to whom the Software is furnished to do so, subject to |
| 12 | ## the following conditions: |
| 13 | ## |
| 14 | ## The above copyright notice and this permission notice shall be |
| 15 | ## included in all copies or substantial portions of the Software. |
| 16 | ## |
| 17 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
| 18 | ## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| 19 | ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
| 20 | ## NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
| 21 | ## LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
| 22 | ## OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
| 23 | ## WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 24 | ## |
| 25 | |
| 26 | """Very fast and light-weight template engine based embedded Python. |
| 27 | See User's Guide and examples for details. |
| 28 | http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html |
| 29 | http://www.kuwata-lab.com/tenjin/pytenjin-examples.html |
| 30 | """ |
| 31 | |
| 32 | __version__ = "$Release: 1.1.1 $"[10:-2] |
| 33 | __license__ = "$License: MIT License $"[10:-2] |
| 34 | __all__ = ('Template', 'Engine', ) |
| 35 | |
| 36 | |
| 37 | import sys, os, re, time, marshal |
| 38 | from time import time as _time |
| 39 | from os.path import getmtime as _getmtime |
| 40 | from os.path import isfile as _isfile |
| 41 | random = pickle = unquote = None # lazy import |
| 42 | python3 = sys.version_info[0] == 3 |
| 43 | python2 = sys.version_info[0] == 2 |
| 44 | |
| 45 | logger = None |
| 46 | |
| 47 | |
| 48 | ## |
| 49 | ## utilities |
| 50 | ## |
| 51 | |
| 52 | def _write_binary_file(filename, content): |
| 53 | global random |
| 54 | if random is None: from random import random |
| 55 | tmpfile = filename + str(random())[1:] |
| 56 | f = open(tmpfile, 'w+b') # on windows, 'w+b' is preffered than 'wb' |
| 57 | try: |
| 58 | f.write(content) |
| 59 | finally: |
| 60 | f.close() |
| 61 | if os.path.exists(tmpfile): |
| 62 | try: |
| 63 | os.rename(tmpfile, filename) |
| 64 | except: |
| 65 | os.remove(filename) # on windows, existing file should be removed before renaming |
| 66 | os.rename(tmpfile, filename) |
| 67 | |
| 68 | def _read_binary_file(filename): |
| 69 | f = open(filename, 'rb') |
| 70 | try: |
| 71 | return f.read() |
| 72 | finally: |
| 73 | f.close() |
| 74 | |
| 75 | codecs = None # lazy import |
| 76 | |
| 77 | def _read_text_file(filename, encoding=None): |
| 78 | global codecs |
| 79 | if not codecs: import codecs |
| 80 | f = codecs.open(filename, encoding=(encoding or 'utf-8')) |
| 81 | try: |
| 82 | return f.read() |
| 83 | finally: |
| 84 | f.close() |
| 85 | |
| 86 | def _read_template_file(filename, encoding=None): |
| 87 | s = _read_binary_file(filename) ## binary(=str) |
| 88 | if encoding: s = s.decode(encoding) ## binary(=str) to unicode |
| 89 | return s |
| 90 | |
| 91 | _basestring = basestring |
| 92 | _unicode = unicode |
| 93 | _bytes = str |
| 94 | |
| 95 | def _ignore_not_found_error(f, default=None): |
| 96 | try: |
| 97 | return f() |
| 98 | except OSError, ex: |
| 99 | if ex.errno == 2: # error: No such file or directory |
| 100 | return default |
| 101 | raise |
| 102 | |
| 103 | def create_module(module_name, dummy_func=None, **kwargs): |
| 104 | """ex. mod = create_module('tenjin.util')""" |
| 105 | try: |
| 106 | mod = type(sys)(module_name) |
| 107 | except: |
| 108 | # The module creation above does not work for Jython 2.5.2 |
| 109 | import imp |
| 110 | mod = imp.new_module(module_name) |
| 111 | |
| 112 | mod.__file__ = __file__ |
| 113 | mod.__dict__.update(kwargs) |
| 114 | sys.modules[module_name] = mod |
| 115 | if dummy_func: |
| 116 | exec(dummy_func.func_code, mod.__dict__) |
| 117 | return mod |
| 118 | |
| 119 | def _raise(exception_class, *args): |
| 120 | raise exception_class(*args) |
| 121 | |
| 122 | |
| 123 | ## |
| 124 | ## helper method's module |
| 125 | ## |
| 126 | |
| 127 | def _dummy(): |
| 128 | global unquote |
| 129 | unquote = None |
| 130 | global to_str, escape, echo, new_cycle, generate_tostrfunc |
| 131 | global start_capture, stop_capture, capture_as, captured_as, CaptureContext |
| 132 | global _p, _P, _decode_params |
| 133 | |
| 134 | def generate_tostrfunc(encode=None, decode=None): |
| 135 | """Generate 'to_str' function with encode or decode encoding. |
| 136 | ex. generate to_str() function which encodes unicode into binary(=str). |
| 137 | to_str = tenjin.generate_tostrfunc(encode='utf-8') |
| 138 | repr(to_str(u'hoge')) #=> 'hoge' (str) |
| 139 | ex. generate to_str() function which decodes binary(=str) into unicode. |
| 140 | to_str = tenjin.generate_tostrfunc(decode='utf-8') |
| 141 | repr(to_str('hoge')) #=> u'hoge' (unicode) |
| 142 | """ |
| 143 | if encode: |
| 144 | if decode: |
| 145 | raise ValueError("can't specify both encode and decode encoding.") |
| 146 | else: |
| 147 | def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _encode=encode): |
| 148 | """Convert val into string or return '' if None. Unicode will be encoded into binary(=str).""" |
| 149 | if _isa(val, _str): return val |
| 150 | if val is None: return '' |
| 151 | #if _isa(val, _unicode): return val.encode(_encode) # unicode to binary(=str) |
| 152 | if _isa(val, _unicode): |
| 153 | return val.encode(_encode) # unicode to binary(=str) |
| 154 | return _str(val) |
| 155 | else: |
| 156 | if decode: |
| 157 | def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _decode=decode): |
| 158 | """Convert val into string or return '' if None. Binary(=str) will be decoded into unicode.""" |
| 159 | #if _isa(val, _str): return val.decode(_decode) # binary(=str) to unicode |
| 160 | if _isa(val, _str): |
| 161 | return val.decode(_decode) |
| 162 | if val is None: return '' |
| 163 | if _isa(val, _unicode): return val |
| 164 | return _unicode(val) |
| 165 | else: |
| 166 | def to_str(val, _str=str, _unicode=unicode, _isa=isinstance): |
| 167 | """Convert val into string or return '' if None. Both binary(=str) and unicode will be retruned as-is.""" |
| 168 | if _isa(val, _str): return val |
| 169 | if val is None: return '' |
| 170 | if _isa(val, _unicode): return val |
| 171 | return _str(val) |
| 172 | return to_str |
| 173 | |
| 174 | to_str = generate_tostrfunc(encode='utf-8') # or encode=None? |
| 175 | |
| 176 | def echo(string): |
| 177 | """add string value into _buf. this is equivarent to '#{string}'.""" |
| 178 | lvars = sys._getframe(1).f_locals # local variables |
| 179 | lvars['_buf'].append(string) |
| 180 | |
| 181 | def new_cycle(*values): |
| 182 | """Generate cycle object. |
| 183 | ex. |
| 184 | cycle = new_cycle('odd', 'even') |
| 185 | print(cycle()) #=> 'odd' |
| 186 | print(cycle()) #=> 'even' |
| 187 | print(cycle()) #=> 'odd' |
| 188 | print(cycle()) #=> 'even' |
| 189 | """ |
| 190 | def gen(values): |
| 191 | i, n = 0, len(values) |
| 192 | while True: |
| 193 | yield values[i] |
| 194 | i = (i + 1) % n |
| 195 | return gen(values).next |
| 196 | |
| 197 | class CaptureContext(object): |
| 198 | |
| 199 | def __init__(self, name, store_to_context=True, lvars=None): |
| 200 | self.name = name |
| 201 | self.store_to_context = store_to_context |
| 202 | self.lvars = lvars or sys._getframe(1).f_locals |
| 203 | |
| 204 | def __enter__(self): |
| 205 | lvars = self.lvars |
| 206 | self._buf_orig = lvars['_buf'] |
| 207 | lvars['_buf'] = _buf = [] |
| 208 | lvars['_extend'] = _buf.extend |
| 209 | return self |
| 210 | |
| 211 | def __exit__(self, *args): |
| 212 | lvars = self.lvars |
| 213 | _buf = lvars['_buf'] |
| 214 | lvars['_buf'] = self._buf_orig |
| 215 | lvars['_extend'] = self._buf_orig.extend |
| 216 | lvars[self.name] = self.captured = ''.join(_buf) |
| 217 | if self.store_to_context and '_context' in lvars: |
| 218 | lvars['_context'][self.name] = self.captured |
| 219 | |
| 220 | def __iter__(self): |
| 221 | self.__enter__() |
| 222 | yield self |
| 223 | self.__exit__() |
| 224 | |
| 225 | def start_capture(varname=None, _depth=1): |
| 226 | """(obsolete) start capturing with name.""" |
| 227 | lvars = sys._getframe(_depth).f_locals |
| 228 | capture_context = CaptureContext(varname, None, lvars) |
| 229 | lvars['_capture_context'] = capture_context |
| 230 | capture_context.__enter__() |
| 231 | |
| 232 | def stop_capture(store_to_context=True, _depth=1): |
| 233 | """(obsolete) stop capturing and return the result of capturing. |
| 234 | if store_to_context is True then the result is stored into _context[varname]. |
| 235 | """ |
| 236 | lvars = sys._getframe(_depth).f_locals |
| 237 | capture_context = lvars.pop('_capture_context', None) |
| 238 | if not capture_context: |
| 239 | raise Exception('stop_capture(): start_capture() is not called before.') |
| 240 | capture_context.store_to_context = store_to_context |
| 241 | capture_context.__exit__() |
| 242 | return capture_context.captured |
| 243 | |
| 244 | def capture_as(name, store_to_context=True): |
| 245 | """capture partial of template.""" |
| 246 | return CaptureContext(name, store_to_context, sys._getframe(1).f_locals) |
| 247 | |
| 248 | def captured_as(name, _depth=1): |
| 249 | """helper method for layout template. |
| 250 | if captured string is found then append it to _buf and return True, |
| 251 | else return False. |
| 252 | """ |
| 253 | lvars = sys._getframe(_depth).f_locals # local variables |
| 254 | if name in lvars: |
| 255 | _buf = lvars['_buf'] |
| 256 | _buf.append(lvars[name]) |
| 257 | return True |
| 258 | return False |
| 259 | |
| 260 | def _p(arg): |
| 261 | """ex. '/show/'+_p("item['id']") => "/show/#{item['id']}" """ |
| 262 | return '<`#%s#`>' % arg # decoded into #{...} by preprocessor |
| 263 | |
| 264 | def _P(arg): |
| 265 | """ex. '<b>%s</b>' % _P("item['id']") => "<b>${item['id']}</b>" """ |
| 266 | return '<`$%s$`>' % arg # decoded into ${...} by preprocessor |
| 267 | |
| 268 | def _decode_params(s): |
| 269 | """decode <`#...#`> and <`$...$`> into #{...} and ${...}""" |
| 270 | global unquote |
| 271 | if unquote is None: |
| 272 | from urllib import unquote |
| 273 | dct = { 'lt':'<', 'gt':'>', 'amp':'&', 'quot':'"', '#039':"'", } |
| 274 | def unescape(s): |
| 275 | #return s.replace('<', '<').replace('>', '>').replace('"', '"').replace(''', "'").replace('&', '&') |
| 276 | return re.sub(r'&(lt|gt|quot|amp|#039);', lambda m: dct[m.group(1)], s) |
| 277 | s = to_str(s) |
| 278 | s = re.sub(r'%3C%60%23(.*?)%23%60%3E', lambda m: '#{%s}' % unquote(m.group(1)), s) |
| 279 | s = re.sub(r'%3C%60%24(.*?)%24%60%3E', lambda m: '${%s}' % unquote(m.group(1)), s) |
| 280 | s = re.sub(r'<`#(.*?)#`>', lambda m: '#{%s}' % unescape(m.group(1)), s) |
| 281 | s = re.sub(r'<`\$(.*?)\$`>', lambda m: '${%s}' % unescape(m.group(1)), s) |
| 282 | s = re.sub(r'<`#(.*?)#`>', r'#{\1}', s) |
| 283 | s = re.sub(r'<`\$(.*?)\$`>', r'${\1}', s) |
| 284 | return s |
| 285 | |
| 286 | helpers = create_module('tenjin.helpers', _dummy, sys=sys, re=re) |
| 287 | helpers.__all__ = ['to_str', 'escape', 'echo', 'new_cycle', 'generate_tostrfunc', |
| 288 | 'start_capture', 'stop_capture', 'capture_as', 'captured_as', |
| 289 | 'not_cached', 'echo_cached', 'cache_as', |
| 290 | '_p', '_P', '_decode_params', |
| 291 | ] |
| 292 | generate_tostrfunc = helpers.generate_tostrfunc |
| 293 | |
| 294 | |
| 295 | ## |
| 296 | ## escaped module |
| 297 | ## |
| 298 | def _dummy(): |
| 299 | global is_escaped, as_escaped, to_escaped |
| 300 | global Escaped, EscapedStr, EscapedUnicode |
| 301 | global __all__ |
| 302 | __all__ = ('is_escaped', 'as_escaped', 'to_escaped', ) #'Escaped', 'EscapedStr', |
| 303 | |
| 304 | class Escaped(object): |
| 305 | """marking class that object is already escaped.""" |
| 306 | pass |
| 307 | |
| 308 | def is_escaped(value): |
| 309 | """return True if value is marked as escaped, else return False.""" |
| 310 | return isinstance(value, Escaped) |
| 311 | |
| 312 | class EscapedStr(str, Escaped): |
| 313 | """string class which is marked as escaped.""" |
| 314 | pass |
| 315 | |
| 316 | class EscapedUnicode(unicode, Escaped): |
| 317 | """unicode class which is marked as escaped.""" |
| 318 | pass |
| 319 | |
| 320 | def as_escaped(s): |
| 321 | """mark string as escaped, without escaping.""" |
| 322 | if isinstance(s, str): return EscapedStr(s) |
| 323 | if isinstance(s, unicode): return EscapedUnicode(s) |
| 324 | raise TypeError("as_escaped(%r): expected str or unicode." % (s, )) |
| 325 | |
| 326 | def to_escaped(value): |
| 327 | """convert any value into string and escape it. |
| 328 | if value is already marked as escaped, don't escape it.""" |
| 329 | if hasattr(value, '__html__'): |
| 330 | value = value.__html__() |
| 331 | if is_escaped(value): |
| 332 | #return value # EscapedUnicode should be convered into EscapedStr |
| 333 | return as_escaped(_helpers.to_str(value)) |
| 334 | #if isinstance(value, _basestring): |
| 335 | # return as_escaped(_helpers.escape(value)) |
| 336 | return as_escaped(_helpers.escape(_helpers.to_str(value))) |
| 337 | |
| 338 | escaped = create_module('tenjin.escaped', _dummy, _helpers=helpers) |
| 339 | |
| 340 | |
| 341 | ## |
| 342 | ## module for html |
| 343 | ## |
| 344 | def _dummy(): |
| 345 | global escape_html, escape_xml, escape, tagattr, tagattrs, _normalize_attrs |
| 346 | global checked, selected, disabled, nl2br, text2html, nv, js_link |
| 347 | |
| 348 | #_escape_table = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } |
| 349 | #_escape_pattern = re.compile(r'[&<>"]') |
| 350 | ##_escape_callable = lambda m: _escape_table[m.group(0)] |
| 351 | ##_escape_callable = lambda m: _escape_table.__get__(m.group(0)) |
| 352 | #_escape_get = _escape_table.__getitem__ |
| 353 | #_escape_callable = lambda m: _escape_get(m.group(0)) |
| 354 | #_escape_sub = _escape_pattern.sub |
| 355 | |
| 356 | #def escape_html(s): |
| 357 | # return s # 3.02 |
| 358 | |
| 359 | #def escape_html(s): |
| 360 | # return _escape_pattern.sub(_escape_callable, s) # 6.31 |
| 361 | |
| 362 | #def escape_html(s): |
| 363 | # return _escape_sub(_escape_callable, s) # 6.01 |
| 364 | |
| 365 | #def escape_html(s, _p=_escape_pattern, _f=_escape_callable): |
| 366 | # return _p.sub(_f, s) # 6.27 |
| 367 | |
| 368 | #def escape_html(s, _sub=_escape_pattern.sub, _callable=_escape_callable): |
| 369 | # return _sub(_callable, s) # 6.04 |
| 370 | |
| 371 | #def escape_html(s): |
| 372 | # s = s.replace('&', '&') |
| 373 | # s = s.replace('<', '<') |
| 374 | # s = s.replace('>', '>') |
| 375 | # s = s.replace('"', '"') |
| 376 | # return s # 5.83 |
| 377 | |
| 378 | def escape_html(s): |
| 379 | """Escape '&', '<', '>', '"' into '&', '<', '>', '"'.""" |
| 380 | return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') # 5.72 |
| 381 | |
| 382 | escape_xml = escape_html # for backward compatibility |
| 383 | |
| 384 | def tagattr(name, expr, value=None, escape=True): |
| 385 | """(experimental) Return ' name="value"' if expr is true value, else '' (empty string). |
| 386 | If value is not specified, expr is used as value instead.""" |
| 387 | if not expr and expr != 0: return _escaped.as_escaped('') |
| 388 | if value is None: value = expr |
| 389 | if escape: value = _escaped.to_escaped(value) |
| 390 | return _escaped.as_escaped(' %s="%s"' % (name, value)) |
| 391 | |
| 392 | def tagattrs(**kwargs): |
| 393 | """(experimental) built html tag attribtes. |
| 394 | ex. |
| 395 | >>> tagattrs(klass='main', size=20) |
| 396 | ' class="main" size="20"' |
| 397 | >>> tagattrs(klass='', size=0) |
| 398 | '' |
| 399 | """ |
| 400 | kwargs = _normalize_attrs(kwargs) |
| 401 | esc = _escaped.to_escaped |
| 402 | s = ''.join([ ' %s="%s"' % (k, esc(v)) for k, v in kwargs.iteritems() if v or v == 0 ]) |
| 403 | return _escaped.as_escaped(s) |
| 404 | |
| 405 | def _normalize_attrs(kwargs): |
| 406 | if 'klass' in kwargs: kwargs['class'] = kwargs.pop('klass') |
| 407 | if 'checked' in kwargs: kwargs['checked'] = kwargs.pop('checked') and 'checked' or None |
| 408 | if 'selected' in kwargs: kwargs['selected'] = kwargs.pop('selected') and 'selected' or None |
| 409 | if 'disabled' in kwargs: kwargs['disabled'] = kwargs.pop('disabled') and 'disabled' or None |
| 410 | return kwargs |
| 411 | |
| 412 | def checked(expr): |
| 413 | """return ' checked="checked"' if expr is true.""" |
| 414 | return _escaped.as_escaped(expr and ' checked="checked"' or '') |
| 415 | |
| 416 | def selected(expr): |
| 417 | """return ' selected="selected"' if expr is true.""" |
| 418 | return _escaped.as_escaped(expr and ' selected="selected"' or '') |
| 419 | |
| 420 | def disabled(expr): |
| 421 | """return ' disabled="disabled"' if expr is true.""" |
| 422 | return _escaped.as_escaped(expr and ' disabled="disabled"' or '') |
| 423 | |
| 424 | def nl2br(text): |
| 425 | """replace "\n" to "<br />\n" and return it.""" |
| 426 | if not text: |
| 427 | return _escaped.as_escaped('') |
| 428 | return _escaped.as_escaped(text.replace('\n', '<br />\n')) |
| 429 | |
| 430 | def text2html(text, use_nbsp=True): |
| 431 | """(experimental) escape xml characters, replace "\n" to "<br />\n", and return it.""" |
| 432 | if not text: |
| 433 | return _escaped.as_escaped('') |
| 434 | s = _escaped.to_escaped(text) |
| 435 | if use_nbsp: s = s.replace(' ', ' ') |
| 436 | #return nl2br(s) |
| 437 | s = s.replace('\n', '<br />\n') |
| 438 | return _escaped.as_escaped(s) |
| 439 | |
| 440 | def nv(name, value, sep=None, **kwargs): |
| 441 | """(experimental) Build name and value attributes. |
| 442 | ex. |
| 443 | >>> nv('rank', 'A') |
| 444 | 'name="rank" value="A"' |
| 445 | >>> nv('rank', 'A', '.') |
| 446 | 'name="rank" value="A" id="rank.A"' |
| 447 | >>> nv('rank', 'A', '.', checked=True) |
| 448 | 'name="rank" value="A" id="rank.A" checked="checked"' |
| 449 | >>> nv('rank', 'A', '.', klass='error', style='color:red') |
| 450 | 'name="rank" value="A" id="rank.A" class="error" style="color:red"' |
| 451 | """ |
| 452 | name = _escaped.to_escaped(name) |
| 453 | value = _escaped.to_escaped(value) |
| 454 | s = sep and 'name="%s" value="%s" id="%s"' % (name, value, name+sep+value) \ |
| 455 | or 'name="%s" value="%s"' % (name, value) |
| 456 | html = kwargs and s + tagattrs(**kwargs) or s |
| 457 | return _escaped.as_escaped(html) |
| 458 | |
| 459 | def js_link(label, onclick, **kwargs): |
| 460 | s = kwargs and tagattrs(**kwargs) or '' |
| 461 | html = '<a href="javascript:undefined" onclick="%s;return false"%s>%s</a>' % \ |
| 462 | (_escaped.to_escaped(onclick), s, _escaped.to_escaped(label)) |
| 463 | return _escaped.as_escaped(html) |
| 464 | |
| 465 | html = create_module('tenjin.html', _dummy, helpers=helpers, _escaped=escaped) |
| 466 | helpers.escape = html.escape_html |
| 467 | helpers.html = html # for backward compatibility |
| 468 | sys.modules['tenjin.helpers.html'] = html |
| 469 | |
| 470 | |
| 471 | ## |
| 472 | ## utility function to set default encoding of template files |
| 473 | ## |
| 474 | _template_encoding = (None, 'utf-8') # encodings for decode and encode |
| 475 | |
| 476 | def set_template_encoding(decode=None, encode=None): |
| 477 | """Set default encoding of template files. |
| 478 | This should be called before importing helper functions. |
| 479 | ex. |
| 480 | ## I like template files to be unicode-base like Django. |
| 481 | import tenjin |
| 482 | tenjin.set_template_encoding('utf-8') # should be called before importing helpers |
| 483 | from tenjin.helpers import * |
| 484 | """ |
| 485 | global _template_encoding |
| 486 | if _template_encoding == (decode, encode): |
| 487 | return |
| 488 | if decode and encode: |
| 489 | raise ValueError("set_template_encoding(): cannot specify both decode and encode.") |
| 490 | if not decode and not encode: |
| 491 | raise ValueError("set_template_encoding(): decode or encode should be specified.") |
| 492 | if decode: |
| 493 | Template.encoding = decode # unicode base template |
| 494 | helpers.to_str = helpers.generate_tostrfunc(decode=decode) |
| 495 | else: |
| 496 | Template.encoding = None # binary base template |
| 497 | helpers.to_str = helpers.generate_tostrfunc(encode=encode) |
| 498 | _template_encoding = (decode, encode) |
| 499 | |
| 500 | |
| 501 | ## |
| 502 | ## Template class |
| 503 | ## |
| 504 | |
| 505 | class TemplateSyntaxError(SyntaxError): |
| 506 | |
| 507 | def build_error_message(self): |
| 508 | ex = self |
| 509 | if not ex.text: |
| 510 | return self.args[0] |
| 511 | return ''.join([ |
| 512 | "%s:%s:%s: %s\n" % (ex.filename, ex.lineno, ex.offset, ex.msg, ), |
| 513 | "%4d: %s\n" % (ex.lineno, ex.text.rstrip(), ), |
| 514 | " %s^\n" % (' ' * ex.offset, ), |
| 515 | ]) |
| 516 | |
| 517 | |
| 518 | class Template(object): |
| 519 | """Convert and evaluate embedded python string. |
| 520 | See User's Guide and examples for details. |
| 521 | http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html |
| 522 | http://www.kuwata-lab.com/tenjin/pytenjin-examples.html |
| 523 | """ |
| 524 | |
| 525 | ## default value of attributes |
| 526 | filename = None |
| 527 | encoding = None |
| 528 | escapefunc = 'escape' |
| 529 | tostrfunc = 'to_str' |
| 530 | indent = 4 |
| 531 | preamble = None # "_buf = []; _expand = _buf.expand; _to_str = to_str; _escape = escape" |
| 532 | postamble = None # "print ''.join(_buf)" |
| 533 | smarttrim = None |
| 534 | args = None |
| 535 | timestamp = None |
| 536 | trace = False # if True then '<!-- begin: file -->' and '<!-- end: file -->' are printed |
| 537 | |
| 538 | def __init__(self, filename=None, encoding=None, input=None, escapefunc=None, tostrfunc=None, |
| 539 | indent=None, preamble=None, postamble=None, smarttrim=None, trace=None): |
| 540 | """Initailizer of Template class. |
| 541 | |
| 542 | filename:str (=None) |
| 543 | Filename to convert (optional). If None, no convert. |
| 544 | encoding:str (=None) |
| 545 | Encoding name. If specified, template string is converted into |
| 546 | unicode object internally. |
| 547 | Template.render() returns str object if encoding is None, |
| 548 | else returns unicode object if encoding name is specified. |
| 549 | input:str (=None) |
| 550 | Input string. In other words, content of template file. |
| 551 | Template file will not be read if this argument is specified. |
| 552 | escapefunc:str (='escape') |
| 553 | Escape function name. |
| 554 | tostrfunc:str (='to_str') |
| 555 | 'to_str' function name. |
| 556 | indent:int (=4) |
| 557 | Indent width. |
| 558 | preamble:str or bool (=None) |
| 559 | Preamble string which is inserted into python code. |
| 560 | If true, '_buf = []; ' is used insated. |
| 561 | postamble:str or bool (=None) |
| 562 | Postamble string which is appended to python code. |
| 563 | If true, 'print("".join(_buf))' is used instead. |
| 564 | smarttrim:bool (=None) |
| 565 | If True then "<div>\\n#{_context}\\n</div>" is parsed as |
| 566 | "<div>\\n#{_context}</div>". |
| 567 | """ |
| 568 | if encoding is not None: self.encoding = encoding |
| 569 | if escapefunc is not None: self.escapefunc = escapefunc |
| 570 | if tostrfunc is not None: self.tostrfunc = tostrfunc |
| 571 | if indent is not None: self.indent = indent |
| 572 | if preamble is not None: self.preamble = preamble |
| 573 | if postamble is not None: self.postamble = postamble |
| 574 | if smarttrim is not None: self.smarttrim = smarttrim |
| 575 | if trace is not None: self.trace = trace |
| 576 | # |
| 577 | if preamble is True: self.preamble = "_buf = []" |
| 578 | if postamble is True: self.postamble = "print(''.join(_buf))" |
| 579 | if input: |
| 580 | self.convert(input, filename) |
| 581 | self.timestamp = False # False means 'file not exist' (= Engine should not check timestamp of file) |
| 582 | elif filename: |
| 583 | self.convert_file(filename) |
| 584 | else: |
| 585 | self._reset() |
| 586 | |
| 587 | def _reset(self, input=None, filename=None): |
| 588 | self.script = None |
| 589 | self.bytecode = None |
| 590 | self.input = input |
| 591 | self.filename = filename |
| 592 | if input != None: |
| 593 | i = input.find("\n") |
| 594 | if i < 0: |
| 595 | self.newline = "\n" # or None |
| 596 | elif len(input) >= 2 and input[i-1] == "\r": |
| 597 | self.newline = "\r\n" |
| 598 | else: |
| 599 | self.newline = "\n" |
| 600 | self._localvars_assignments_added = False |
| 601 | |
| 602 | def _localvars_assignments(self): |
| 603 | return "_extend=_buf.extend;_to_str=%s;_escape=%s; " % (self.tostrfunc, self.escapefunc) |
| 604 | |
| 605 | def before_convert(self, buf): |
| 606 | if self.preamble: |
| 607 | eol = self.input.startswith('<?py') and "\n" or "; " |
| 608 | buf.append(self.preamble + eol) |
| 609 | |
| 610 | def after_convert(self, buf): |
| 611 | if self.postamble: |
| 612 | if buf and not buf[-1].endswith("\n"): |
| 613 | buf.append("\n") |
| 614 | buf.append(self.postamble + "\n") |
| 615 | |
| 616 | def convert_file(self, filename): |
| 617 | """Convert file into python script and return it. |
| 618 | This is equivarent to convert(open(filename).read(), filename). |
| 619 | """ |
| 620 | input = _read_template_file(filename) |
| 621 | return self.convert(input, filename) |
| 622 | |
| 623 | def convert(self, input, filename=None): |
| 624 | """Convert string in which python code is embedded into python script and return it. |
| 625 | |
| 626 | input:str |
| 627 | Input string to convert into python code. |
| 628 | filename:str (=None) |
| 629 | Filename of input. this is optional but recommended to report errors. |
| 630 | """ |
| 631 | if self.encoding and isinstance(input, str): |
| 632 | input = input.decode(self.encoding) |
| 633 | self._reset(input, filename) |
| 634 | buf = [] |
| 635 | self.before_convert(buf) |
| 636 | self.parse_stmts(buf, input) |
| 637 | self.after_convert(buf) |
| 638 | script = ''.join(buf) |
| 639 | self.script = script |
| 640 | return script |
| 641 | |
| 642 | STMT_PATTERN = (r'<\?py( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?', re.S) |
| 643 | |
| 644 | def stmt_pattern(self): |
| 645 | pat = self.STMT_PATTERN |
| 646 | if isinstance(pat, tuple): |
| 647 | pat = self.__class__.STMT_PATTERN = re.compile(*pat) |
| 648 | return pat |
| 649 | |
| 650 | def parse_stmts(self, buf, input): |
| 651 | if not input: return |
| 652 | rexp = self.stmt_pattern() |
| 653 | is_bol = True |
| 654 | index = 0 |
| 655 | for m in rexp.finditer(input): |
| 656 | mspace, code, rspace = m.groups() |
| 657 | #mspace, close, rspace = m.groups() |
| 658 | #code = input[m.start()+4+len(mspace):m.end()-len(close)-(rspace and len(rspace) or 0)] |
| 659 | text = input[index:m.start()] |
| 660 | index = m.end() |
| 661 | ## detect spaces at beginning of line |
| 662 | lspace = None |
| 663 | if text == '': |
| 664 | if is_bol: |
| 665 | lspace = '' |
| 666 | elif text[-1] == '\n': |
| 667 | lspace = '' |
| 668 | else: |
| 669 | rindex = text.rfind('\n') |
| 670 | if rindex < 0: |
| 671 | if is_bol and text.isspace(): |
| 672 | lspace, text = text, '' |
| 673 | else: |
| 674 | s = text[rindex+1:] |
| 675 | if s.isspace(): |
| 676 | lspace, text = s, text[:rindex+1] |
| 677 | #is_bol = rspace is not None |
| 678 | ## add text, spaces, and statement |
| 679 | self.parse_exprs(buf, text, is_bol) |
| 680 | is_bol = rspace is not None |
| 681 | #if mspace == "\n": |
| 682 | if mspace and mspace.endswith("\n"): |
| 683 | code = "\n" + (code or "") |
| 684 | #if rspace == "\n": |
| 685 | if rspace and rspace.endswith("\n"): |
| 686 | code = (code or "") + "\n" |
| 687 | if code: |
| 688 | code = self.statement_hook(code) |
| 689 | m = self._match_to_args_declaration(code) |
| 690 | if m: |
| 691 | self._add_args_declaration(buf, m) |
| 692 | else: |
| 693 | self.add_stmt(buf, code) |
| 694 | rest = input[index:] |
| 695 | if rest: |
| 696 | self.parse_exprs(buf, rest) |
| 697 | self._arrange_indent(buf) |
| 698 | |
| 699 | def statement_hook(self, stmt): |
| 700 | """expand macros and parse '#@ARGS' in a statement.""" |
| 701 | return stmt.replace("\r\n", "\n") # Python can't handle "\r\n" in code |
| 702 | |
| 703 | def _match_to_args_declaration(self, stmt): |
| 704 | if self.args is not None: |
| 705 | return None |
| 706 | args_pattern = r'^ *#@ARGS(?:[ \t]+(.*?))?$' |
| 707 | return re.match(args_pattern, stmt) |
| 708 | |
| 709 | def _add_args_declaration(self, buf, m): |
| 710 | arr = (m.group(1) or '').split(',') |
| 711 | args = []; declares = [] |
| 712 | for s in arr: |
| 713 | arg = s.strip() |
| 714 | if not s: continue |
| 715 | if not re.match('^[a-zA-Z_]\w*$', arg): |
| 716 | raise ValueError("%r: invalid template argument." % arg) |
| 717 | args.append(arg) |
| 718 | declares.append("%s = _context.get('%s'); " % (arg, arg)) |
| 719 | self.args = args |
| 720 | #nl = stmt[m.end():] |
| 721 | #if nl: declares.append(nl) |
| 722 | buf.append(''.join(declares) + "\n") |
| 723 | |
| 724 | s = '(?:\{.*?\}.*?)*' |
| 725 | EXPR_PATTERN = (r'#\{(.*?'+s+r')\}|\$\{(.*?'+s+r')\}|\{=(?:=(.*?)=|(.*?))=\}', re.S) |
| 726 | del s |
| 727 | |
| 728 | def expr_pattern(self): |
| 729 | pat = self.EXPR_PATTERN |
| 730 | if isinstance(pat, tuple): |
| 731 | self.__class__.EXPR_PATTERN = pat = re.compile(*pat) |
| 732 | return pat |
| 733 | |
| 734 | def get_expr_and_flags(self, match): |
| 735 | expr1, expr2, expr3, expr4 = match.groups() |
| 736 | if expr1 is not None: return expr1, (False, True) # not escape, call to_str |
| 737 | if expr2 is not None: return expr2, (True, True) # call escape, call to_str |
| 738 | if expr3 is not None: return expr3, (False, True) # not escape, call to_str |
| 739 | if expr4 is not None: return expr4, (True, True) # call escape, call to_str |
| 740 | |
| 741 | def parse_exprs(self, buf, input, is_bol=False): |
| 742 | buf2 = [] |
| 743 | self._parse_exprs(buf2, input, is_bol) |
| 744 | if buf2: |
| 745 | buf.append(''.join(buf2)) |
| 746 | |
| 747 | def _parse_exprs(self, buf, input, is_bol=False): |
| 748 | if not input: return |
| 749 | self.start_text_part(buf) |
| 750 | rexp = self.expr_pattern() |
| 751 | smarttrim = self.smarttrim |
| 752 | nl = self.newline |
| 753 | nl_len = len(nl) |
| 754 | pos = 0 |
| 755 | for m in rexp.finditer(input): |
| 756 | start = m.start() |
| 757 | text = input[pos:start] |
| 758 | pos = m.end() |
| 759 | expr, flags = self.get_expr_and_flags(m) |
| 760 | # |
| 761 | if text: |
| 762 | self.add_text(buf, text) |
| 763 | self.add_expr(buf, expr, *flags) |
| 764 | # |
| 765 | if smarttrim: |
| 766 | flag_bol = text.endswith(nl) or not text and (start > 0 or is_bol) |
| 767 | if flag_bol and not flags[0] and input[pos:pos+nl_len] == nl: |
| 768 | pos += nl_len |
| 769 | buf.append("\n") |
| 770 | if smarttrim: |
| 771 | if buf and buf[-1] == "\n": |
| 772 | buf.pop() |
| 773 | rest = input[pos:] |
| 774 | if rest: |
| 775 | self.add_text(buf, rest, True) |
| 776 | self.stop_text_part(buf) |
| 777 | if input[-1] == '\n': |
| 778 | buf.append("\n") |
| 779 | |
| 780 | def start_text_part(self, buf): |
| 781 | self._add_localvars_assignments_to_text(buf) |
| 782 | #buf.append("_buf.extend((") |
| 783 | buf.append("_extend((") |
| 784 | |
| 785 | def _add_localvars_assignments_to_text(self, buf): |
| 786 | if not self._localvars_assignments_added: |
| 787 | self._localvars_assignments_added = True |
| 788 | buf.append(self._localvars_assignments()) |
| 789 | |
| 790 | def stop_text_part(self, buf): |
| 791 | buf.append("));") |
| 792 | |
| 793 | def _quote_text(self, text): |
| 794 | text = re.sub(r"(['\\\\])", r"\\\1", text) |
| 795 | text = text.replace("\r\n", "\\r\n") |
| 796 | return text |
| 797 | |
| 798 | def add_text(self, buf, text, encode_newline=False): |
| 799 | if not text: return |
| 800 | use_unicode = self.encoding and python2 |
| 801 | buf.append(use_unicode and "u'''" or "'''") |
| 802 | text = self._quote_text(text) |
| 803 | if not encode_newline: buf.extend((text, "''', ")) |
| 804 | elif text.endswith("\r\n"): buf.extend((text[0:-2], "\\r\\n''', ")) |
| 805 | elif text.endswith("\n"): buf.extend((text[0:-1], "\\n''', ")) |
| 806 | else: buf.extend((text, "''', ")) |
| 807 | |
| 808 | _add_text = add_text |
| 809 | |
| 810 | def add_expr(self, buf, code, *flags): |
| 811 | if not code or code.isspace(): return |
| 812 | flag_escape, flag_tostr = flags |
| 813 | if not self.tostrfunc: flag_tostr = False |
| 814 | if not self.escapefunc: flag_escape = False |
| 815 | if flag_tostr and flag_escape: s1, s2 = "_escape(_to_str(", ")), " |
| 816 | elif flag_tostr: s1, s2 = "_to_str(", "), " |
| 817 | elif flag_escape: s1, s2 = "_escape(", "), " |
| 818 | else: s1, s2 = "(", "), " |
| 819 | buf.extend((s1, code, s2, )) |
| 820 | |
| 821 | def add_stmt(self, buf, code): |
| 822 | if not code: return |
| 823 | lines = code.splitlines(True) # keep "\n" |
| 824 | if lines[-1][-1] != "\n": |
| 825 | lines[-1] = lines[-1] + "\n" |
| 826 | buf.extend(lines) |
| 827 | self._add_localvars_assignments_to_stmts(buf) |
| 828 | |
| 829 | def _add_localvars_assignments_to_stmts(self, buf): |
| 830 | if self._localvars_assignments_added: |
| 831 | return |
| 832 | for index, stmt in enumerate(buf): |
| 833 | if not re.match(r'^[ \t]*(?:\#|_buf ?= ?\[\]|from __future__)', stmt): |
| 834 | break |
| 835 | else: |
| 836 | return |
| 837 | self._localvars_assignments_added = True |
| 838 | if re.match(r'^[ \t]*(if|for|while|def|with|class)\b', stmt): |
| 839 | buf.insert(index, self._localvars_assignments() + "\n") |
| 840 | else: |
| 841 | buf[index] = self._localvars_assignments() + buf[index] |
| 842 | |
| 843 | |
| 844 | _START_WORDS = dict.fromkeys(('for', 'if', 'while', 'def', 'try:', 'with', 'class'), True) |
| 845 | _END_WORDS = dict.fromkeys(('#end', '#endfor', '#endif', '#endwhile', '#enddef', '#endtry', '#endwith', '#endclass'), True) |
| 846 | _CONT_WORDS = dict.fromkeys(('elif', 'else:', 'except', 'except:', 'finally:'), True) |
| 847 | _WORD_REXP = re.compile(r'\S+') |
| 848 | |
| 849 | depth = -1 |
| 850 | |
| 851 | ## |
| 852 | ## ex. |
| 853 | ## input = r""" |
| 854 | ## if items: |
| 855 | ## _buf.extend(('<ul>\n', )) |
| 856 | ## i = 0 |
| 857 | ## for item in items: |
| 858 | ## i += 1 |
| 859 | ## _buf.extend(('<li>', to_str(item), '</li>\n', )) |
| 860 | ## #endfor |
| 861 | ## _buf.extend(('</ul>\n', )) |
| 862 | ## #endif |
| 863 | ## """[1:] |
| 864 | ## lines = input.splitlines(True) |
| 865 | ## block = self.parse_lines(lines) |
| 866 | ## #=> [ "if items:\n", |
| 867 | ## [ "_buf.extend(('<ul>\n', ))\n", |
| 868 | ## "i = 0\n", |
| 869 | ## "for item in items:\n", |
| 870 | ## [ "i += 1\n", |
| 871 | ## "_buf.extend(('<li>', to_str(item), '</li>\n', ))\n", |
| 872 | ## ], |
| 873 | ## "#endfor\n", |
| 874 | ## "_buf.extend(('</ul>\n', ))\n", |
| 875 | ## ], |
| 876 | ## "#endif\n", |
| 877 | ## ] |
| 878 | def parse_lines(self, lines): |
| 879 | block = [] |
| 880 | try: |
| 881 | self._parse_lines(lines.__iter__(), False, block, 0) |
| 882 | except StopIteration: |
| 883 | if self.depth > 0: |
| 884 | fname, linenum, colnum, linetext = self.filename, len(lines), None, None |
| 885 | raise TemplateSyntaxError("unexpected EOF.", (fname, linenum, colnum, linetext)) |
| 886 | else: |
| 887 | pass |
| 888 | return block |
| 889 | |
| 890 | def _parse_lines(self, lines_iter, end_block, block, linenum): |
| 891 | if block is None: block = [] |
| 892 | _START_WORDS = self._START_WORDS |
| 893 | _END_WORDS = self._END_WORDS |
| 894 | _CONT_WORDS = self._CONT_WORDS |
| 895 | _WORD_REXP = self._WORD_REXP |
| 896 | get_line = lines_iter.next |
| 897 | while True: |
| 898 | line = get_line() |
| 899 | linenum += line.count("\n") |
| 900 | m = _WORD_REXP.search(line) |
| 901 | if not m: |
| 902 | block.append(line) |
| 903 | continue |
| 904 | word = m.group(0) |
| 905 | if word in _END_WORDS: |
| 906 | if word != end_block and word != '#end': |
| 907 | if end_block is False: |
| 908 | msg = "'%s' found but corresponding statement is missing." % (word, ) |
| 909 | else: |
| 910 | msg = "'%s' expected but got '%s'." % (end_block, word) |
| 911 | colnum = m.start() + 1 |
| 912 | raise TemplateSyntaxError(msg, (self.filename, linenum, colnum, line)) |
| 913 | return block, line, None, linenum |
| 914 | elif line.endswith(':\n') or line.endswith(':\r\n'): |
| 915 | if word in _CONT_WORDS: |
| 916 | return block, line, word, linenum |
| 917 | elif word in _START_WORDS: |
| 918 | block.append(line) |
| 919 | self.depth += 1 |
| 920 | cont_word = None |
| 921 | try: |
| 922 | child_block, line, cont_word, linenum = \ |
| 923 | self._parse_lines(lines_iter, '#end'+word, [], linenum) |
| 924 | block.extend((child_block, line, )) |
| 925 | while cont_word: # 'elif' or 'else:' |
| 926 | child_block, line, cont_word, linenum = \ |
| 927 | self._parse_lines(lines_iter, '#end'+word, [], linenum) |
| 928 | block.extend((child_block, line, )) |
| 929 | except StopIteration: |
| 930 | msg = "'%s' is not closed." % (cont_word or word) |
| 931 | colnum = m.start() + 1 |
| 932 | raise TemplateSyntaxError(msg, (self.filename, linenum, colnum, line)) |
| 933 | self.depth -= 1 |
| 934 | else: |
| 935 | block.append(line) |
| 936 | else: |
| 937 | block.append(line) |
| 938 | assert "unreachable" |
| 939 | |
| 940 | def _join_block(self, block, buf, depth): |
| 941 | indent = ' ' * (self.indent * depth) |
| 942 | for line in block: |
| 943 | if isinstance(line, list): |
| 944 | self._join_block(line, buf, depth+1) |
| 945 | elif line.isspace(): |
| 946 | buf.append(line) |
| 947 | else: |
| 948 | buf.append(indent + line.lstrip()) |
| 949 | |
| 950 | def _arrange_indent(self, buf): |
| 951 | """arrange indentation of statements in buf""" |
| 952 | block = self.parse_lines(buf) |
| 953 | buf[:] = [] |
| 954 | self._join_block(block, buf, 0) |
| 955 | |
| 956 | |
| 957 | def render(self, context=None, globals=None, _buf=None): |
| 958 | """Evaluate python code with context dictionary. |
| 959 | If _buf is None then return the result of evaluation as str, |
| 960 | else return None. |
| 961 | |
| 962 | context:dict (=None) |
| 963 | Context object to evaluate. If None then new dict is created. |
| 964 | globals:dict (=None) |
| 965 | Global object. If None then globals() is used. |
| 966 | _buf:list (=None) |
| 967 | If None then new list is created. |
| 968 | """ |
| 969 | if context is None: |
| 970 | locals = context = {} |
| 971 | elif self.args is None: |
| 972 | locals = context.copy() |
| 973 | else: |
| 974 | locals = {} |
| 975 | if '_engine' in context: |
| 976 | context.get('_engine').hook_context(locals) |
| 977 | locals['_context'] = context |
| 978 | if globals is None: |
| 979 | globals = sys._getframe(1).f_globals |
| 980 | bufarg = _buf |
| 981 | if _buf is None: |
| 982 | _buf = [] |
| 983 | locals['_buf'] = _buf |
| 984 | if not self.bytecode: |
| 985 | self.compile() |
| 986 | if self.trace: |
| 987 | _buf.append("<!-- ***** begin: %s ***** -->\n" % self.filename) |
| 988 | exec(self.bytecode, globals, locals) |
| 989 | _buf.append("<!-- ***** end: %s ***** -->\n" % self.filename) |
| 990 | else: |
| 991 | exec(self.bytecode, globals, locals) |
| 992 | if bufarg is not None: |
| 993 | return bufarg |
| 994 | elif not logger: |
| 995 | return ''.join(_buf) |
| 996 | else: |
| 997 | try: |
| 998 | return ''.join(_buf) |
| 999 | except UnicodeDecodeError, ex: |
| 1000 | logger.error("[tenjin.Template] " + str(ex)) |
| 1001 | logger.error("[tenjin.Template] (_buf=%r)" % (_buf, )) |
| 1002 | raise |
| 1003 | |
| 1004 | def compile(self): |
| 1005 | """compile self.script into self.bytecode""" |
| 1006 | self.bytecode = compile(self.script, self.filename or '(tenjin)', 'exec') |
| 1007 | |
| 1008 | |
| 1009 | ## |
| 1010 | ## preprocessor class |
| 1011 | ## |
| 1012 | |
| 1013 | class Preprocessor(Template): |
| 1014 | """Template class for preprocessing.""" |
| 1015 | |
| 1016 | STMT_PATTERN = (r'<\?PY( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?', re.S) |
| 1017 | |
| 1018 | EXPR_PATTERN = (r'#\{\{(.*?)\}\}|\$\{\{(.*?)\}\}|\{#=(?:=(.*?)=|(.*?))=#\}', re.S) |
| 1019 | |
| 1020 | def add_expr(self, buf, code, *flags): |
| 1021 | if not code or code.isspace(): |
| 1022 | return |
| 1023 | code = "_decode_params(%s)" % code |
| 1024 | Template.add_expr(self, buf, code, *flags) |
| 1025 | |
| 1026 | |
| 1027 | class TemplatePreprocessor(object): |
| 1028 | factory = Preprocessor |
| 1029 | |
| 1030 | def __init__(self, factory=None): |
| 1031 | if factory is not None: self.factory = factory |
| 1032 | self.globals = sys._getframe(1).f_globals |
| 1033 | |
| 1034 | def __call__(self, input, **kwargs): |
| 1035 | filename = kwargs.get('filename') |
| 1036 | context = kwargs.get('context') or {} |
| 1037 | globals = kwargs.get('globals') or self.globals |
| 1038 | template = self.factory() |
| 1039 | template.convert(input, filename) |
| 1040 | return template.render(context, globals=globals) |
| 1041 | |
| 1042 | |
| 1043 | class TrimPreprocessor(object): |
| 1044 | |
| 1045 | _rexp = re.compile(r'^[ \t]+<', re.M) |
| 1046 | _rexp_all = re.compile(r'^[ \t]+', re.M) |
| 1047 | |
| 1048 | def __init__(self, all=False): |
| 1049 | self.all = all |
| 1050 | |
| 1051 | def __call__(self, input, **kwargs): |
| 1052 | if self.all: |
| 1053 | return self._rexp_all.sub('', input) |
| 1054 | else: |
| 1055 | return self._rexp.sub('<', input) |
| 1056 | |
| 1057 | |
| 1058 | class PrefixedLinePreprocessor(object): |
| 1059 | |
| 1060 | def __init__(self, prefix='::(?=[ \t]|$)'): |
| 1061 | self.prefix = prefix |
| 1062 | self.regexp = re.compile(r'^([ \t]*)' + prefix + r'(.*)', re.M) |
| 1063 | |
| 1064 | def convert_prefixed_lines(self, text): |
| 1065 | fn = lambda m: "%s<?py%s ?>" % (m.group(1), m.group(2)) |
| 1066 | return self.regexp.sub(fn, text) |
| 1067 | |
| 1068 | STMT_REXP = re.compile(r'<\?py\s.*?\?>', re.S) |
| 1069 | |
| 1070 | def __call__(self, input, **kwargs): |
| 1071 | buf = []; append = buf.append |
| 1072 | pos = 0 |
| 1073 | for m in self.STMT_REXP.finditer(input): |
| 1074 | text = input[pos:m.start()] |
| 1075 | stmt = m.group(0) |
| 1076 | pos = m.end() |
| 1077 | if text: append(self.convert_prefixed_lines(text)) |
| 1078 | append(stmt) |
| 1079 | rest = input[pos:] |
| 1080 | if rest: append(self.convert_prefixed_lines(rest)) |
| 1081 | return "".join(buf) |
| 1082 | |
| 1083 | |
| 1084 | class ParseError(Exception): |
| 1085 | pass |
| 1086 | |
| 1087 | |
| 1088 | class JavaScriptPreprocessor(object): |
| 1089 | |
| 1090 | def __init__(self, **attrs): |
| 1091 | self._attrs = attrs |
| 1092 | |
| 1093 | def __call__(self, input, **kwargs): |
| 1094 | return self.parse(input, kwargs.get('filename')) |
| 1095 | |
| 1096 | def parse(self, input, filename=None): |
| 1097 | buf = [] |
| 1098 | self._parse_chunks(input, buf, filename) |
| 1099 | return ''.join(buf) |
| 1100 | |
| 1101 | CHUNK_REXP = re.compile(r'(?:^( *)<|<)!-- *#(?:JS: (\$?\w+(?:\.\w+)*\(.*?\))|/JS:?) *-->([ \t]*\r?\n)?', re.M) |
| 1102 | |
| 1103 | def _scan_chunks(self, input, filename): |
| 1104 | rexp = self.CHUNK_REXP |
| 1105 | pos = 0 |
| 1106 | curr_funcdecl = None |
| 1107 | for m in rexp.finditer(input): |
| 1108 | lspace, funcdecl, rspace = m.groups() |
| 1109 | text = input[pos:m.start()] |
| 1110 | pos = m.end() |
| 1111 | if funcdecl: |
| 1112 | if curr_funcdecl: |
| 1113 | raise ParseError("%s is nested in %s. (file: %s, line: %s)" % \ |
| 1114 | (funcdecl, curr_funcdecl, filename, _linenum(input, m.start()), )) |
| 1115 | curr_funcdecl = funcdecl |
| 1116 | else: |
| 1117 | if not curr_funcdecl: |
| 1118 | raise ParseError("unexpected '<!-- #/JS -->'. (file: %s, line: %s)" % \ |
| 1119 | (filename, _linenum(input, m.start()), )) |
| 1120 | curr_funcdecl = None |
| 1121 | yield text, lspace, funcdecl, rspace, False |
| 1122 | if curr_funcdecl: |
| 1123 | raise ParseError("%s is not closed by '<!-- #/JS -->'. (file: %s, line: %s)" % \ |
| 1124 | (curr_funcdecl, filename, _linenum(input, m.start()), )) |
| 1125 | rest = input[pos:] |
| 1126 | yield rest, None, None, None, True |
| 1127 | |
| 1128 | def _parse_chunks(self, input, buf, filename=None): |
| 1129 | if not input: return |
| 1130 | stag = '<script' |
| 1131 | if self._attrs: |
| 1132 | for k in self._attrs: |
| 1133 | stag = "".join((stag, ' ', k, '="', self._attrs[k], '"')) |
| 1134 | stag += '>' |
| 1135 | etag = '</script>' |
| 1136 | for text, lspace, funcdecl, rspace, end_p in self._scan_chunks(input, filename): |
| 1137 | if end_p: break |
| 1138 | if funcdecl: |
| 1139 | buf.append(text) |
| 1140 | if re.match(r'^\$?\w+\(', funcdecl): |
| 1141 | buf.extend((lspace or '', stag, 'function ', funcdecl, "{var _buf='';", rspace or '')) |
| 1142 | else: |
| 1143 | m = re.match(r'(.+?)\((.*)\)', funcdecl) |
| 1144 | buf.extend((lspace or '', stag, m.group(1), '=function(', m.group(2), "){var _buf='';", rspace or '')) |
| 1145 | else: |
| 1146 | self._parse_stmts(text, buf) |
| 1147 | buf.extend((lspace or '', "return _buf;};", etag, rspace or '')) |
| 1148 | # |
| 1149 | buf.append(text) |
| 1150 | |
| 1151 | STMT_REXP = re.compile(r'(?:^( *)<|<)\?js(\s.*?) ?\?>([ \t]*\r?\n)?', re.M | re.S) |
| 1152 | |
| 1153 | def _scan_stmts(self, input): |
| 1154 | rexp = self.STMT_REXP |
| 1155 | pos = 0 |
| 1156 | for m in rexp.finditer(input): |
| 1157 | lspace, code, rspace = m.groups() |
| 1158 | text = input[pos:m.start()] |
| 1159 | pos = m.end() |
| 1160 | yield text, lspace, code, rspace, False |
| 1161 | rest = input[pos:] |
| 1162 | yield rest, None, None, None, True |
| 1163 | |
| 1164 | def _parse_stmts(self, input, buf): |
| 1165 | if not input: return |
| 1166 | for text, lspace, code, rspace, end_p in self._scan_stmts(input): |
| 1167 | if end_p: break |
| 1168 | if lspace is not None and rspace is not None: |
| 1169 | self._parse_exprs(text, buf) |
| 1170 | buf.extend((lspace, code, rspace)) |
| 1171 | else: |
| 1172 | if lspace: |
| 1173 | text += lspace |
| 1174 | self._parse_exprs(text, buf) |
| 1175 | buf.append(code) |
| 1176 | if rspace: |
| 1177 | self._parse_exprs(rspace, buf) |
| 1178 | if text: |
| 1179 | self._parse_exprs(text, buf) |
| 1180 | |
| 1181 | s = r'(?:\{[^{}]*?\}[^{}]*?)*' |
| 1182 | EXPR_REXP = re.compile(r'\{=(.*?)=\}|([$#])\{(.*?' + s + r')\}', re.S) |
| 1183 | del s |
| 1184 | |
| 1185 | def _get_expr(self, m): |
| 1186 | code1, ch, code2 = m.groups() |
| 1187 | if ch: |
| 1188 | code = code2 |
| 1189 | escape_p = ch == '$' |
| 1190 | elif code1[0] == code1[-1] == '=': |
| 1191 | code = code1[1:-1] |
| 1192 | escape_p = False |
| 1193 | else: |
| 1194 | code = code1 |
| 1195 | escape_p = True |
| 1196 | return code, escape_p |
| 1197 | |
| 1198 | def _scan_exprs(self, input): |
| 1199 | rexp = self.EXPR_REXP |
| 1200 | pos = 0 |
| 1201 | for m in rexp.finditer(input): |
| 1202 | text = input[pos:m.start()] |
| 1203 | pos = m.end() |
| 1204 | code, escape_p = self._get_expr(m) |
| 1205 | yield text, code, escape_p, False |
| 1206 | rest = input[pos:] |
| 1207 | yield rest, None, None, True |
| 1208 | |
| 1209 | def _parse_exprs(self, input, buf): |
| 1210 | if not input: return |
| 1211 | buf.append("_buf+=") |
| 1212 | extend = buf.extend |
| 1213 | op = '' |
| 1214 | for text, code, escape_p, end_p in self._scan_exprs(input): |
| 1215 | if end_p: |
| 1216 | break |
| 1217 | if text: |
| 1218 | extend((op, self._escape_text(text))) |
| 1219 | op = '+' |
| 1220 | if code: |
| 1221 | extend((op, escape_p and '_E(' or '_S(', code, ')')) |
| 1222 | op = '+' |
| 1223 | rest = text |
| 1224 | if rest: |
| 1225 | extend((op, self._escape_text(rest))) |
| 1226 | if input.endswith("\n"): |
| 1227 | buf.append(";\n") |
| 1228 | else: |
| 1229 | buf.append(";") |
| 1230 | |
| 1231 | def _escape_text(self, text): |
| 1232 | lines = text.splitlines(True) |
| 1233 | fn = self._escape_str |
| 1234 | s = "\\\n".join( fn(line) for line in lines ) |
| 1235 | return "".join(("'", s, "'")) |
| 1236 | |
| 1237 | def _escape_str(self, string): |
| 1238 | return string.replace("\\", "\\\\").replace("'", "\\'").replace("\n", r"\n") |
| 1239 | |
| 1240 | |
| 1241 | def _linenum(input, pos): |
| 1242 | return input[0:pos].count("\n") + 1 |
| 1243 | |
| 1244 | |
| 1245 | JS_FUNC = r""" |
| 1246 | function _S(x){return x==null?'':x;} |
| 1247 | function _E(x){return x==null?'':typeof(x)!=='string'?x:x.replace(/[&<>"']/g,_EF);} |
| 1248 | var _ET={'&':"&",'<':"<",'>':">",'"':""","'":"'"}; |
| 1249 | function _EF(c){return _ET[c];}; |
| 1250 | """[1:-1] |
| 1251 | JS_FUNC = escaped.EscapedStr(JS_FUNC) |
| 1252 | |
| 1253 | |
| 1254 | |
| 1255 | ## |
| 1256 | ## cache storages |
| 1257 | ## |
| 1258 | |
| 1259 | class CacheStorage(object): |
| 1260 | """[abstract] Template object cache class (in memory and/or file)""" |
| 1261 | |
| 1262 | def __init__(self): |
| 1263 | self.items = {} # key: full path, value: template object |
| 1264 | |
| 1265 | def get(self, cachepath, create_template): |
| 1266 | """get template object. if not found, load attributes from cache file and restore template object.""" |
| 1267 | template = self.items.get(cachepath) |
| 1268 | if not template: |
| 1269 | dct = self._load(cachepath) |
| 1270 | if dct: |
| 1271 | template = create_template() |
| 1272 | for k in dct: |
| 1273 | setattr(template, k, dct[k]) |
| 1274 | self.items[cachepath] = template |
| 1275 | return template |
| 1276 | |
| 1277 | def set(self, cachepath, template): |
| 1278 | """set template object and save template attributes into cache file.""" |
| 1279 | self.items[cachepath] = template |
| 1280 | dct = self._save_data_of(template) |
| 1281 | return self._store(cachepath, dct) |
| 1282 | |
| 1283 | def _save_data_of(self, template): |
| 1284 | return { 'args' : template.args, 'bytecode' : template.bytecode, |
| 1285 | 'script': template.script, 'timestamp': template.timestamp } |
| 1286 | |
| 1287 | def unset(self, cachepath): |
| 1288 | """remove template object from dict and cache file.""" |
| 1289 | self.items.pop(cachepath, None) |
| 1290 | return self._delete(cachepath) |
| 1291 | |
| 1292 | def clear(self): |
| 1293 | """remove all template objects and attributes from dict and cache file.""" |
| 1294 | d, self.items = self.items, {} |
| 1295 | for k in d.iterkeys(): |
| 1296 | self._delete(k) |
| 1297 | d.clear() |
| 1298 | |
| 1299 | def _load(self, cachepath): |
| 1300 | """(abstract) load dict object which represents template object attributes from cache file.""" |
| 1301 | raise NotImplementedError.new("%s#_load(): not implemented yet." % self.__class__.__name__) |
| 1302 | |
| 1303 | def _store(self, cachepath, template): |
| 1304 | """(abstract) load dict object which represents template object attributes from cache file.""" |
| 1305 | raise NotImplementedError.new("%s#_store(): not implemented yet." % self.__class__.__name__) |
| 1306 | |
| 1307 | def _delete(self, cachepath): |
| 1308 | """(abstract) remove template object from cache file.""" |
| 1309 | raise NotImplementedError.new("%s#_delete(): not implemented yet." % self.__class__.__name__) |
| 1310 | |
| 1311 | |
| 1312 | class MemoryCacheStorage(CacheStorage): |
| 1313 | |
| 1314 | def _load(self, cachepath): |
| 1315 | return None |
| 1316 | |
| 1317 | def _store(self, cachepath, template): |
| 1318 | pass |
| 1319 | |
| 1320 | def _delete(self, cachepath): |
| 1321 | pass |
| 1322 | |
| 1323 | |
| 1324 | class FileCacheStorage(CacheStorage): |
| 1325 | |
| 1326 | def _load(self, cachepath): |
| 1327 | if not _isfile(cachepath): return None |
| 1328 | if logger: logger.info("[tenjin.%s] load cache (file=%r)" % (self.__class__.__name__, cachepath)) |
| 1329 | data = _read_binary_file(cachepath) |
| 1330 | return self._restore(data) |
| 1331 | |
| 1332 | def _store(self, cachepath, dct): |
| 1333 | if logger: logger.info("[tenjin.%s] store cache (file=%r)" % (self.__class__.__name__, cachepath)) |
| 1334 | data = self._dump(dct) |
| 1335 | _write_binary_file(cachepath, data) |
| 1336 | |
| 1337 | def _restore(self, data): |
| 1338 | raise NotImplementedError("%s._restore(): not implemented yet." % self.__class__.__name__) |
| 1339 | |
| 1340 | def _dump(self, dct): |
| 1341 | raise NotImplementedError("%s._dump(): not implemented yet." % self.__class__.__name__) |
| 1342 | |
| 1343 | def _delete(self, cachepath): |
| 1344 | _ignore_not_found_error(lambda: os.unlink(cachepath)) |
| 1345 | |
| 1346 | |
| 1347 | class MarshalCacheStorage(FileCacheStorage): |
| 1348 | |
| 1349 | def _restore(self, data): |
| 1350 | return marshal.loads(data) |
| 1351 | |
| 1352 | def _dump(self, dct): |
| 1353 | return marshal.dumps(dct) |
| 1354 | |
| 1355 | |
| 1356 | class PickleCacheStorage(FileCacheStorage): |
| 1357 | |
| 1358 | def __init__(self, *args, **kwargs): |
| 1359 | global pickle |
| 1360 | if pickle is None: |
| 1361 | import cPickle as pickle |
| 1362 | FileCacheStorage.__init__(self, *args, **kwargs) |
| 1363 | |
| 1364 | def _restore(self, data): |
| 1365 | return pickle.loads(data) |
| 1366 | |
| 1367 | def _dump(self, dct): |
| 1368 | dct.pop('bytecode', None) |
| 1369 | return pickle.dumps(dct) |
| 1370 | |
| 1371 | |
| 1372 | class TextCacheStorage(FileCacheStorage): |
| 1373 | |
| 1374 | def _restore(self, data): |
| 1375 | header, script = data.split("\n\n", 1) |
| 1376 | timestamp = encoding = args = None |
| 1377 | for line in header.split("\n"): |
| 1378 | key, val = line.split(": ", 1) |
| 1379 | if key == 'timestamp': timestamp = float(val) |
| 1380 | elif key == 'encoding': encoding = val |
| 1381 | elif key == 'args': args = val.split(', ') |
| 1382 | if encoding: script = script.decode(encoding) ## binary(=str) to unicode |
| 1383 | return {'args': args, 'script': script, 'timestamp': timestamp} |
| 1384 | |
| 1385 | def _dump(self, dct): |
| 1386 | s = dct['script'] |
| 1387 | if dct.get('encoding') and isinstance(s, unicode): |
| 1388 | s = s.encode(dct['encoding']) ## unicode to binary(=str) |
| 1389 | sb = [] |
| 1390 | sb.append("timestamp: %s\n" % dct['timestamp']) |
| 1391 | if dct.get('encoding'): |
| 1392 | sb.append("encoding: %s\n" % dct['encoding']) |
| 1393 | if dct.get('args') is not None: |
| 1394 | sb.append("args: %s\n" % ', '.join(dct['args'])) |
| 1395 | sb.append("\n") |
| 1396 | sb.append(s) |
| 1397 | s = ''.join(sb) |
| 1398 | if python3: |
| 1399 | if isinstance(s, str): |
| 1400 | s = s.encode(dct.get('encoding') or 'utf-8') ## unicode(=str) to binary |
| 1401 | return s |
| 1402 | |
| 1403 | def _save_data_of(self, template): |
| 1404 | dct = FileCacheStorage._save_data_of(self, template) |
| 1405 | dct['encoding'] = template.encoding |
| 1406 | return dct |
| 1407 | |
| 1408 | |
| 1409 | |
| 1410 | ## |
| 1411 | ## abstract class for data cache |
| 1412 | ## |
| 1413 | class KeyValueStore(object): |
| 1414 | |
| 1415 | def get(self, key, *options): |
| 1416 | raise NotImplementedError("%s.get(): not implemented yet." % self.__class__.__name__) |
| 1417 | |
| 1418 | def set(self, key, value, *options): |
| 1419 | raise NotImplementedError("%s.set(): not implemented yet." % self.__class__.__name__) |
| 1420 | |
| 1421 | def delete(self, key, *options): |
| 1422 | raise NotImplementedError("%s.del(): not implemented yet." % self.__class__.__name__) |
| 1423 | |
| 1424 | def has(self, key, *options): |
| 1425 | raise NotImplementedError("%s.has(): not implemented yet." % self.__class__.__name__) |
| 1426 | |
| 1427 | |
| 1428 | ## |
| 1429 | ## memory base data cache |
| 1430 | ## |
| 1431 | class MemoryBaseStore(KeyValueStore): |
| 1432 | |
| 1433 | def __init__(self): |
| 1434 | self.values = {} |
| 1435 | |
| 1436 | def get(self, key, original_timestamp=None): |
| 1437 | tupl = self.values.get(key) |
| 1438 | if not tupl: |
| 1439 | return None |
| 1440 | value, created_at, expires_at = tupl |
| 1441 | if original_timestamp is not None and created_at < original_timestamp: |
| 1442 | self.delete(key) |
| 1443 | return None |
| 1444 | if expires_at < _time(): |
| 1445 | self.delete(key) |
| 1446 | return None |
| 1447 | return value |
| 1448 | |
| 1449 | def set(self, key, value, lifetime=0): |
| 1450 | created_at = _time() |
| 1451 | expires_at = lifetime and created_at + lifetime or 0 |
| 1452 | self.values[key] = (value, created_at, expires_at) |
| 1453 | return True |
| 1454 | |
| 1455 | def delete(self, key): |
| 1456 | try: |
| 1457 | del self.values[key] |
| 1458 | return True |
| 1459 | except KeyError: |
| 1460 | return False |
| 1461 | |
| 1462 | def has(self, key): |
| 1463 | pair = self.values.get(key) |
| 1464 | if not pair: |
| 1465 | return False |
| 1466 | value, created_at, expires_at = pair |
| 1467 | if expires_at and expires_at < _time(): |
| 1468 | self.delete(key) |
| 1469 | return False |
| 1470 | return True |
| 1471 | |
| 1472 | |
| 1473 | ## |
| 1474 | ## file base data cache |
| 1475 | ## |
| 1476 | class FileBaseStore(KeyValueStore): |
| 1477 | |
| 1478 | lifetime = 604800 # = 60*60*24*7 |
| 1479 | |
| 1480 | def __init__(self, root_path, encoding=None): |
| 1481 | if not os.path.isdir(root_path): |
| 1482 | raise ValueError("%r: directory not found." % (root_path, )) |
| 1483 | self.root_path = root_path |
| 1484 | if encoding is None and python3: |
| 1485 | encoding = 'utf-8' |
| 1486 | self.encoding = encoding |
| 1487 | |
| 1488 | _pat = re.compile(r'[^-.\/\w]') |
| 1489 | |
| 1490 | def filepath(self, key, _pat1=_pat): |
| 1491 | return os.path.join(self.root_path, _pat1.sub('_', key)) |
| 1492 | |
| 1493 | def get(self, key, original_timestamp=None): |
| 1494 | fpath = self.filepath(key) |
| 1495 | #if not _isfile(fpath): return None |
| 1496 | stat = _ignore_not_found_error(lambda: os.stat(fpath), None) |
| 1497 | if stat is None: |
| 1498 | return None |
| 1499 | created_at = stat.st_ctime |
| 1500 | expires_at = stat.st_mtime |
| 1501 | if original_timestamp is not None and created_at < original_timestamp: |
| 1502 | self.delete(key) |
| 1503 | return None |
| 1504 | if expires_at < _time(): |
| 1505 | self.delete(key) |
| 1506 | return None |
| 1507 | if self.encoding: |
| 1508 | f = lambda: _read_text_file(fpath, self.encoding) |
| 1509 | else: |
| 1510 | f = lambda: _read_binary_file(fpath) |
| 1511 | return _ignore_not_found_error(f, None) |
| 1512 | |
| 1513 | def set(self, key, value, lifetime=0): |
| 1514 | fpath = self.filepath(key) |
| 1515 | dirname = os.path.dirname(fpath) |
| 1516 | if not os.path.isdir(dirname): |
| 1517 | os.makedirs(dirname) |
| 1518 | now = _time() |
| 1519 | if isinstance(value, _unicode): |
| 1520 | value = value.encode(self.encoding or 'utf-8') |
| 1521 | _write_binary_file(fpath, value) |
| 1522 | expires_at = now + (lifetime or self.lifetime) # timestamp |
| 1523 | os.utime(fpath, (expires_at, expires_at)) |
| 1524 | return True |
| 1525 | |
| 1526 | def delete(self, key): |
| 1527 | fpath = self.filepath(key) |
| 1528 | ret = _ignore_not_found_error(lambda: os.unlink(fpath), False) |
| 1529 | return ret != False |
| 1530 | |
| 1531 | def has(self, key): |
| 1532 | fpath = self.filepath(key) |
| 1533 | if not _isfile(fpath): |
| 1534 | return False |
| 1535 | if _getmtime(fpath) < _time(): |
| 1536 | self.delete(key) |
| 1537 | return False |
| 1538 | return True |
| 1539 | |
| 1540 | |
| 1541 | |
| 1542 | ## |
| 1543 | ## html fragment cache helper class |
| 1544 | ## |
| 1545 | class FragmentCacheHelper(object): |
| 1546 | """html fragment cache helper class.""" |
| 1547 | |
| 1548 | lifetime = 60 # 1 minute |
| 1549 | prefix = None |
| 1550 | |
| 1551 | def __init__(self, store, lifetime=None, prefix=None): |
| 1552 | self.store = store |
| 1553 | if lifetime is not None: self.lifetime = lifetime |
| 1554 | if prefix is not None: self.prefix = prefix |
| 1555 | |
| 1556 | def not_cached(self, cache_key, lifetime=None): |
| 1557 | """(obsolete. use cache_as() instead of this.) |
| 1558 | html fragment cache helper. see document of FragmentCacheHelper class.""" |
| 1559 | context = sys._getframe(1).f_locals['_context'] |
| 1560 | context['_cache_key'] = cache_key |
| 1561 | key = self.prefix and self.prefix + cache_key or cache_key |
| 1562 | value = self.store.get(key) |
| 1563 | if value: ## cached |
| 1564 | if logger: logger.debug('[tenjin.not_cached] %r: cached.' % (cache_key, )) |
| 1565 | context[key] = value |
| 1566 | return False |
| 1567 | else: ## not cached |
| 1568 | if logger: logger.debug('[tenjin.not_cached]: %r: not cached.' % (cache_key, )) |
| 1569 | if key in context: del context[key] |
| 1570 | if lifetime is None: lifetime = self.lifetime |
| 1571 | context['_cache_lifetime'] = lifetime |
| 1572 | helpers.start_capture(cache_key, _depth=2) |
| 1573 | return True |
| 1574 | |
| 1575 | def echo_cached(self): |
| 1576 | """(obsolete. use cache_as() instead of this.) |
| 1577 | html fragment cache helper. see document of FragmentCacheHelper class.""" |
| 1578 | f_locals = sys._getframe(1).f_locals |
| 1579 | context = f_locals['_context'] |
| 1580 | cache_key = context.pop('_cache_key') |
| 1581 | key = self.prefix and self.prefix + cache_key or cache_key |
| 1582 | if key in context: ## cached |
| 1583 | value = context.pop(key) |
| 1584 | else: ## not cached |
| 1585 | value = helpers.stop_capture(False, _depth=2) |
| 1586 | lifetime = context.pop('_cache_lifetime') |
| 1587 | self.store.set(key, value, lifetime) |
| 1588 | f_locals['_buf'].append(value) |
| 1589 | |
| 1590 | def functions(self): |
| 1591 | """(obsolete. use cache_as() instead of this.)""" |
| 1592 | return (self.not_cached, self.echo_cached) |
| 1593 | |
| 1594 | def cache_as(self, cache_key, lifetime=None): |
| 1595 | key = self.prefix and self.prefix + cache_key or cache_key |
| 1596 | _buf = sys._getframe(1).f_locals['_buf'] |
| 1597 | value = self.store.get(key) |
| 1598 | if value: |
| 1599 | if logger: logger.debug('[tenjin.cache_as] %r: cache found.' % (cache_key, )) |
| 1600 | _buf.append(value) |
| 1601 | else: |
| 1602 | if logger: logger.debug('[tenjin.cache_as] %r: expired or not cached yet.' % (cache_key, )) |
| 1603 | _buf_len = len(_buf) |
| 1604 | yield None |
| 1605 | value = ''.join(_buf[_buf_len:]) |
| 1606 | self.store.set(key, value, lifetime) |
| 1607 | |
| 1608 | ## you can change default store by 'tenjin.helpers.fragment_cache.store = ...' |
| 1609 | helpers.fragment_cache = FragmentCacheHelper(MemoryBaseStore()) |
| 1610 | helpers.not_cached = helpers.fragment_cache.not_cached |
| 1611 | helpers.echo_cached = helpers.fragment_cache.echo_cached |
| 1612 | helpers.cache_as = helpers.fragment_cache.cache_as |
| 1613 | helpers.__all__.extend(('not_cached', 'echo_cached', 'cache_as')) |
| 1614 | |
| 1615 | |
| 1616 | |
| 1617 | ## |
| 1618 | ## helper class to find and read template |
| 1619 | ## |
| 1620 | class Loader(object): |
| 1621 | |
| 1622 | def exists(self, filepath): |
| 1623 | raise NotImplementedError("%s.exists(): not implemented yet." % self.__class__.__name__) |
| 1624 | |
| 1625 | def find(self, filename, dirs=None): |
| 1626 | #: if dirs provided then search template file from it. |
| 1627 | if dirs: |
| 1628 | for dirname in dirs: |
| 1629 | filepath = os.path.join(dirname, filename) |
| 1630 | if self.exists(filepath): |
| 1631 | return filepath |
| 1632 | #: if dirs not provided then just return filename if file exists. |
| 1633 | else: |
| 1634 | if self.exists(filename): |
| 1635 | return filename |
| 1636 | #: if file not found then return None. |
| 1637 | return None |
| 1638 | |
| 1639 | def abspath(self, filename): |
| 1640 | raise NotImplementedError("%s.abspath(): not implemented yet." % self.__class__.__name__) |
| 1641 | |
| 1642 | def timestamp(self, filepath): |
| 1643 | raise NotImplementedError("%s.timestamp(): not implemented yet." % self.__class__.__name__) |
| 1644 | |
| 1645 | def load(self, filepath): |
| 1646 | raise NotImplementedError("%s.timestamp(): not implemented yet." % self.__class__.__name__) |
| 1647 | |
| 1648 | |
| 1649 | |
| 1650 | ## |
| 1651 | ## helper class to find and read files |
| 1652 | ## |
| 1653 | class FileSystemLoader(Loader): |
| 1654 | |
| 1655 | def exists(self, filepath): |
| 1656 | #: return True if filepath exists as a file. |
| 1657 | return os.path.isfile(filepath) |
| 1658 | |
| 1659 | def abspath(self, filepath): |
| 1660 | #: return full-path of filepath |
| 1661 | return os.path.abspath(filepath) |
| 1662 | |
| 1663 | def timestamp(self, filepath): |
| 1664 | #: return mtime of file |
| 1665 | return _getmtime(filepath) |
| 1666 | |
| 1667 | def load(self, filepath): |
| 1668 | #: if file exists, return file content and mtime |
| 1669 | def f(): |
| 1670 | mtime = _getmtime(filepath) |
| 1671 | input = _read_template_file(filepath) |
| 1672 | mtime2 = _getmtime(filepath) |
| 1673 | if mtime != mtime2: |
| 1674 | mtime = mtime2 |
| 1675 | input = _read_template_file(filepath) |
| 1676 | mtime2 = _getmtime(filepath) |
| 1677 | if mtime != mtime2: |
| 1678 | if logger: |
| 1679 | logger.warn("[tenjin] %s.load(): timestamp is changed while reading file." % self.__class__.__name__) |
| 1680 | return input, mtime |
| 1681 | #: if file not exist, return None |
| 1682 | return _ignore_not_found_error(f) |
| 1683 | |
| 1684 | |
| 1685 | ## |
| 1686 | ## |
| 1687 | ## |
| 1688 | class TemplateNotFoundError(Exception): |
| 1689 | pass |
| 1690 | |
| 1691 | |
| 1692 | |
| 1693 | ## |
| 1694 | ## template engine class |
| 1695 | ## |
| 1696 | |
| 1697 | class Engine(object): |
| 1698 | """Template Engine class. |
| 1699 | See User's Guide and examples for details. |
| 1700 | http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html |
| 1701 | http://www.kuwata-lab.com/tenjin/pytenjin-examples.html |
| 1702 | """ |
| 1703 | |
| 1704 | ## default value of attributes |
| 1705 | prefix = '' |
| 1706 | postfix = '' |
| 1707 | layout = None |
| 1708 | templateclass = Template |
| 1709 | path = None |
| 1710 | cache = TextCacheStorage() # save converted Python code into text file |
| 1711 | lang = None |
| 1712 | loader = FileSystemLoader() |
| 1713 | preprocess = False |
| 1714 | preprocessorclass = Preprocessor |
| 1715 | timestamp_interval = 1 # seconds |
| 1716 | |
| 1717 | 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): |
| 1718 | """Initializer of Engine class. |
| 1719 | |
| 1720 | prefix:str (='') |
| 1721 | Prefix string used to convert template short name to template filename. |
| 1722 | postfix:str (='') |
| 1723 | Postfix string used to convert template short name to template filename. |
| 1724 | layout:str (=None) |
| 1725 | Default layout template name. |
| 1726 | path:list of str(=None) |
| 1727 | List of directory names which contain template files. |
| 1728 | cache:bool or CacheStorage instance (=True) |
| 1729 | Cache storage object to store converted python code. |
| 1730 | If True, default cache storage (=Engine.cache) is used (if it is None |
| 1731 | then create MarshalCacheStorage object for each engine object). |
| 1732 | If False, no cache storage is used nor no cache files are created. |
| 1733 | preprocess:bool(=False) |
| 1734 | Activate preprocessing or not. |
| 1735 | templateclass:class (=Template) |
| 1736 | Template class which engine creates automatically. |
| 1737 | lang:str (=None) |
| 1738 | Language name such as 'en', 'fr', 'ja', and so on. If you specify |
| 1739 | this, cache file path will be 'inex.html.en.cache' for example. |
| 1740 | pp:list (=None) |
| 1741 | List of preprocessor object which is callable and manipulates template content. |
| 1742 | kwargs:dict |
| 1743 | Options for Template class constructor. |
| 1744 | See document of Template.__init__() for details. |
| 1745 | """ |
| 1746 | if prefix: self.prefix = prefix |
| 1747 | if postfix: self.postfix = postfix |
| 1748 | if layout: self.layout = layout |
| 1749 | if templateclass: self.templateclass = templateclass |
| 1750 | if preprocessorclass: self.preprocessorclass = preprocessorclass |
| 1751 | if path is not None: self.path = path |
| 1752 | if lang is not None: self.lang = lang |
| 1753 | if loader is not None: self.loader = loader |
| 1754 | if preprocess is not None: self.preprocess = preprocess |
| 1755 | if pp is None: pp = [] |
| 1756 | elif isinstance(pp, list): pass |
| 1757 | elif isinstance(pp, tuple): pp = list(pp) |
| 1758 | else: |
| 1759 | raise TypeError("'pp' expected to be a list but got %r." % (pp,)) |
| 1760 | self.pp = pp |
| 1761 | if preprocess: |
| 1762 | self.pp.append(TemplatePreprocessor(self.preprocessorclass)) |
| 1763 | self.kwargs = kwargs |
| 1764 | self.encoding = kwargs.get('encoding') |
| 1765 | self._filepaths = {} # template_name => relative path and absolute path |
| 1766 | self._added_templates = {} # templates added by add_template() |
| 1767 | #self.cache = cache |
| 1768 | self._set_cache_storage(cache) |
| 1769 | |
| 1770 | def _set_cache_storage(self, cache): |
| 1771 | if cache is True: |
| 1772 | if not self.cache: |
| 1773 | self.cache = MarshalCacheStorage() |
| 1774 | elif cache is None: |
| 1775 | pass |
| 1776 | elif cache is False: |
| 1777 | self.cache = None |
| 1778 | elif isinstance(cache, CacheStorage): |
| 1779 | self.cache = cache |
| 1780 | else: |
| 1781 | raise ValueError("%r: invalid cache object." % (cache, )) |
| 1782 | |
| 1783 | def cachename(self, filepath): |
| 1784 | #: if lang is provided then add it to cache filename. |
| 1785 | if self.lang: |
| 1786 | return '%s.%s.cache' % (filepath, self.lang) |
| 1787 | #: return cache file name. |
| 1788 | else: |
| 1789 | return filepath + '.cache' |
| 1790 | |
| 1791 | def to_filename(self, template_name): |
| 1792 | """Convert template short name into filename. |
| 1793 | ex. |
| 1794 | >>> engine = tenjin.Engine(prefix='user_', postfix='.pyhtml') |
| 1795 | >>> engine.to_filename(':list') |
| 1796 | 'user_list.pyhtml' |
| 1797 | >>> engine.to_filename('list') |
| 1798 | 'list' |
| 1799 | """ |
| 1800 | #: if template_name starts with ':', add prefix and postfix to it. |
| 1801 | if template_name[0] == ':' : |
| 1802 | return self.prefix + template_name[1:] + self.postfix |
| 1803 | #: if template_name doesn't start with ':', just return it. |
| 1804 | return template_name |
| 1805 | |
| 1806 | def _create_template(self, input=None, filepath=None, _context=None, _globals=None): |
| 1807 | #: if input is not specified then just create empty template object. |
| 1808 | template = self.templateclass(None, **self.kwargs) |
| 1809 | #: if input is specified then create template object and return it. |
| 1810 | if input: |
| 1811 | template.convert(input, filepath) |
| 1812 | return template |
| 1813 | |
| 1814 | def _preprocess(self, input, filepath, _context, _globals): |
| 1815 | #if _context is None: _context = {} |
| 1816 | #if _globals is None: _globals = sys._getframe(3).f_globals |
| 1817 | #: preprocess template and return result |
| 1818 | #preprocessor = self.preprocessorclass(filepath, input=input) |
| 1819 | #return preprocessor.render(_context, globals=_globals) |
| 1820 | #: preprocesses input with _context and returns result. |
| 1821 | if '_engine' not in _context: |
| 1822 | self.hook_context(_context) |
| 1823 | for pp in self.pp: |
| 1824 | input = pp.__call__(input, filename=filepath, context=_context, globals=_globals) |
| 1825 | return input |
| 1826 | |
| 1827 | def add_template(self, template): |
| 1828 | self._added_templates[template.filename] = template |
| 1829 | |
| 1830 | def _get_template_from_cache(self, cachepath, filepath): |
| 1831 | #: if template not found in cache, return None |
| 1832 | template = self.cache.get(cachepath, self.templateclass) |
| 1833 | if not template: |
| 1834 | return None |
| 1835 | assert template.timestamp is not None |
| 1836 | #: if checked within a sec, skip timestamp check. |
| 1837 | now = _time() |
| 1838 | last_checked = getattr(template, '_last_checked_at', None) |
| 1839 | if last_checked and now < last_checked + self.timestamp_interval: |
| 1840 | #if logger: logger.trace('[tenjin.%s] timestamp check skipped (%f < %f + %f)' % \ |
| 1841 | # (self.__class__.__name__, now, template._last_checked_at, self.timestamp_interval)) |
| 1842 | return template |
| 1843 | #: if timestamp of template objectis same as file, return it. |
| 1844 | if template.timestamp == self.loader.timestamp(filepath): |
| 1845 | template._last_checked_at = now |
| 1846 | return template |
| 1847 | #: if timestamp of template object is different from file, clear it |
| 1848 | #cache._delete(cachepath) |
| 1849 | if logger: logger.info("[tenjin.%s] cache expired (filepath=%r)" % \ |
| 1850 | (self.__class__.__name__, filepath)) |
| 1851 | return None |
| 1852 | |
| 1853 | def get_template(self, template_name, _context=None, _globals=None): |
| 1854 | """Return template object. |
| 1855 | If template object has not registered, template engine creates |
| 1856 | and registers template object automatically. |
| 1857 | """ |
| 1858 | #: accept template_name such as ':index'. |
| 1859 | filename = self.to_filename(template_name) |
| 1860 | #: if template object is added by add_template(), return it. |
| 1861 | if filename in self._added_templates: |
| 1862 | return self._added_templates[filename] |
| 1863 | #: get filepath and fullpath of template |
| 1864 | pair = self._filepaths.get(filename) |
| 1865 | if pair: |
| 1866 | filepath, fullpath = pair |
| 1867 | else: |
| 1868 | #: if template file is not found then raise TemplateNotFoundError. |
| 1869 | filepath = self.loader.find(filename, self.path) |
| 1870 | if not filepath: |
| 1871 | raise TemplateNotFoundError('%s: filename not found (path=%r).' % (filename, self.path)) |
| 1872 | # |
| 1873 | fullpath = self.loader.abspath(filepath) |
| 1874 | self._filepaths[filename] = (filepath, fullpath) |
| 1875 | #: use full path as base of cache file path |
| 1876 | cachepath = self.cachename(fullpath) |
| 1877 | #: get template object from cache |
| 1878 | cache = self.cache |
| 1879 | template = cache and self._get_template_from_cache(cachepath, filepath) or None |
| 1880 | #: if template object is not found in cache or is expired... |
| 1881 | if not template: |
| 1882 | ret = self.loader.load(filepath) |
| 1883 | if not ret: |
| 1884 | raise TemplateNotFoundError("%r: template not found." % filepath) |
| 1885 | input, timestamp = ret |
| 1886 | if self.pp: ## required for preprocessing |
| 1887 | if _context is None: _context = {} |
| 1888 | if _globals is None: _globals = sys._getframe(1).f_globals |
| 1889 | input = self._preprocess(input, filepath, _context, _globals) |
| 1890 | #: create template object. |
| 1891 | template = self._create_template(input, filepath, _context, _globals) |
| 1892 | #: set timestamp and filename of template object. |
| 1893 | template.timestamp = timestamp |
| 1894 | template._last_checked_at = _time() |
| 1895 | #: save template object into cache. |
| 1896 | if cache: |
| 1897 | if not template.bytecode: |
| 1898 | #: ignores syntax error when compiling. |
| 1899 | try: template.compile() |
| 1900 | except SyntaxError: pass |
| 1901 | cache.set(cachepath, template) |
| 1902 | #else: |
| 1903 | # template.compile() |
| 1904 | #: |
| 1905 | template.filename = filepath |
| 1906 | return template |
| 1907 | |
| 1908 | def include(self, template_name, append_to_buf=True, **kwargs): |
| 1909 | """Evaluate template using current local variables as context. |
| 1910 | |
| 1911 | template_name:str |
| 1912 | Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template. |
| 1913 | append_to_buf:boolean (=True) |
| 1914 | If True then append output into _buf and return None, |
| 1915 | else return stirng output. |
| 1916 | |
| 1917 | ex. |
| 1918 | <?py include('file.pyhtml') ?> |
| 1919 | #{include('file.pyhtml', False)} |
| 1920 | <?py val = include('file.pyhtml', False) ?> |
| 1921 | """ |
| 1922 | #: get local and global vars of caller. |
| 1923 | frame = sys._getframe(1) |
| 1924 | locals = frame.f_locals |
| 1925 | globals = frame.f_globals |
| 1926 | #: get _context from caller's local vars. |
| 1927 | assert '_context' in locals |
| 1928 | context = locals['_context'] |
| 1929 | #: if kwargs specified then add them into context. |
| 1930 | if kwargs: |
| 1931 | context.update(kwargs) |
| 1932 | #: get template object with context data and global vars. |
| 1933 | ## (context and globals are passed to get_template() only for preprocessing.) |
| 1934 | template = self.get_template(template_name, context, globals) |
| 1935 | #: if append_to_buf is true then add output to _buf. |
| 1936 | #: if append_to_buf is false then don't add output to _buf. |
| 1937 | if append_to_buf: _buf = locals['_buf'] |
| 1938 | else: _buf = None |
| 1939 | #: render template and return output. |
| 1940 | s = template.render(context, globals, _buf=_buf) |
| 1941 | #: kwargs are removed from context data. |
| 1942 | if kwargs: |
| 1943 | for k in kwargs: |
| 1944 | del context[k] |
| 1945 | return s |
| 1946 | |
| 1947 | def render(self, template_name, context=None, globals=None, layout=True): |
| 1948 | """Evaluate template with layout file and return result of evaluation. |
| 1949 | |
| 1950 | template_name:str |
| 1951 | Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template. |
| 1952 | context:dict (=None) |
| 1953 | Context object to evaluate. If None then new dict is used. |
| 1954 | globals:dict (=None) |
| 1955 | Global context to evaluate. If None then globals() is used. |
| 1956 | layout:str or Bool(=True) |
| 1957 | If True, the default layout name specified in constructor is used. |
| 1958 | If False, no layout template is used. |
| 1959 | If str, it is regarded as layout template name. |
| 1960 | |
| 1961 | If temlate object related with the 'template_name' argument is not exist, |
| 1962 | engine generates a template object and register it automatically. |
| 1963 | """ |
| 1964 | if context is None: |
| 1965 | context = {} |
| 1966 | if globals is None: |
| 1967 | globals = sys._getframe(1).f_globals |
| 1968 | self.hook_context(context) |
| 1969 | while True: |
| 1970 | ## context and globals are passed to get_template() only for preprocessing |
| 1971 | template = self.get_template(template_name, context, globals) |
| 1972 | content = template.render(context, globals) |
| 1973 | layout = context.pop('_layout', layout) |
| 1974 | if layout is True or layout is None: |
| 1975 | layout = self.layout |
| 1976 | if not layout: |
| 1977 | break |
| 1978 | template_name = layout |
| 1979 | layout = False |
| 1980 | context['_content'] = content |
| 1981 | context.pop('_content', None) |
| 1982 | return content |
| 1983 | |
| 1984 | def hook_context(self, context): |
| 1985 | #: add engine itself into context data. |
| 1986 | context['_engine'] = self |
| 1987 | #context['render'] = self.render |
| 1988 | #: add include() method into context data. |
| 1989 | context['include'] = self.include |
| 1990 | |
| 1991 | |
| 1992 | ## |
| 1993 | ## safe template and engine |
| 1994 | ## |
| 1995 | |
| 1996 | class SafeTemplate(Template): |
| 1997 | """Uses 'to_escaped()' instead of 'escape()'. |
| 1998 | '#{...}' is not allowed with this class. Use '[==...==]' instead. |
| 1999 | """ |
| 2000 | |
| 2001 | tostrfunc = 'to_str' |
| 2002 | escapefunc = 'to_escaped' |
| 2003 | |
| 2004 | def get_expr_and_flags(self, match): |
| 2005 | return _get_expr_and_flags(match, "#{%s}: '#{}' is not allowed with SafeTemplate.") |
| 2006 | |
| 2007 | |
| 2008 | class SafePreprocessor(Preprocessor): |
| 2009 | |
| 2010 | tostrfunc = 'to_str' |
| 2011 | escapefunc = 'to_escaped' |
| 2012 | |
| 2013 | def get_expr_and_flags(self, match): |
| 2014 | return _get_expr_and_flags(match, "#{{%s}}: '#{{}}' is not allowed with SafePreprocessor.") |
| 2015 | |
| 2016 | |
| 2017 | def _get_expr_and_flags(match, errmsg): |
| 2018 | expr1, expr2, expr3, expr4 = match.groups() |
| 2019 | if expr1 is not None: |
| 2020 | raise TemplateSyntaxError(errmsg % match.group(1)) |
| 2021 | if expr2 is not None: return expr2, (True, False) # #{...} : call escape, not to_str |
| 2022 | if expr3 is not None: return expr3, (False, True) # [==...==] : not escape, call to_str |
| 2023 | if expr4 is not None: return expr4, (True, False) # [=...=] : call escape, not to_str |
| 2024 | |
| 2025 | |
| 2026 | class SafeEngine(Engine): |
| 2027 | |
| 2028 | templateclass = SafeTemplate |
| 2029 | preprocessorclass = SafePreprocessor |
| 2030 | |
| 2031 | |
| 2032 | ## |
| 2033 | ## for Google App Engine |
| 2034 | ## (should separate into individual file or module?) |
| 2035 | ## |
| 2036 | |
| 2037 | def _dummy(): |
| 2038 | global memcache, _tenjin |
| 2039 | memcache = _tenjin = None # lazy import of google.appengine.api.memcache |
| 2040 | global GaeMemcacheCacheStorage, GaeMemcacheStore, init |
| 2041 | |
| 2042 | class GaeMemcacheCacheStorage(CacheStorage): |
| 2043 | |
| 2044 | lifetime = 0 # 0 means unlimited |
| 2045 | |
| 2046 | def __init__(self, lifetime=None, namespace=None): |
| 2047 | CacheStorage.__init__(self) |
| 2048 | if lifetime is not None: self.lifetime = lifetime |
| 2049 | self.namespace = namespace |
| 2050 | |
| 2051 | def _load(self, cachepath): |
| 2052 | key = cachepath |
| 2053 | if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheCacheStorage] load cache (key=%r)" % (key, )) |
| 2054 | return memcache.get(key, namespace=self.namespace) |
| 2055 | |
| 2056 | def _store(self, cachepath, dct): |
| 2057 | dct.pop('bytecode', None) |
| 2058 | key = cachepath |
| 2059 | if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheCacheStorage] store cache (key=%r)" % (key, )) |
| 2060 | ret = memcache.set(key, dct, self.lifetime, namespace=self.namespace) |
| 2061 | if not ret: |
| 2062 | if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheCacheStorage] failed to store cache (key=%r)" % (key, )) |
| 2063 | |
| 2064 | def _delete(self, cachepath): |
| 2065 | key = cachepath |
| 2066 | memcache.delete(key, namespace=self.namespace) |
| 2067 | |
| 2068 | |
| 2069 | class GaeMemcacheStore(KeyValueStore): |
| 2070 | |
| 2071 | lifetime = 0 |
| 2072 | |
| 2073 | def __init__(self, lifetime=None, namespace=None): |
| 2074 | if lifetime is not None: self.lifetime = lifetime |
| 2075 | self.namespace = namespace |
| 2076 | |
| 2077 | def get(self, key): |
| 2078 | return memcache.get(key, namespace=self.namespace) |
| 2079 | |
| 2080 | def set(self, key, value, lifetime=None): |
| 2081 | if lifetime is None: lifetime = self.lifetime |
| 2082 | if memcache.set(key, value, lifetime, namespace=self.namespace): |
| 2083 | return True |
| 2084 | else: |
| 2085 | if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheStore] failed to set (key=%r)" % (key, )) |
| 2086 | return False |
| 2087 | |
| 2088 | def delete(self, key): |
| 2089 | return memcache.delete(key, namespace=self.namespace) |
| 2090 | |
| 2091 | def has(self, key): |
| 2092 | if memcache.add(key, 'dummy', namespace=self.namespace): |
| 2093 | memcache.delete(key, namespace=self.namespace) |
| 2094 | return False |
| 2095 | else: |
| 2096 | return True |
| 2097 | |
| 2098 | |
| 2099 | def init(): |
| 2100 | global memcache, _tenjin |
| 2101 | if not memcache: |
| 2102 | from google.appengine.api import memcache |
| 2103 | if not _tenjin: import tenjin as _tenjin |
| 2104 | ## avoid cache confliction between versions |
| 2105 | ver = os.environ.get('CURRENT_VERSION_ID', '1.1')#.split('.')[0] |
| 2106 | Engine.cache = GaeMemcacheCacheStorage(namespace=ver) |
| 2107 | ## set fragment cache store |
| 2108 | helpers.fragment_cache.store = GaeMemcacheStore(namespace=ver) |
| 2109 | helpers.fragment_cache.lifetime = 60 # 1 minute |
| 2110 | helpers.fragment_cache.prefix = 'fragment.' |
| 2111 | |
| 2112 | |
| 2113 | gae = create_module('tenjin.gae', _dummy, |
| 2114 | os=os, helpers=helpers, Engine=Engine, |
| 2115 | CacheStorage=CacheStorage, KeyValueStore=KeyValueStore) |
| 2116 | |
| 2117 | |
| 2118 | del _dummy |