blob: db8cddee77ed9ab6e09505a415b76016c1032655 [file] [log] [blame]
Rich Lanea06d0c32013-03-25 08:52:03 -07001##
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
37import sys, os, re, time, marshal
38from time import time as _time
39from os.path import getmtime as _getmtime
40from os.path import isfile as _isfile
41random = pickle = unquote = None # lazy import
42python3 = sys.version_info[0] == 3
43python2 = sys.version_info[0] == 2
44
45logger = None
46
47
48##
49## utilities
50##
51
52def _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
68def _read_binary_file(filename):
69 f = open(filename, 'rb')
70 try:
71 return f.read()
72 finally:
73 f.close()
74
75codecs = None # lazy import
76
77def _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
86def _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
95def _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
103def 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
119def _raise(exception_class, *args):
120 raise exception_class(*args)
121
122
123##
124## helper method's module
125##
126
127def _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('&lt;', '<').replace('&gt;', '>').replace('&quot;', '"').replace('&#039;', "'").replace('&amp;', '&')
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'&lt;`#(.*?)#`&gt;', lambda m: '#{%s}' % unescape(m.group(1)), s)
281 s = re.sub(r'&lt;`\$(.*?)\$`&gt;', 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
286helpers = create_module('tenjin.helpers', _dummy, sys=sys, re=re)
287helpers.__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 ]
292generate_tostrfunc = helpers.generate_tostrfunc
293
294
295##
296## escaped module
297##
298def _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
338escaped = create_module('tenjin.escaped', _dummy, _helpers=helpers)
339
340
341##
342## module for html
343##
344def _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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
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('&', '&amp;')
373 # s = s.replace('<', '&lt;')
374 # s = s.replace('>', '&gt;')
375 # s = s.replace('"', '&quot;')
376 # return s # 5.83
377
378 def escape_html(s):
379 """Escape '&', '<', '>', '"' into '&amp;', '&lt;', '&gt;', '&quot;'."""
380 return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;') # 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(' ', ' &nbsp;')
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
465html = create_module('tenjin.html', _dummy, helpers=helpers, _escaped=escaped)
466helpers.escape = html.escape_html
467helpers.html = html # for backward compatibility
468sys.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
476def 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
505class 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
518class 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
1013class 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
1027class 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
1043class 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
1058class 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
1084class ParseError(Exception):
1085 pass
1086
1087
1088class 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
1241def _linenum(input, pos):
1242 return input[0:pos].count("\n") + 1
1243
1244
1245JS_FUNC = r"""
1246function _S(x){return x==null?'':x;}
1247function _E(x){return x==null?'':typeof(x)!=='string'?x:x.replace(/[&<>"']/g,_EF);}
1248var _ET={'&':"&amp;",'<':"&lt;",'>':"&gt;",'"':"&quot;","'":"&#039;"};
1249function _EF(c){return _ET[c];};
1250"""[1:-1]
1251JS_FUNC = escaped.EscapedStr(JS_FUNC)
1252
1253
1254
1255##
1256## cache storages
1257##
1258
1259class 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
1312class 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
1324class 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
1347class 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
1356class 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
1372class 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##
1413class 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##
1431class 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##
1476class 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##
1545class 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 = ...'
1609helpers.fragment_cache = FragmentCacheHelper(MemoryBaseStore())
1610helpers.not_cached = helpers.fragment_cache.not_cached
1611helpers.echo_cached = helpers.fragment_cache.echo_cached
1612helpers.cache_as = helpers.fragment_cache.cache_as
1613helpers.__all__.extend(('not_cached', 'echo_cached', 'cache_as'))
1614
1615
1616
1617##
1618## helper class to find and read template
1619##
1620class 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##
1653class 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##
1688class TemplateNotFoundError(Exception):
1689 pass
1690
1691
1692
1693##
1694## template engine class
1695##
1696
1697class 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
1996class 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
2008class 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
2017def _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
2026class 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
2037def _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
2113gae = create_module('tenjin.gae', _dummy,
2114 os=os, helpers=helpers, Engine=Engine,
2115 CacheStorage=CacheStorage, KeyValueStore=KeyValueStore)
2116
2117
2118del _dummy