PageRenderTime 32ms CodeModel.GetById 10ms RepoModel.GetById 1ms app.codeStats 0ms

/tenjin.py

https://github.com/tangyao0792/saepy-log
Python | 1776 lines | 1541 code | 84 blank | 151 comment | 106 complexity | c3285481f69d91217ebd5f502b0cde57 MD5 | raw file
  1. ##
  2. ## $Release: 1.0.1 $
  3. ## $Copyright: copyright(c) 2007-2011 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. """Very fast and light-weight template engine based embedded Python.
  26. See User's Guide and examples for details.
  27. http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html
  28. http://www.kuwata-lab.com/tenjin/pytenjin-examples.html
  29. """
  30. __version__ = "$Release: 1.0.1 $"[10:-2]
  31. __license__ = "$License: MIT License $"[10:-2]
  32. __all__ = ('Template', 'Engine', )
  33. import sys, os, re, time, marshal
  34. from time import time as _time
  35. from os.path import getmtime as _getmtime
  36. from os.path import isfile as _isfile
  37. random = pickle = unquote = None # lazy import
  38. python3 = sys.version_info[0] == 3
  39. python2 = sys.version_info[0] == 2
  40. logger = None
  41. ##
  42. ## utilities
  43. ##
  44. def _write_binary_file(filename, content):
  45. global random
  46. if random is None: from random import random
  47. tmpfile = filename + str(random())[1:]
  48. f = open(tmpfile, 'w+b') # on windows, 'w+b' is preffered than 'wb'
  49. try:
  50. f.write(content)
  51. finally:
  52. f.close()
  53. if os.path.exists(tmpfile):
  54. try:
  55. os.rename(tmpfile, filename)
  56. except:
  57. os.remove(filename) # on windows, existing file should be removed before renaming
  58. os.rename(tmpfile, filename)
  59. def _read_binary_file(filename):
  60. f = open(filename, 'rb')
  61. try:
  62. return f.read()
  63. finally:
  64. f.close()
  65. codecs = None # lazy import
  66. def _read_text_file(filename, encoding=None):
  67. global codecs
  68. if not codecs: import codecs
  69. f = codecs.open(filename, encoding=(encoding or 'utf-8'))
  70. try:
  71. return f.read()
  72. finally:
  73. f.close()
  74. def _read_template_file(filename, encoding=None):
  75. s = _read_binary_file(filename) ## binary(=str)
  76. if encoding: s = s.decode(encoding) ## binary(=str) to unicode
  77. return s
  78. _basestring = basestring
  79. _unicode = unicode
  80. _bytes = str
  81. def _ignore_not_found_error(f, default=None):
  82. try:
  83. return f()
  84. except OSError, ex:
  85. if ex.errno == 2: # error: No such file or directory
  86. return default
  87. raise
  88. def create_module(module_name, dummy_func=None, **kwargs):
  89. """ex. mod = create_module('tenjin.util')"""
  90. mod = type(sys)(module_name)
  91. mod.__file__ = __file__
  92. mod.__dict__.update(kwargs)
  93. sys.modules[module_name] = mod
  94. if dummy_func:
  95. exec(dummy_func.func_code, mod.__dict__)
  96. return mod
  97. def _raise(exception_class, *args):
  98. raise exception_class(*args)
  99. ##
  100. ## helper method's module
  101. ##
  102. def _dummy():
  103. global unquote
  104. unquote = None
  105. global to_str, escape, echo, new_cycle, generate_tostrfunc
  106. global start_capture, stop_capture, capture_as, captured_as, CaptureContext
  107. global _p, _P, _decode_params
  108. def generate_tostrfunc(encode=None, decode=None):
  109. """Generate 'to_str' function with encode or decode encoding.
  110. ex. generate to_str() function which encodes unicode into binary(=str).
  111. to_str = tenjin.generate_tostrfunc(encode='utf-8')
  112. repr(to_str(u'hoge')) #=> 'hoge' (str)
  113. ex. generate to_str() function which decodes binary(=str) into unicode.
  114. to_str = tenjin.generate_tostrfunc(decode='utf-8')
  115. repr(to_str('hoge')) #=> u'hoge' (unicode)
  116. """
  117. if encode:
  118. if decode:
  119. raise ValueError("can't specify both encode and decode encoding.")
  120. else:
  121. def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _encode=encode):
  122. """Convert val into string or return '' if None. Unicode will be encoded into binary(=str)."""
  123. if _isa(val, _str): return val
  124. if val is None: return ''
  125. #if _isa(val, _unicode): return val.encode(_encode) # unicode to binary(=str)
  126. if _isa(val, _unicode):
  127. return val.encode(_encode) # unicode to binary(=str)
  128. return _str(val)
  129. else:
  130. if decode:
  131. def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _decode=decode):
  132. """Convert val into string or return '' if None. Binary(=str) will be decoded into unicode."""
  133. #if _isa(val, _str): return val.decode(_decode) # binary(=str) to unicode
  134. if _isa(val, _str):
  135. return val.decode(_decode)
  136. if val is None: return ''
  137. if _isa(val, _unicode): return val
  138. return _unicode(val)
  139. else:
  140. def to_str(val, _str=str, _unicode=unicode, _isa=isinstance):
  141. """Convert val into string or return '' if None. Both binary(=str) and unicode will be retruned as-is."""
  142. if _isa(val, _str): return val
  143. if val is None: return ''
  144. if _isa(val, _unicode): return val
  145. return _str(val)
  146. return to_str
  147. to_str = generate_tostrfunc(encode='utf-8') # or encode=None?
  148. def echo(string):
  149. """add string value into _buf. this is equivarent to '#{string}'."""
  150. lvars = sys._getframe(1).f_locals # local variables
  151. lvars['_buf'].append(string)
  152. def new_cycle(*values):
  153. """Generate cycle object.
  154. ex.
  155. cycle = new_cycle('odd', 'even')
  156. print(cycle()) #=> 'odd'
  157. print(cycle()) #=> 'even'
  158. print(cycle()) #=> 'odd'
  159. print(cycle()) #=> 'even'
  160. """
  161. def gen(values):
  162. i, n = 0, len(values)
  163. while True:
  164. yield values[i]
  165. i = (i + 1) % n
  166. return gen(values).next
  167. class CaptureContext(object):
  168. def __init__(self, name, store_to_context=True, lvars=None):
  169. self.name = name
  170. self.store_to_context = store_to_context
  171. self.lvars = lvars or sys._getframe(1).f_locals
  172. def __enter__(self):
  173. lvars = self.lvars
  174. self._buf_orig = lvars['_buf']
  175. lvars['_buf'] = _buf = []
  176. lvars['_extend'] = _buf.extend
  177. return self
  178. def __exit__(self, *args):
  179. lvars = self.lvars
  180. _buf = lvars['_buf']
  181. lvars['_buf'] = self._buf_orig
  182. lvars['_extend'] = self._buf_orig.extend
  183. lvars[self.name] = self.captured = ''.join(_buf)
  184. if self.store_to_context and '_context' in lvars:
  185. lvars['_context'][self.name] = self.captured
  186. def __iter__(self):
  187. self.__enter__()
  188. yield self
  189. self.__exit__()
  190. def start_capture(varname=None, _depth=1):
  191. """(obsolete) start capturing with name."""
  192. lvars = sys._getframe(_depth).f_locals
  193. capture_context = CaptureContext(varname, None, lvars)
  194. lvars['_capture_context'] = capture_context
  195. capture_context.__enter__()
  196. def stop_capture(store_to_context=True, _depth=1):
  197. """(obsolete) stop capturing and return the result of capturing.
  198. if store_to_context is True then the result is stored into _context[varname].
  199. """
  200. lvars = sys._getframe(_depth).f_locals
  201. capture_context = lvars.pop('_capture_context', None)
  202. if not capture_context:
  203. raise Exception('stop_capture(): start_capture() is not called before.')
  204. capture_context.store_to_context = store_to_context
  205. capture_context.__exit__()
  206. return capture_context.captured
  207. def capture_as(name, store_to_context=True):
  208. """capture partial of template."""
  209. return CaptureContext(name, store_to_context, sys._getframe(1).f_locals)
  210. def captured_as(name, _depth=1):
  211. """helper method for layout template.
  212. if captured string is found then append it to _buf and return True,
  213. else return False.
  214. """
  215. lvars = sys._getframe(_depth).f_locals # local variables
  216. if name in lvars:
  217. _buf = lvars['_buf']
  218. _buf.append(lvars[name])
  219. return True
  220. return False
  221. def _p(arg):
  222. """ex. '/show/'+_p("item['id']") => "/show/#{item['id']}" """
  223. return '<`#%s#`>' % arg # decoded into #{...} by preprocessor
  224. def _P(arg):
  225. """ex. '<b>%s</b>' % _P("item['id']") => "<b>${item['id']}</b>" """
  226. return '<`$%s$`>' % arg # decoded into ${...} by preprocessor
  227. def _decode_params(s):
  228. """decode <`#...#`> and <`$...$`> into #{...} and ${...}"""
  229. global unquote
  230. if unquote is None:
  231. from urllib import unquote
  232. dct = { 'lt':'<', 'gt':'>', 'amp':'&', 'quot':'"', '#039':"'", }
  233. def unescape(s):
  234. #return s.replace('&lt;', '<').replace('&gt;', '>').replace('&quot;', '"').replace('&#039;', "'").replace('&amp;', '&')
  235. return re.sub(r'&(lt|gt|quot|amp|#039);', lambda m: dct[m.group(1)], s)
  236. s = to_str(s)
  237. s = re.sub(r'%3C%60%23(.*?)%23%60%3E', lambda m: '#{%s}' % unquote(m.group(1)), s)
  238. s = re.sub(r'%3C%60%24(.*?)%24%60%3E', lambda m: '${%s}' % unquote(m.group(1)), s)
  239. s = re.sub(r'&lt;`#(.*?)#`&gt;', lambda m: '#{%s}' % unescape(m.group(1)), s)
  240. s = re.sub(r'&lt;`\$(.*?)\$`&gt;', lambda m: '${%s}' % unescape(m.group(1)), s)
  241. s = re.sub(r'<`#(.*?)#`>', r'#{\1}', s)
  242. s = re.sub(r'<`\$(.*?)\$`>', r'${\1}', s)
  243. return s
  244. helpers = create_module('tenjin.helpers', _dummy, sys=sys, re=re)
  245. helpers.__all__ = ['to_str', 'escape', 'echo', 'new_cycle', 'generate_tostrfunc',
  246. 'start_capture', 'stop_capture', 'capture_as', 'captured_as',
  247. 'not_cached', 'echo_cached', 'cache_as',
  248. '_p', '_P', '_decode_params',
  249. ]
  250. generate_tostrfunc = helpers.generate_tostrfunc
  251. ##
  252. ## escaped module
  253. ##
  254. def _dummy():
  255. global is_escaped, as_escaped, to_escaped
  256. global Escaped, EscapedStr, EscapedUnicode
  257. global __all__
  258. __all__ = ('is_escaped', 'as_escaped', 'to_escaped', ) #'Escaped', 'EscapedStr',
  259. class Escaped(object):
  260. """marking class that object is already escaped."""
  261. pass
  262. def is_escaped(value):
  263. """return True if value is marked as escaped, else return False."""
  264. return isinstance(value, Escaped)
  265. class EscapedStr(str, Escaped):
  266. """string class which is marked as escaped."""
  267. pass
  268. class EscapedUnicode(unicode, Escaped):
  269. """unicode class which is marked as escaped."""
  270. pass
  271. def as_escaped(s):
  272. """mark string as escaped, without escaping."""
  273. if isinstance(s, str): return EscapedStr(s)
  274. if isinstance(s, unicode): return EscapedUnicode(s)
  275. raise TypeError("as_escaped(%r): expected str or unicode." % (s, ))
  276. def to_escaped(value):
  277. """convert any value into string and escape it.
  278. if value is already marked as escaped, don't escape it."""
  279. if hasattr(value, '__html__'):
  280. value = value.__html__()
  281. if is_escaped(value):
  282. #return value # EscapedUnicode should be convered into EscapedStr
  283. return as_escaped(_helpers.to_str(value))
  284. #if isinstance(value, _basestring):
  285. # return as_escaped(_helpers.escape(value))
  286. return as_escaped(_helpers.escape(_helpers.to_str(value)))
  287. escaped = create_module('tenjin.escaped', _dummy, _helpers=helpers)
  288. ##
  289. ## module for html
  290. ##
  291. def _dummy():
  292. global escape_html, escape_xml, escape, tagattr, tagattrs, _normalize_attrs
  293. global checked, selected, disabled, nl2br, text2html, nv, js_link
  294. #_escape_table = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
  295. #_escape_pattern = re.compile(r'[&<>"]')
  296. ##_escape_callable = lambda m: _escape_table[m.group(0)]
  297. ##_escape_callable = lambda m: _escape_table.__get__(m.group(0))
  298. #_escape_get = _escape_table.__getitem__
  299. #_escape_callable = lambda m: _escape_get(m.group(0))
  300. #_escape_sub = _escape_pattern.sub
  301. #def escape_html(s):
  302. # return s # 3.02
  303. #def escape_html(s):
  304. # return _escape_pattern.sub(_escape_callable, s) # 6.31
  305. #def escape_html(s):
  306. # return _escape_sub(_escape_callable, s) # 6.01
  307. #def escape_html(s, _p=_escape_pattern, _f=_escape_callable):
  308. # return _p.sub(_f, s) # 6.27
  309. #def escape_html(s, _sub=_escape_pattern.sub, _callable=_escape_callable):
  310. # return _sub(_callable, s) # 6.04
  311. #def escape_html(s):
  312. # s = s.replace('&', '&amp;')
  313. # s = s.replace('<', '&lt;')
  314. # s = s.replace('>', '&gt;')
  315. # s = s.replace('"', '&quot;')
  316. # return s # 5.83
  317. def escape_html(s):
  318. """Escape '&', '<', '>', '"' into '&amp;', '&lt;', '&gt;', '&quot;'."""
  319. return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;') # 5.72
  320. escape_xml = escape_html # for backward compatibility
  321. def tagattr(name, expr, value=None, escape=True):
  322. """(experimental) Return ' name="value"' if expr is true value, else '' (empty string).
  323. If value is not specified, expr is used as value instead."""
  324. if not expr and expr != 0: return _escaped.as_escaped('')
  325. if value is None: value = expr
  326. if escape: value = _escaped.to_escaped(value)
  327. return _escaped.as_escaped(' %s="%s"' % (name, value))
  328. def tagattrs(**kwargs):
  329. """(experimental) built html tag attribtes.
  330. ex.
  331. >>> tagattrs(klass='main', size=20)
  332. ' class="main" size="20"'
  333. >>> tagattrs(klass='', size=0)
  334. ''
  335. """
  336. kwargs = _normalize_attrs(kwargs)
  337. esc = _escaped.to_escaped
  338. s = ''.join([ ' %s="%s"' % (k, esc(v)) for k, v in kwargs.iteritems() if v or v == 0 ])
  339. return _escaped.as_escaped(s)
  340. def _normalize_attrs(kwargs):
  341. if 'klass' in kwargs: kwargs['class'] = kwargs.pop('klass')
  342. if 'checked' in kwargs: kwargs['checked'] = kwargs.pop('checked') and 'checked' or None
  343. if 'selected' in kwargs: kwargs['selected'] = kwargs.pop('selected') and 'selected' or None
  344. if 'disabled' in kwargs: kwargs['disabled'] = kwargs.pop('disabled') and 'disabled' or None
  345. return kwargs
  346. def checked(expr):
  347. """return ' checked="checked"' if expr is true."""
  348. return _escaped.as_escaped(expr and ' checked="checked"' or '')
  349. def selected(expr):
  350. """return ' selected="selected"' if expr is true."""
  351. return _escaped.as_escaped(expr and ' selected="selected"' or '')
  352. def disabled(expr):
  353. """return ' disabled="disabled"' if expr is true."""
  354. return _escaped.as_escaped(expr and ' disabled="disabled"' or '')
  355. def nl2br(text):
  356. """replace "\n" to "<br />\n" and return it."""
  357. if not text:
  358. return _escaped.as_escaped('')
  359. return _escaped.as_escaped(text.replace('\n', '<br />\n'))
  360. def text2html(text, use_nbsp=True):
  361. """(experimental) escape xml characters, replace "\n" to "<br />\n", and return it."""
  362. if not text:
  363. return _escaped.as_escaped('')
  364. s = _escaped.to_escaped(text)
  365. if use_nbsp: s = s.replace(' ', ' &nbsp;')
  366. #return nl2br(s)
  367. s = s.replace('\n', '<br />\n')
  368. return _escaped.as_escaped(s)
  369. def nv(name, value, sep=None, **kwargs):
  370. """(experimental) Build name and value attributes.
  371. ex.
  372. >>> nv('rank', 'A')
  373. 'name="rank" value="A"'
  374. >>> nv('rank', 'A', '.')
  375. 'name="rank" value="A" id="rank.A"'
  376. >>> nv('rank', 'A', '.', checked=True)
  377. 'name="rank" value="A" id="rank.A" checked="checked"'
  378. >>> nv('rank', 'A', '.', klass='error', style='color:red')
  379. 'name="rank" value="A" id="rank.A" class="error" style="color:red"'
  380. """
  381. name = _escaped.to_escaped(name)
  382. value = _escaped.to_escaped(value)
  383. s = sep and 'name="%s" value="%s" id="%s"' % (name, value, name+sep+value) \
  384. or 'name="%s" value="%s"' % (name, value)
  385. html = kwargs and s + tagattrs(**kwargs) or s
  386. return _escaped.as_escaped(html)
  387. def js_link(label, onclick, **kwargs):
  388. s = kwargs and tagattrs(**kwargs) or ''
  389. html = '<a href="javascript:undefined" onclick="%s;return false"%s>%s</a>' % \
  390. (_escaped.to_escaped(onclick), s, _escaped.to_escaped(label))
  391. return _escaped.as_escaped(html)
  392. html = create_module('tenjin.html', _dummy, helpers=helpers, _escaped=escaped)
  393. helpers.escape = html.escape_html
  394. helpers.html = html # for backward compatibility
  395. ##
  396. ## utility function to set default encoding of template files
  397. ##
  398. _template_encoding = (None, 'utf-8') # encodings for decode and encode
  399. def set_template_encoding(decode=None, encode=None):
  400. """Set default encoding of template files.
  401. This should be called before importing helper functions.
  402. ex.
  403. ## I like template files to be unicode-base like Django.
  404. import tenjin
  405. tenjin.set_template_encoding('utf-8') # should be called before importing helpers
  406. from tenjin.helpers import *
  407. """
  408. global _template_encoding
  409. if _template_encoding == (decode, encode):
  410. return
  411. if decode and encode:
  412. raise ValueError("set_template_encoding(): cannot specify both decode and encode.")
  413. if not decode and not encode:
  414. raise ValueError("set_template_encoding(): decode or encode should be specified.")
  415. if decode:
  416. Template.encoding = decode # unicode base template
  417. helpers.to_str = helpers.generate_tostrfunc(decode=decode)
  418. else:
  419. Template.encoding = None # binary base template
  420. helpers.to_str = helpers.generate_tostrfunc(encode=encode)
  421. _template_encoding = (decode, encode)
  422. ##
  423. ## Template class
  424. ##
  425. class TemplateSyntaxError(SyntaxError):
  426. def build_error_message(self):
  427. ex = self
  428. if not ex.text:
  429. return self.args[0]
  430. return ''.join([
  431. "%s:%s:%s: %s\n" % (ex.filename, ex.lineno, ex.offset, ex.msg, ),
  432. "%4d: %s\n" % (ex.lineno, ex.text.rstrip(), ),
  433. " %s^\n" % (' ' * ex.offset, ),
  434. ])
  435. class Template(object):
  436. """Convert and evaluate embedded python string.
  437. See User's Guide and examples for details.
  438. http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html
  439. http://www.kuwata-lab.com/tenjin/pytenjin-examples.html
  440. """
  441. ## default value of attributes
  442. filename = None
  443. encoding = None
  444. escapefunc = 'escape'
  445. tostrfunc = 'to_str'
  446. indent = 8
  447. preamble = None # "_buf = []; _expand = _buf.expand; _to_str = to_str; _escape = escape"
  448. postamble = None # "print ''.join(_buf)"
  449. smarttrim = None
  450. args = None
  451. timestamp = None
  452. trace = False # if True then '<!-- begin: file -->' and '<!-- end: file -->' are printed
  453. def __init__(self, filename=None, encoding=None, input=None, escapefunc=None, tostrfunc=None,
  454. indent=None, preamble=None, postamble=None, smarttrim=None, trace=None):
  455. """Initailizer of Template class.
  456. filename:str (=None)
  457. Filename to convert (optional). If None, no convert.
  458. encoding:str (=None)
  459. Encoding name. If specified, template string is converted into
  460. unicode object internally.
  461. Template.render() returns str object if encoding is None,
  462. else returns unicode object if encoding name is specified.
  463. input:str (=None)
  464. Input string. In other words, content of template file.
  465. Template file will not be read if this argument is specified.
  466. escapefunc:str (='escape')
  467. Escape function name.
  468. tostrfunc:str (='to_str')
  469. 'to_str' function name.
  470. indent:int (=8)
  471. Indent width.
  472. preamble:str or bool (=None)
  473. Preamble string which is inserted into python code.
  474. If true, '_buf = []; ' is used insated.
  475. postamble:str or bool (=None)
  476. Postamble string which is appended to python code.
  477. If true, 'print("".join(_buf))' is used instead.
  478. smarttrim:bool (=None)
  479. If True then "<div>\\n#{_context}\\n</div>" is parsed as
  480. "<div>\\n#{_context}</div>".
  481. """
  482. if encoding is not None: self.encoding = encoding
  483. if escapefunc is not None: self.escapefunc = escapefunc
  484. if tostrfunc is not None: self.tostrfunc = tostrfunc
  485. if indent is not None: self.indent = indent
  486. if preamble is not None: self.preamble = preamble
  487. if postamble is not None: self.postamble = postamble
  488. if smarttrim is not None: self.smarttrim = smarttrim
  489. if trace is not None: self.trace = trace
  490. #
  491. if preamble is True: self.preamble = "_buf = []"
  492. if postamble is True: self.postamble = "print(''.join(_buf))"
  493. if input:
  494. self.convert(input, filename)
  495. self.timestamp = False # False means 'file not exist' (= Engine should not check timestamp of file)
  496. elif filename:
  497. self.convert_file(filename)
  498. else:
  499. self._reset()
  500. def _reset(self, input=None, filename=None):
  501. self.script = None
  502. self.bytecode = None
  503. self.input = input
  504. self.filename = filename
  505. if input != None:
  506. i = input.find("\n")
  507. if i < 0:
  508. self.newline = "\n" # or None
  509. elif len(input) >= 2 and input[i-1] == "\r":
  510. self.newline = "\r\n"
  511. else:
  512. self.newline = "\n"
  513. self._localvars_assignments_added = False
  514. def _localvars_assignments(self):
  515. return "_extend=_buf.extend;_to_str=%s;_escape=%s; " % (self.tostrfunc, self.escapefunc)
  516. def before_convert(self, buf):
  517. if self.preamble:
  518. eol = self.input.startswith('<?py') and "\n" or "; "
  519. buf.append(self.preamble + eol)
  520. def after_convert(self, buf):
  521. if self.postamble:
  522. if buf and not buf[-1].endswith("\n"):
  523. buf.append("\n")
  524. buf.append(self.postamble + "\n")
  525. def convert_file(self, filename):
  526. """Convert file into python script and return it.
  527. This is equivarent to convert(open(filename).read(), filename).
  528. """
  529. input = _read_template_file(filename)
  530. return self.convert(input, filename)
  531. def convert(self, input, filename=None):
  532. """Convert string in which python code is embedded into python script and return it.
  533. input:str
  534. Input string to convert into python code.
  535. filename:str (=None)
  536. Filename of input. this is optional but recommended to report errors.
  537. """
  538. if self.encoding and isinstance(input, str):
  539. input = input.decode(self.encoding)
  540. self._reset(input, filename)
  541. buf = []
  542. self.before_convert(buf)
  543. self.parse_stmts(buf, input)
  544. self.after_convert(buf)
  545. script = ''.join(buf)
  546. self.script = script
  547. return script
  548. STMT_PATTERN = (r'<\?py( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?', re.S)
  549. def stmt_pattern(self):
  550. pat = self.STMT_PATTERN
  551. if isinstance(pat, tuple):
  552. pat = self.__class__.STMT_PATTERN = re.compile(*pat)
  553. return pat
  554. def parse_stmts(self, buf, input):
  555. if not input: return
  556. rexp = self.stmt_pattern()
  557. is_bol = True
  558. index = 0
  559. for m in rexp.finditer(input):
  560. mspace, code, rspace = m.groups()
  561. #mspace, close, rspace = m.groups()
  562. #code = input[m.start()+4+len(mspace):m.end()-len(close)-(rspace and len(rspace) or 0)]
  563. text = input[index:m.start()]
  564. index = m.end()
  565. ## detect spaces at beginning of line
  566. lspace = None
  567. if text == '':
  568. if is_bol:
  569. lspace = ''
  570. elif text[-1] == '\n':
  571. lspace = ''
  572. else:
  573. rindex = text.rfind('\n')
  574. if rindex < 0:
  575. if is_bol and text.isspace():
  576. lspace, text = text, ''
  577. else:
  578. s = text[rindex+1:]
  579. if s.isspace():
  580. lspace, text = s, text[:rindex+1]
  581. #is_bol = rspace is not None
  582. ## add text, spaces, and statement
  583. self.parse_exprs(buf, text, is_bol)
  584. is_bol = rspace is not None
  585. #if mspace == "\n":
  586. if mspace and mspace.endswith("\n"):
  587. code = "\n" + (code or "")
  588. #if rspace == "\n":
  589. if rspace and rspace.endswith("\n"):
  590. code = (code or "") + "\n"
  591. if code:
  592. code = self.statement_hook(code)
  593. m = self._match_to_args_declaration(code)
  594. if m:
  595. self._add_args_declaration(buf, m)
  596. else:
  597. self.add_stmt(buf, code)
  598. rest = input[index:]
  599. if rest:
  600. self.parse_exprs(buf, rest)
  601. self._arrange_indent(buf)
  602. def statement_hook(self, stmt):
  603. """expand macros and parse '#@ARGS' in a statement."""
  604. return stmt.replace("\r\n", "\n") # Python can't handle "\r\n" in code
  605. def _match_to_args_declaration(self, stmt):
  606. if self.args is not None:
  607. return None
  608. args_pattern = r'^ *#@ARGS(?:[ \t]+(.*?))?$'
  609. return re.match(args_pattern, stmt)
  610. def _add_args_declaration(self, buf, m):
  611. arr = (m.group(1) or '').split(',')
  612. args = []; declares = []
  613. for s in arr:
  614. arg = s.strip()
  615. if not s: continue
  616. if not re.match('^[a-zA-Z_]\w*$', arg):
  617. raise ValueError("%r: invalid template argument." % arg)
  618. args.append(arg)
  619. declares.append("%s = _context.get('%s'); " % (arg, arg))
  620. self.args = args
  621. #nl = stmt[m.end():]
  622. #if nl: declares.append(nl)
  623. buf.append(''.join(declares) + "\n")
  624. EXPR_PATTERN = (r'#\{(.*?)\}|\$\{(.*?)\}|\{=(?:=(.*?)=|(.*?))=\}', re.S)
  625. def expr_pattern(self):
  626. pat = self.EXPR_PATTERN
  627. if isinstance(pat, tuple):
  628. self.__class__.EXPR_PATTERN = pat = re.compile(*pat)
  629. return pat
  630. def get_expr_and_flags(self, match):
  631. expr1, expr2, expr3, expr4 = match.groups()
  632. if expr1 is not None: return expr1, (False, True) # not escape, call to_str
  633. if expr2 is not None: return expr2, (True, True) # call escape, call to_str
  634. if expr3 is not None: return expr3, (False, True) # not escape, call to_str
  635. if expr4 is not None: return expr4, (True, True) # call escape, call to_str
  636. def parse_exprs(self, buf, input, is_bol=False):
  637. buf2 = []
  638. self._parse_exprs(buf2, input, is_bol)
  639. if buf2:
  640. buf.append(''.join(buf2))
  641. def _parse_exprs(self, buf, input, is_bol=False):
  642. if not input: return
  643. self.start_text_part(buf)
  644. rexp = self.expr_pattern()
  645. smarttrim = self.smarttrim
  646. nl = self.newline
  647. nl_len = len(nl)
  648. pos = 0
  649. for m in rexp.finditer(input):
  650. start = m.start()
  651. text = input[pos:start]
  652. pos = m.end()
  653. expr, flags = self.get_expr_and_flags(m)
  654. #
  655. if text:
  656. self.add_text(buf, text)
  657. self.add_expr(buf, expr, *flags)
  658. #
  659. if smarttrim:
  660. flag_bol = text.endswith(nl) or not text and (start > 0 or is_bol)
  661. if flag_bol and not flags[0] and input[pos:pos+nl_len] == nl:
  662. pos += nl_len
  663. buf.append("\n")
  664. if smarttrim:
  665. if buf and buf[-1] == "\n":
  666. buf.pop()
  667. rest = input[pos:]
  668. if rest:
  669. self.add_text(buf, rest, True)
  670. self.stop_text_part(buf)
  671. if input[-1] == '\n':
  672. buf.append("\n")
  673. def start_text_part(self, buf):
  674. self._add_localvars_assignments_to_text(buf)
  675. #buf.append("_buf.extend((")
  676. buf.append("_extend((")
  677. def _add_localvars_assignments_to_text(self, buf):
  678. if not self._localvars_assignments_added:
  679. self._localvars_assignments_added = True
  680. buf.append(self._localvars_assignments())
  681. def stop_text_part(self, buf):
  682. buf.append("));")
  683. def _quote_text(self, text):
  684. return re.sub(r"(['\\\\])", r"\\\1", text)
  685. def add_text(self, buf, text, encode_newline=False):
  686. if not text: return
  687. use_unicode = self.encoding and python2
  688. buf.append(use_unicode and "u'''" or "'''")
  689. text = self._quote_text(text)
  690. if not encode_newline: buf.extend((text, "''', "))
  691. elif text.endswith("\r\n"): buf.extend((text[0:-2], "\\r\\n''', "))
  692. elif text.endswith("\n"): buf.extend((text[0:-1], "\\n''', "))
  693. else: buf.extend((text, "''', "))
  694. _add_text = add_text
  695. def add_expr(self, buf, code, *flags):
  696. if not code or code.isspace(): return
  697. flag_escape, flag_tostr = flags
  698. if not self.tostrfunc: flag_tostr = False
  699. if not self.escapefunc: flag_escape = False
  700. if flag_tostr and flag_escape: s1, s2 = "_escape(_to_str(", ")), "
  701. elif flag_tostr: s1, s2 = "_to_str(", "), "
  702. elif flag_escape: s1, s2 = "_escape(", "), "
  703. else: s1, s2 = "(", "), "
  704. buf.extend((s1, code, s2, ))
  705. def add_stmt(self, buf, code):
  706. if not code: return
  707. lines = code.splitlines(True) # keep "\n"
  708. if lines[-1][-1] != "\n":
  709. lines[-1] = lines[-1] + "\n"
  710. buf.extend(lines)
  711. self._add_localvars_assignments_to_stmts(buf)
  712. def _add_localvars_assignments_to_stmts(self, buf):
  713. if self._localvars_assignments_added:
  714. return
  715. for index, stmt in enumerate(buf):
  716. if not re.match(r'^[ \t]*(?:\#|_buf ?= ?\[\]|from __future__)', stmt):
  717. break
  718. else:
  719. return
  720. self._localvars_assignments_added = True
  721. if re.match(r'^[ \t]*(if|for|while|def|with|class)\b', stmt):
  722. buf.insert(index, self._localvars_assignments() + "\n")
  723. else:
  724. buf[index] = self._localvars_assignments() + buf[index]
  725. _START_WORDS = dict.fromkeys(('for', 'if', 'while', 'def', 'try:', 'with', 'class'), True)
  726. _END_WORDS = dict.fromkeys(('#end', '#endfor', '#endif', '#endwhile', '#enddef', '#endtry', '#endwith', '#endclass'), True)
  727. _CONT_WORDS = dict.fromkeys(('elif', 'else:', 'except', 'except:', 'finally:'), True)
  728. _WORD_REXP = re.compile(r'\S+')
  729. depth = -1
  730. ##
  731. ## ex.
  732. ## input = r"""
  733. ## if items:
  734. ## _buf.extend(('<ul>\n', ))
  735. ## i = 0
  736. ## for item in items:
  737. ## i += 1
  738. ## _buf.extend(('<li>', to_str(item), '</li>\n', ))
  739. ## #endfor
  740. ## _buf.extend(('</ul>\n', ))
  741. ## #endif
  742. ## """[1:]
  743. ## lines = input.splitlines(True)
  744. ## block = self.parse_lines(lines)
  745. ## #=> [ "if items:\n",
  746. ## [ "_buf.extend(('<ul>\n', ))\n",
  747. ## "i = 0\n",
  748. ## "for item in items:\n",
  749. ## [ "i += 1\n",
  750. ## "_buf.extend(('<li>', to_str(item), '</li>\n', ))\n",
  751. ## ],
  752. ## "#endfor\n",
  753. ## "_buf.extend(('</ul>\n', ))\n",
  754. ## ],
  755. ## "#endif\n",
  756. ## ]
  757. def parse_lines(self, lines):
  758. block = []
  759. try:
  760. self._parse_lines(lines.__iter__(), False, block, 0)
  761. except StopIteration:
  762. if self.depth > 0:
  763. fname, linenum, colnum, linetext = self.filename, len(lines), None, None
  764. raise TemplateSyntaxError("unexpected EOF.", (fname, linenum, colnum, linetext))
  765. else:
  766. pass
  767. return block
  768. def _parse_lines(self, lines_iter, end_block, block, linenum):
  769. if block is None: block = []
  770. _START_WORDS = self._START_WORDS
  771. _END_WORDS = self._END_WORDS
  772. _CONT_WORDS = self._CONT_WORDS
  773. _WORD_REXP = self._WORD_REXP
  774. get_line = lines_iter.next
  775. while True:
  776. line = get_line()
  777. linenum += line.count("\n")
  778. m = _WORD_REXP.search(line)
  779. if not m:
  780. block.append(line)
  781. continue
  782. word = m.group(0)
  783. if word in _END_WORDS:
  784. if word != end_block and word != '#end':
  785. if end_block is False:
  786. msg = "'%s' found but corresponding statement is missing." % (word, )
  787. else:
  788. msg = "'%s' expected but got '%s'." % (end_block, word)
  789. colnum = m.start() + 1
  790. raise TemplateSyntaxError(msg, (self.filename, linenum, colnum, line))
  791. return block, line, None, linenum
  792. elif line.endswith(':\n') or line.endswith(':\r\n'):
  793. if word in _CONT_WORDS:
  794. return block, line, word, linenum
  795. elif word in _START_WORDS:
  796. block.append(line)
  797. self.depth += 1
  798. cont_word = None
  799. try:
  800. child_block, line, cont_word, linenum = \
  801. self._parse_lines(lines_iter, '#end'+word, [], linenum)
  802. block.extend((child_block, line, ))
  803. while cont_word: # 'elif' or 'else:'
  804. child_block, line, cont_word, linenum = \
  805. self._parse_lines(lines_iter, '#end'+word, [], linenum)
  806. block.extend((child_block, line, ))
  807. except StopIteration:
  808. msg = "'%s' is not closed." % (cont_word or word)
  809. colnum = m.start() + 1
  810. raise TemplateSyntaxError(msg, (self.filename, linenum, colnum, line))
  811. self.depth -= 1
  812. else:
  813. block.append(line)
  814. else:
  815. block.append(line)
  816. assert "unreachable"
  817. def _join_block(self, block, buf, depth):
  818. indent = ' ' * (self.indent * depth)
  819. for line in block:
  820. if isinstance(line, list):
  821. self._join_block(line, buf, depth+1)
  822. elif line.isspace():
  823. buf.append(line)
  824. else:
  825. buf.append(indent + line.lstrip())
  826. def _arrange_indent(self, buf):
  827. """arrange indentation of statements in buf"""
  828. block = self.parse_lines(buf)
  829. buf[:] = []
  830. self._join_block(block, buf, 0)
  831. def render(self, context=None, globals=None, _buf=None):
  832. """Evaluate python code with context dictionary.
  833. If _buf is None then return the result of evaluation as str,
  834. else return None.
  835. context:dict (=None)
  836. Context object to evaluate. If None then new dict is created.
  837. globals:dict (=None)
  838. Global object. If None then globals() is used.
  839. _buf:list (=None)
  840. If None then new list is created.
  841. """
  842. if context is None:
  843. locals = context = {}
  844. elif self.args is None:
  845. locals = context.copy()
  846. else:
  847. locals = {}
  848. if '_engine' in context:
  849. context.get('_engine').hook_context(locals)
  850. locals['_context'] = context
  851. if globals is None:
  852. globals = sys._getframe(1).f_globals
  853. bufarg = _buf
  854. if _buf is None:
  855. _buf = []
  856. locals['_buf'] = _buf
  857. if not self.bytecode:
  858. self.compile()
  859. if self.trace:
  860. _buf.append("<!-- ***** begin: %s ***** -->\n" % self.filename)
  861. exec(self.bytecode, globals, locals)
  862. _buf.append("<!-- ***** end: %s ***** -->\n" % self.filename)
  863. else:
  864. exec(self.bytecode, globals, locals)
  865. if bufarg is not None:
  866. return bufarg
  867. elif not logger:
  868. return ''.join(_buf)
  869. else:
  870. try:
  871. return ''.join(_buf)
  872. except UnicodeDecodeError, ex:
  873. logger.error("[tenjin.Template] " + str(ex))
  874. logger.error("[tenjin.Template] (_buf=%r)" % (_buf, ))
  875. raise
  876. def compile(self):
  877. """compile self.script into self.bytecode"""
  878. self.bytecode = compile(self.script, self.filename or '(tenjin)', 'exec')
  879. ##
  880. ## preprocessor class
  881. ##
  882. class Preprocessor(Template):
  883. """Template class for preprocessing."""
  884. STMT_PATTERN = (r'<\?PY( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?', re.S)
  885. EXPR_PATTERN = (r'#\{\{(.*?)\}\}|\$\{\{(.*?)\}\}|\{#=(?:=(.*?)=|(.*?))=#\}', re.S)
  886. def add_expr(self, buf, code, *flags):
  887. if not code or code.isspace():
  888. return
  889. code = "_decode_params(%s)" % code
  890. Template.add_expr(self, buf, code, *flags)
  891. ##
  892. ## cache storages
  893. ##
  894. class CacheStorage(object):
  895. """[abstract] Template object cache class (in memory and/or file)"""
  896. def __init__(self):
  897. self.items = {} # key: full path, value: template object
  898. def get(self, cachepath, create_template):
  899. """get template object. if not found, load attributes from cache file and restore template object."""
  900. template = self.items.get(cachepath)
  901. if not template:
  902. dct = self._load(cachepath)
  903. if dct:
  904. template = create_template()
  905. for k in dct:
  906. setattr(template, k, dct[k])
  907. self.items[cachepath] = template
  908. return template
  909. def set(self, cachepath, template):
  910. """set template object and save template attributes into cache file."""
  911. self.items[cachepath] = template
  912. dct = self._save_data_of(template)
  913. return self._store(cachepath, dct)
  914. def _save_data_of(self, template):
  915. return { 'args' : template.args, 'bytecode' : template.bytecode,
  916. 'script': template.script, 'timestamp': template.timestamp }
  917. def unset(self, cachepath):
  918. """remove template object from dict and cache file."""
  919. self.items.pop(cachepath, None)
  920. return self._delete(cachepath)
  921. def clear(self):
  922. """remove all template objects and attributes from dict and cache file."""
  923. d, self.items = self.items, {}
  924. for k in d.iterkeys():
  925. self._delete(k)
  926. d.clear()
  927. def _load(self, cachepath):
  928. """(abstract) load dict object which represents template object attributes from cache file."""
  929. raise NotImplementedError.new("%s#_load(): not implemented yet." % self.__class__.__name__)
  930. def _store(self, cachepath, template):
  931. """(abstract) load dict object which represents template object attributes from cache file."""
  932. raise NotImplementedError.new("%s#_store(): not implemented yet." % self.__class__.__name__)
  933. def _delete(self, cachepath):
  934. """(abstract) remove template object from cache file."""
  935. raise NotImplementedError.new("%s#_delete(): not implemented yet." % self.__class__.__name__)
  936. class MemoryCacheStorage(CacheStorage):
  937. def _load(self, cachepath):
  938. return None
  939. def _store(self, cachepath, template):
  940. pass
  941. def _delete(self, cachepath):
  942. pass
  943. class FileCacheStorage(CacheStorage):
  944. def _load(self, cachepath):
  945. if not _isfile(cachepath): return None
  946. if logger: logger.info("[tenjin.%s] load cache (file=%r)" % (self.__class__.__name__, cachepath))
  947. data = _read_binary_file(cachepath)
  948. return self._restore(data)
  949. def _store(self, cachepath, dct):
  950. if logger: logger.info("[tenjin.%s] store cache (file=%r)" % (self.__class__.__name__, cachepath))
  951. data = self._dump(dct)
  952. _write_binary_file(cachepath, data)
  953. def _restore(self, data):
  954. raise NotImplementedError("%s._restore(): not implemented yet." % self.__class__.__name__)
  955. def _dump(self, dct):
  956. raise NotImplementedError("%s._dump(): not implemented yet." % self.__class__.__name__)
  957. def _delete(self, cachepath):
  958. _ignore_not_found_error(lambda: os.unlink(cachepath))
  959. class MarshalCacheStorage(FileCacheStorage):
  960. def _restore(self, data):
  961. return marshal.loads(data)
  962. def _dump(self, dct):
  963. return marshal.dumps(dct)
  964. class PickleCacheStorage(FileCacheStorage):
  965. def __init__(self, *args, **kwargs):
  966. global pickle
  967. if pickle is None:
  968. import cPickle as pickle
  969. FileCacheStorage.__init__(self, *args, **kwargs)
  970. def _restore(self, data):
  971. return pickle.loads(data)
  972. def _dump(self, dct):
  973. dct.pop('bytecode', None)
  974. return pickle.dumps(dct)
  975. class TextCacheStorage(FileCacheStorage):
  976. def _restore(self, data):
  977. header, script = data.split("\n\n", 1)
  978. timestamp = encoding = args = None
  979. for line in header.split("\n"):
  980. key, val = line.split(": ", 1)
  981. if key == 'timestamp': timestamp = float(val)
  982. elif key == 'encoding': encoding = val
  983. elif key == 'args': args = val.split(', ')
  984. if encoding: script = script.decode(encoding) ## binary(=str) to unicode
  985. return {'args': args, 'script': script, 'timestamp': timestamp}
  986. def _dump(self, dct):
  987. s = dct['script']
  988. if dct.get('encoding') and isinstance(s, unicode):
  989. s = s.encode(dct['encoding']) ## unicode to binary(=str)
  990. sb = []
  991. sb.append("timestamp: %s\n" % dct['timestamp'])
  992. if dct.get('encoding'):
  993. sb.append("encoding: %s\n" % dct['encoding'])
  994. if dct.get('args') is not None:
  995. sb.append("args: %s\n" % ', '.join(dct['args']))
  996. sb.append("\n")
  997. sb.append(s)
  998. s = ''.join(sb)
  999. if python3:
  1000. if isinstance(s, str):
  1001. s = s.encode(dct.get('encoding') or 'utf-8') ## unicode(=str) to binary
  1002. return s
  1003. def _save_data_of(self, template):
  1004. dct = FileCacheStorage._save_data_of(self, template)
  1005. dct['encoding'] = template.encoding
  1006. return dct
  1007. ##
  1008. ## abstract class for data cache
  1009. ##
  1010. class KeyValueStore(object):
  1011. def get(self, key, *options):
  1012. raise NotImplementedError("%s.get(): not implemented yet." % self.__class__.__name__)
  1013. def set(self, key, value, *options):
  1014. raise NotImplementedError("%s.set(): not implemented yet." % self.__class__.__name__)
  1015. def delete(self, key, *options):
  1016. raise NotImplementedError("%s.del(): not implemented yet." % self.__class__.__name__)
  1017. def has(self, key, *options):
  1018. raise NotImplementedError("%s.has(): not implemented yet." % self.__class__.__name__)
  1019. ##
  1020. ## memory base data cache
  1021. ##
  1022. class MemoryBaseStore(KeyValueStore):
  1023. def __init__(self):
  1024. self.values = {}
  1025. def get(self, key, original_timestamp=None):
  1026. tupl = self.values.get(key)
  1027. if not tupl:
  1028. return None
  1029. value, created_at, expires_at = tupl
  1030. if original_timestamp is not None and created_at < original_timestamp:
  1031. self.delete(key)
  1032. return None
  1033. if expires_at < _time():
  1034. self.delete(key)
  1035. return None
  1036. return value
  1037. def set(self, key, value, lifetime=0):
  1038. created_at = _time()
  1039. expires_at = lifetime and created_at + lifetime or 0
  1040. self.values[key] = (value, created_at, expires_at)
  1041. return True
  1042. def delete(self, key):
  1043. try:
  1044. del self.values[key]
  1045. return True
  1046. except KeyError:
  1047. return False
  1048. def has(self, key):
  1049. pair = self.values.get(key)
  1050. if not pair:
  1051. return False
  1052. value, created_at, expires_at = pair
  1053. if expires_at and expires_at < _time():
  1054. self.delete(key)
  1055. return False
  1056. return True
  1057. ##
  1058. ## file base data cache
  1059. ##
  1060. class FileBaseStore(KeyValueStore):
  1061. lifetime = 604800 # = 60*60*24*7
  1062. def __init__(self, root_path, encoding=None):
  1063. if not os.path.isdir(root_path):
  1064. raise ValueError("%r: directory not found." % (root_path, ))
  1065. self.root_path = root_path
  1066. if encoding is None and python3:
  1067. encoding = 'utf-8'
  1068. self.encoding = encoding
  1069. _pat = re.compile(r'[^-.\/\w]')
  1070. def filepath(self, key, _pat1=_pat):
  1071. return os.path.join(self.root_path, _pat1.sub('_', key))
  1072. def get(self, key, original_timestamp=None):
  1073. fpath = self.filepath(key)
  1074. #if not _isfile(fpath): return None
  1075. stat = _ignore_not_found_error(lambda: os.stat(fpath), None)
  1076. if stat is None:
  1077. return None
  1078. created_at = stat.st_ctime
  1079. expires_at = stat.st_mtime
  1080. if original_timestamp is not None and created_at < original_timestamp:
  1081. self.delete(key)
  1082. return None
  1083. if expires_at < _time():
  1084. self.delete(key)
  1085. return None
  1086. if self.encoding:
  1087. f = lambda: _read_text_file(fpath, self.encoding)
  1088. else:
  1089. f = lambda: _read_binary_file(fpath)
  1090. return _ignore_not_found_error(f, None)
  1091. def set(self, key, value, lifetime=0):
  1092. fpath = self.filepath(key)
  1093. dirname = os.path.dirname(fpath)
  1094. if not os.path.isdir(dirname):
  1095. os.makedirs(dirname)
  1096. now = _time()
  1097. if isinstance(value, _unicode):
  1098. value = value.encode(self.encoding or 'utf-8')
  1099. _write_binary_file(fpath, value)
  1100. expires_at = now + (lifetime or self.lifetime) # timestamp
  1101. os.utime(fpath, (expires_at, expires_at))
  1102. return True
  1103. def delete(self, key):
  1104. fpath = self.filepath(key)
  1105. ret = _ignore_not_found_error(lambda: os.unlink(fpath), False)
  1106. return ret != False
  1107. def has(self, key):
  1108. fpath = self.filepath(key)
  1109. if not _isfile(fpath):
  1110. return False
  1111. if _getmtime(fpath) < _time():
  1112. self.delete(key)
  1113. return False
  1114. return True
  1115. ##
  1116. ## html fragment cache helper class
  1117. ##
  1118. class FragmentCacheHelper(object):
  1119. """html fragment cache helper class."""
  1120. lifetime = 60 # 1 minute
  1121. prefix = None
  1122. def __init__(self, store, lifetime=None, prefix=None):
  1123. self.store = store
  1124. if lifetime is not None: self.lifetime = lifetime
  1125. if prefix is not None: self.prefix = prefix
  1126. def not_cached(self, cache_key, lifetime=None):
  1127. """(obsolete. use cache_as() instead of this.)
  1128. html fragment cache helper. see document of FragmentCacheHelper class."""
  1129. context = sys._getframe(1).f_locals['_context']
  1130. context['_cache_key'] = cache_key
  1131. key = self.prefix and self.prefix + cache_key or cache_key
  1132. value = self.store.get(key)
  1133. if value: ## cached
  1134. if logger: logger.debug('[tenjin.not_cached] %r: cached.' % (cache_key, ))
  1135. context[key] = value
  1136. return False
  1137. else: ## not cached
  1138. if logger: logger.debug('[tenjin.not_cached]: %r: not cached.' % (cache_key, ))
  1139. if key in context: del context[key]
  1140. if lifetime is None: lifetime = self.lifetime
  1141. context['_cache_lifetime'] = lifetime
  1142. helpers.start_capture(cache_key, _depth=2)
  1143. return True
  1144. def echo_cached(self):
  1145. """(obsolete. use cache_as() instead of this.)
  1146. html fragment cache helper. see document of FragmentCacheHelper class."""
  1147. f_locals = sys._getframe(1).f_locals
  1148. context = f_locals['_context']
  1149. cache_key = context.pop('_cache_key')
  1150. key = self.prefix and self.prefix + cache_key or cache_key
  1151. if key in context: ## cached
  1152. value = context.pop(key)
  1153. else: ## not cached
  1154. value = helpers.stop_capture(False, _depth=2)
  1155. lifetime = context.pop('_cache_lifetime')
  1156. self.store.set(key, value, lifetime)
  1157. f_locals['_buf'].append(value)
  1158. def functions(self):
  1159. """(obsolete. use cache_as() instead of this.)"""
  1160. return (self.not_cached, self.echo_cached)
  1161. def cache_as(self, cache_key, lifetime=None):
  1162. key = self.prefix and self.prefix + cache_key or cache_key
  1163. _buf = sys._getframe(1).f_locals['_buf']
  1164. value = self.store.get(key)
  1165. if value:
  1166. if logger: logger.debug('[tenjin.cache_as] %r: cache found.' % (cache_key, ))
  1167. _buf.append(value)
  1168. else:
  1169. if logger: logger.debug('[tenjin.cache_as] %r: expired or not cached yet.' % (cache_key, ))
  1170. _buf_len = len(_buf)
  1171. yield None
  1172. value = ''.join(_buf[_buf_len:])
  1173. self.store.set(key, value, lifetime)
  1174. ## you can change default store by 'tenjin.helpers.fragment_cache.store = ...'
  1175. helpers.fragment_cache = FragmentCacheHelper(MemoryBaseStore())
  1176. helpers.not_cached = helpers.fragment_cache.not_cached
  1177. helpers.echo_cached = helpers.fragment_cache.echo_cached
  1178. helpers.cache_as = helpers.fragment_cache.cache_as
  1179. helpers.__all__.extend(('not_cached', 'echo_cached', 'cache_as'))
  1180. ##
  1181. ## helper class to find and read template
  1182. ##
  1183. class Loader(object):
  1184. def exists(self, filepath):
  1185. raise NotImplementedError("%s.exists(): not implemented yet." % self.__class__.__name__)
  1186. def find(self, filename, dirs=None):
  1187. #: if dirs provided then search template file from it.
  1188. if dirs:
  1189. for dirname in dirs:
  1190. filepath = os.path.join(dirname, filename)
  1191. if self.exists(filepath):
  1192. return filepath
  1193. #: if dirs not provided then just return filename if file exists.
  1194. else:
  1195. if self.exists(filename):
  1196. return filename
  1197. #: if file not found then return None.
  1198. return None
  1199. def abspath(self, filename):
  1200. raise NotImplementedError("%s.abspath(): not implemented yet." % self.__class__.__name__)
  1201. def timestamp(self, filepath):
  1202. raise NotImplementedError("%s.timestamp(): not implemented yet." % self.__class__.__name__)
  1203. def load(self, filepath):
  1204. raise NotImplementedError("%s.timestamp(): not implemented yet." % self.__class__.__name__)
  1205. ##
  1206. ## helper class to find and read files
  1207. ##
  1208. class FileSystemLoader(Loader):
  1209. def exists(self, filepath):
  1210. #: return True if filepath exists as a file.
  1211. return os.path.isfile(filepath)
  1212. def abspath(self, filepath):
  1213. #: return full-path of filepath
  1214. return os.path.abspath(filepath)
  1215. def timestamp(self, filepath):
  1216. #: return mtime of file
  1217. return _getmtime(filepath)
  1218. def load(self, filepath):
  1219. #: if file exists, return file content and mtime
  1220. def f():
  1221. mtime = _getmtime(filepath)
  1222. input = _read_template_file(filepath)
  1223. mtime2 = _getmtime(filepath)
  1224. if mtime != mtime2:
  1225. mtime = mtime2
  1226. input = _read_template_file(filepath)
  1227. mtime2 = _getmtime(filepath)
  1228. if mtime != mtime2:
  1229. if logger:
  1230. logger.warn("[tenjin] %s.load(): timestamp is changed while reading file." % self.__class__.__name__)
  1231. return input, mtime
  1232. #: if file not exist, return None
  1233. return _ignore_not_found_error(f)
  1234. ##
  1235. ##
  1236. ##
  1237. class TemplateNotFoundError(Exception):
  1238. pass
  1239. ##
  1240. ## template engine class
  1241. ##
  1242. class Engine(object):
  1243. """Template Engine class.
  1244. See User's Guide and examples for details.
  1245. http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html
  1246. http://www.kuwata-lab.com/tenjin/pytenjin-examples.html
  1247. """
  1248. ## default value of attributes
  1249. prefix = ''
  1250. postfix = ''
  1251. layout = None
  1252. templateclass = Template
  1253. path = None
  1254. cache = MarshalCacheStorage() # save converted Python code into file by marshal-format
  1255. lang = None
  1256. loader = FileSystemLoader()
  1257. preprocess = False
  1258. preprocessorclass = Preprocessor
  1259. timestamp_interval = 1 # seconds
  1260. def __init__(self, prefix=None, postfix=None, layout=None, path=None, cache=True, preprocess=None, templateclass=None, preprocessorclass=None, lang=None, loader=None, **kwargs):
  1261. """Initializer of Engine class.
  1262. prefix:str (='')
  1263. Prefix string used to convert template short name to template filename.
  1264. postfix:str (='')
  1265. Postfix string used to convert template short name to template filename.
  1266. layout:str (=None)
  1267. Default layout template name.
  1268. path:list of str(=None)
  1269. List of directory names which contain template files.
  1270. cache:bool or CacheStorage instance (=True)
  1271. Cache storage object to store converted python code.
  1272. If True, default cache storage (=Engine.cache) is used (if it is None
  1273. then create MarshalCacheStorage object for each engine object).
  1274. If False, no cache storage is used nor no cache files are created.
  1275. preprocess:bool(=False)
  1276. Activate preprocessing or not.
  1277. templateclass:class (=Template)
  1278. Template class which engine creates automatically.
  1279. lang:str (=None)
  1280. Language name such as 'en', 'fr', 'ja', and so on. If you specify
  1281. this, cache file path will be 'inex.html.en.cache' for example.
  1282. kwargs:dict
  1283. Options for Template class constructor.
  1284. See document of Template.__init__() for details.
  1285. """
  1286. if prefix: self.prefix = prefix
  1287. if postfix: self.postfix = postfix
  1288. if layout: self.layout = layout
  1289. if templateclass: self.templateclass = templateclass
  1290. if preprocessorclass: self.preprocessorclass = preprocessorclass
  1291. if path is not None: self.path = path
  1292. if lang is not None: self.lang = lang
  1293. if loader is not None: self.loader = loader
  1294. if preprocess is not None: self.preprocess = preprocess
  1295. self.kwargs = kwargs
  1296. self.encoding = kwargs.get('encoding')
  1297. self._filepaths = {} # template_name => relative path and absolute path
  1298. self._added_templates = {} # templates added by add_template()
  1299. #self.cache = cache
  1300. self._set_cache_storage(cache)
  1301. def _set_cache_storage(self, cache):
  1302. if cache is True:
  1303. if not self.cache:
  1304. self.cache = MarshalCacheStorage()
  1305. elif cache is None:
  1306. pass
  1307. elif cache is False:
  1308. self.cache = None
  1309. elif isinstance(cache, CacheStorage):
  1310. self.cache = cache
  1311. else:
  1312. raise ValueError("%r: invalid cache object." % (cache, ))
  1313. def cachename(self, filepath):
  1314. #: if lang is provided then add it to cache filename.
  1315. if self.lang:
  1316. return '%s.%s.cache' % (filepath, self.lang)
  1317. #: return cache file name.
  1318. else:
  1319. return filepath + '.cache'
  1320. def to_filename(self, template_name):
  1321. """Convert template short name into filename.
  1322. ex.
  1323. >>> engine = tenjin.Engine(prefix='user_', postfix='.pyhtml')
  1324. >>> engine.to_filename(':list')
  1325. 'user_list.pyhtml'
  1326. >>> engine.to_filename('list')
  1327. 'list'
  1328. """
  1329. #: if template_name starts with ':', add prefix and postfix to it.
  1330. if template_name[0] == ':' :
  1331. return self.prefix + template_name[1:] + self.postfix
  1332. #: if template_name doesn't start with ':', just return it.
  1333. return template_name
  1334. def _create_template(self, input=None, filepath=None, _context=None, _globals=None):
  1335. #: if input is not specified then just create empty template object.
  1336. template = self.templateclass(None, **self.kwargs)
  1337. #: if input is specified then create template object and return it.
  1338. if input:
  1339. template.convert(input, filepath)
  1340. return template
  1341. def _preprocess(self, input, filepath, _context, _globals):
  1342. #if _context is None: _context = {}
  1343. #if _globals is None: _globals = sys._getframe(3).f_globals
  1344. #: preprocess template and return result
  1345. if '_engine' not in _context:
  1346. self.hook_context(_context)
  1347. preprocessor = self.preprocessorclass(filepath, input=input)
  1348. return preprocessor.render(_context, globals=_globals)
  1349. def add_template(self, template):
  1350. self._added_templates[template.filename] = template
  1351. def _get_template_from_cache(self, cachepath, filepath):
  1352. #: if template not found in cache, return None
  1353. template = self.cache.get(cachepath, self.templateclass)
  1354. if not template:
  1355. return None
  1356. assert template.timestamp is not None
  1357. #: if checked within a sec, skip timestamp check.
  1358. now = _time()
  1359. last_checked = getattr(template, '_last_checked_at', None)
  1360. if last_checked and now < last_checked + self.timestamp_interval:
  1361. #if logger: logger.trace('[tenjin.%s] timestamp check skipped (%f < %f + %f)' % \
  1362. # (self.__class__.__name__, now, template._last_checked_at, self.timestamp_interval))
  1363. return template
  1364. #: if timestamp of template objectis same as file, return it.
  1365. if template.timestamp == self.loader.timestamp(filepath):
  1366. template._last_checked_at = now
  1367. return template
  1368. #: if timestamp of template object is different from file, clear it
  1369. #cache._delete(cachepath)
  1370. if logger: logger.info("[tenjin.%s] cache expired (filepath=%r)" % \
  1371. (self.__class__.__name__, filepath))
  1372. return None
  1373. def get_template(self, template_name, _context=None, _globals=None):
  1374. """Return template object.
  1375. If template object has not registered, template engine creates
  1376. and registers template object automatically.
  1377. """
  1378. #: accept template_name such as ':index'.
  1379. filename = self.to_filename(template_name)
  1380. #: if template object is added by add_template(), return it.
  1381. if filename in self._added_templates:
  1382. return self._added_templates[filename]
  1383. #: get filepath and fullpath of template
  1384. pair = self._filepaths.get(filename)
  1385. if pair:
  1386. filepath, fullpath = pair
  1387. else:
  1388. #: if template file is not found then raise TemplateNotFoundError.
  1389. filepath = self.loader.find(filename, self.path)
  1390. if not filepath:
  1391. raise TemplateNotFoundError('%s: filename not found (path=%r).' % (filename, self.path))
  1392. #
  1393. fullpath = self.loader.abspath(filepath)
  1394. self._filepaths[filename] = (filepath, fullpath)
  1395. #: use full path as base of cache file path
  1396. cachepath = self.cachename(fullpath)
  1397. #: get template object from cache
  1398. cache = self.cache
  1399. template = cache and self._get_template_from_cache(cachepath, filepath) or None
  1400. #: if template object is not found in cache or is expired...
  1401. if not template:
  1402. ret = self.loader.load(filepath)
  1403. if not ret:
  1404. raise TemplateNotFoundError("%r: template not found." % filepath)
  1405. input, timestamp = ret
  1406. if self.preprocess: ## required for preprocessing
  1407. if _context is None: _context = {}
  1408. if _globals is None: _globals = sys._getframe(1).f_globals
  1409. input = self._preprocess(input, filepath, _context, _globals)
  1410. #: create template object.
  1411. template = self._create_template(input, filepath, _context, _globals)
  1412. #: set timestamp and filename of template object.
  1413. template.timestamp = timestamp
  1414. template._last_checked_at = _time()
  1415. #: save template object into cache.
  1416. if cache:
  1417. if not template.bytecode: template.compile()
  1418. cache.set(cachepath, template)
  1419. #else:
  1420. # template.compile()
  1421. #:
  1422. template.filename = filepath
  1423. return template
  1424. def include(self, template_name, append_to_buf=True, **kwargs):
  1425. """Evaluate template using current local variables as context.
  1426. template_name:str
  1427. Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template.
  1428. append_to_buf:boolean (=True)
  1429. If True then append output into _buf and return None,
  1430. else return stirng output.
  1431. ex.
  1432. <?py include('file.pyhtml') ?>
  1433. #{include('file.pyhtml', False)}
  1434. <?py val = include('file.pyhtml', False) ?>
  1435. """
  1436. #: get local and global vars of caller.
  1437. frame = sys._getframe(1)
  1438. locals = frame.f_locals
  1439. globals = frame.f_globals
  1440. #: get _context from caller's local vars.
  1441. assert '_context' in locals
  1442. context = locals['_context']
  1443. #: if kwargs specified then add them into context.
  1444. if kwargs:
  1445. context.update(kwargs)
  1446. #: get template object with context data and global vars.
  1447. ## (context and globals are passed to get_template() only for preprocessing.)
  1448. template = self.get_template(template_name, context, globals)
  1449. #: if append_to_buf is true then add output to _buf.
  1450. #: if append_to_buf is false then don't add output to _buf.
  1451. if append_to_buf: _buf = locals['_buf']
  1452. else: _buf = None
  1453. #: render template and return output.
  1454. s = template.render(context, globals, _buf=_buf)
  1455. #: kwargs are removed from context data.
  1456. if kwargs:
  1457. for k in kwargs:
  1458. del context[k]
  1459. return s
  1460. def render(self, template_name, context=None, globals=None, layout=True):
  1461. """Evaluate template with layout file and return result of evaluation.
  1462. template_name:str
  1463. Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template.
  1464. context:dict (=None)
  1465. Context object to evaluate. If None then new dict is used.
  1466. globals:dict (=None)
  1467. Global context to evaluate. If None then globals() is used.
  1468. layout:str or Bool(=True)
  1469. If True, the default layout name specified in constructor is used.
  1470. If False, no layout template is used.
  1471. If str, it is regarded as layout template name.
  1472. If temlate object related with the 'template_name' argument is not exist,
  1473. engine generates a template object and register it automatically.
  1474. """
  1475. if context is None:
  1476. context = {}
  1477. if globals is None:
  1478. globals = sys._getframe(1).f_globals
  1479. self.hook_context(context)
  1480. while True:
  1481. ## context and globals are passed to get_template() only for preprocessing
  1482. template = self.get_template(template_name, context, globals)
  1483. content = template.render(context, globals)
  1484. layout = context.pop('_layout', layout)
  1485. if layout is True or layout is None:
  1486. layout = self.layout
  1487. if not layout:
  1488. break
  1489. template_name = layout
  1490. layout = False
  1491. context['_content'] = content
  1492. context.pop('_content', None)
  1493. return content
  1494. def hook_context(self, context):
  1495. #: add engine itself into context data.
  1496. context['_engine'] = self
  1497. #context['render'] = self.render
  1498. #: add include() method into context data.
  1499. context['include'] = self.include
  1500. ##
  1501. ## safe template and engine
  1502. ##
  1503. class SafeTemplate(Template):
  1504. """Uses 'to_escaped()' instead of 'escape()'.
  1505. '#{...}' is not allowed with this class. Use '[==...==]' instead.
  1506. """
  1507. tostrfunc = 'to_str'
  1508. escapefunc = 'to_escaped'
  1509. def get_expr_and_flags(self, match):
  1510. return _get_expr_and_flags(match, "#{%s}: '#{}' is not allowed with SafeTemplate.")
  1511. class SafePreprocessor(Preprocessor):
  1512. tostrfunc = 'to_str'
  1513. escapefunc = 'to_escaped'
  1514. def get_expr_and_flags(self, match):
  1515. return _get_expr_and_flags(match, "#{{%s}}: '#{{}}' is not allowed with SafePreprocessor.")
  1516. def _get_expr_and_flags(match, errmsg):
  1517. expr1, expr2, expr3, expr4 = match.groups()
  1518. if expr1 is not None:
  1519. raise TemplateSyntaxError(errmsg % match.group(1))
  1520. if expr2 is not None: return expr2, (True, False) # #{...} : call escape, not to_str
  1521. if expr3 is not None: return expr3, (False, True) # [==...==] : not escape, call to_str
  1522. if expr4 is not None: return expr4, (True, False) # [=...=] : call escape, not to_str
  1523. class SafeEngine(Engine):
  1524. templateclass = SafeTemplate
  1525. preprocessorclass = SafePreprocessor
  1526. del _dummy