PageRenderTime 28ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/uliweb/core/template.py

http://uliweb.googlecode.com/
Python | 682 lines | 667 code | 13 blank | 2 comment | 44 complexity | 3f06f30e94104befcc0e9ed3b45d4f07 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
  1. #! /usr/bin/env python
  2. #coding=utf-8
  3. import re
  4. import os
  5. import StringIO
  6. import cgi
  7. __templates_temp_dir__ = 'tmp/templates_temp'
  8. __options__ = {'use_temp_dir':False}
  9. __nodes__ = {} #user defined nodes
  10. class TemplateException(Exception): pass
  11. class ContextPopException(TemplateException):
  12. "pop() has been called more times than push()"
  13. pass
  14. def use_tempdir(dir=''):
  15. global __options__, __templates_temp_dir__
  16. if dir:
  17. __templates_temp_dir__ = dir
  18. __options__['use_temp_dir'] = True
  19. if not os.path.exists(__templates_temp_dir__):
  20. os.makedirs(__templates_temp_dir__)
  21. def set_options(**options):
  22. """
  23. default use_temp_dir=False
  24. """
  25. __options__.update(options)
  26. def get_temp_template(filename):
  27. if __options__['use_temp_dir']:
  28. f, filename = os.path.splitdrive(filename)
  29. filename = filename.replace('\\', '_')
  30. filename = filename.replace('/', '_')
  31. f, ext = os.path.splitext(filename)
  32. filename = f + '.py'
  33. return os.path.normcase(os.path.join(__templates_temp_dir__, filename))
  34. return filename
  35. def register_node(name, node):
  36. assert issubclass(node, Node)
  37. __nodes__[name] = node
  38. def reindent(text):
  39. lines=text.split('\n')
  40. new_lines=[]
  41. credit=k=0
  42. for raw_line in lines:
  43. line=raw_line.strip()
  44. if not line or line[0]=='#':
  45. new_lines.append(line)
  46. continue
  47. if line[:5]=='elif ' or line[:5]=='else:' or \
  48. line[:7]=='except:' or line[:7]=='except ' or \
  49. line[:7]=='finally:' or line[:5]=='with ':
  50. k=k+credit-1
  51. if k<0: k=0
  52. new_lines.append(' '*k+line)
  53. credit=0
  54. if line=='pass' or line[:5]=='pass ':
  55. credit=0
  56. k-=1
  57. if line=='return' or line[:7]=='return ' or \
  58. line=='continue' or line[:9]=='continue ' or \
  59. line=='break' or line[:6]=='break':
  60. credit=1
  61. k-=1
  62. if line[-1:]==':' or line[:3]=='if ':
  63. k+=1
  64. text='\n'.join(new_lines)
  65. return text
  66. def get_templatefile(filename, dirs, default_template=None):
  67. if os.path.exists(filename):
  68. return filename
  69. if filename:
  70. if dirs:
  71. for d in dirs:
  72. path = os.path.join(d, filename)
  73. if os.path.exists(path):
  74. return path
  75. if default_template:
  76. if isinstance(default_template, (list, tuple)):
  77. for i in default_template:
  78. f = get_templatefile(i, dirs)
  79. if f:
  80. return f
  81. else:
  82. return get_templatefile(default_template, dirs)
  83. def parse_arguments(text, key='with'):
  84. r = re.compile(r'\s+%s\s+' % key)
  85. k = re.compile(r'^\s*([\w][a-zA-Z_0-9]*\s*)=\s*(.*)')
  86. b = r.split(text)
  87. if len(b) == 1:
  88. name, args, kwargs = b[0], (), {}
  89. else:
  90. name = b[0]
  91. s = b[1].split(',')
  92. args = []
  93. kwargs = {}
  94. for x in s:
  95. ret = k.search(x)
  96. if ret:
  97. kwargs[ret.group(1)] = ret.group(2)
  98. else:
  99. args.append(x)
  100. return name, args, kwargs
  101. def eval_vars(vs, vars, env):
  102. if isinstance(vs, (tuple, list)):
  103. return [eval_vars(x, vars, env) for x in vs]
  104. elif isinstance(vs, dict):
  105. return dict([(x, eval_vars(y, vars, env)) for x, y in vs.iteritems()])
  106. else:
  107. return eval(vs, vars, env)
  108. r_tag = re.compile(r'(\{\{.*?\}\})', re.DOTALL|re.M)
  109. class Node(object):
  110. block = 0
  111. var = False
  112. def __init__(self, value=None, content=None, template=None):
  113. self.value = value
  114. self.content = content
  115. self.template = template
  116. def __str__(self):
  117. if self.value:
  118. return self.value
  119. else:
  120. return ''
  121. def __repr__(self):
  122. return self.__str__()
  123. class BlockNode(Node):
  124. block = 1
  125. def __init__(self, name='', content=None):
  126. self.nodes = []
  127. self.name = name
  128. self.content = content
  129. def add(self, node):
  130. self.nodes.append(node)
  131. if isinstance(node, BlockNode):
  132. v = self.content.root.block_vars.setdefault(node.name, [])
  133. v.append(node)
  134. def merge(self, content):
  135. self.nodes.extend(content.nodes)
  136. def __repr__(self):
  137. s = ['{{block %s}}' % self.name]
  138. for x in self.nodes:
  139. s.append(repr(x))
  140. s.append('{{end}}')
  141. return ''.join(s)
  142. def __str__(self):
  143. return self.render()
  144. def render(self, top=True):
  145. """
  146. Top: if output the toppest block node
  147. """
  148. if top and self.name in self.content.root.block_vars and self is not self.content.root.block_vars[self.name][-1]:
  149. return self.content.root.block_vars[self.name][-1].render(False)
  150. s = []
  151. for x in self.nodes:
  152. if isinstance(x, BlockNode):
  153. if x.name in self.content.root.block_vars:
  154. s.append(str(self.content.root.block_vars[x.name][-1]))
  155. else:
  156. s.append(str(x))
  157. else:
  158. s.append(str(x))
  159. return ''.join(s)
  160. class SuperNode(Node):
  161. def __init__(self, parent, content):
  162. self.parent = parent
  163. self.content = content
  164. def __str__(self):
  165. for i, v in enumerate(self.content.root.block_vars[self.parent.name]):
  166. if self.parent is v:
  167. if i > 0:
  168. return self.content.root.block_vars[self.parent.name][i-1].render(False)
  169. return ''
  170. def __repr__(self):
  171. return '{{super}}'
  172. class Content(BlockNode):
  173. def __init__(self, root=None):
  174. self.nodes = []
  175. self.block_vars = {}
  176. self.begin = []
  177. self.end = []
  178. self.root = root or self
  179. def add(self, node):
  180. self.nodes.append(node)
  181. if isinstance(node, BlockNode):
  182. if node.name:
  183. v = self.block_vars.setdefault(node.name, [])
  184. v.append(node)
  185. def merge(self, content):
  186. self.nodes.extend(content.nodes)
  187. for k, v in content.block_vars.items():
  188. d = self.block_vars.setdefault(k, [])
  189. d.extend(v)
  190. content.root = self.root
  191. def clear_content(self):
  192. self.nodes = []
  193. def __str__(self):
  194. s = self.begin[:]
  195. for x in self.nodes:
  196. s.append(str(x))
  197. s.extend(self.end)
  198. return ''.join(s)
  199. def __repr__(self):
  200. s = []
  201. for x in self.nodes:
  202. s.append(repr(x))
  203. return ''.join(s)
  204. class Context(object):
  205. "A stack container for variable context"
  206. def __init__(self, dict_=None):
  207. dict_ = dict_ or {}
  208. self.dicts = [dict_]
  209. self.dirty = True
  210. self.result = None
  211. def __repr__(self):
  212. return repr(self.dicts)
  213. def __iter__(self):
  214. for d in self.dicts:
  215. yield d
  216. def push(self):
  217. d = {}
  218. self.dicts = [d] + self.dicts
  219. self.dirty = True
  220. return d
  221. def pop(self):
  222. if len(self.dicts) == 1:
  223. raise ContextPopException
  224. return self.dicts.pop(0)
  225. self.dirty = True
  226. def __setitem__(self, key, value):
  227. "Set a variable in the current context"
  228. self.dicts[0][key] = value
  229. self.dirty = True
  230. def __getitem__(self, key):
  231. "Get a variable's value, starting at the current context and going upward"
  232. for d in self.dicts:
  233. if key in d:
  234. return d[key]
  235. raise KeyError(key)
  236. def __delitem__(self, key):
  237. "Delete a variable from the current context"
  238. del self.dicts[0][key]
  239. self.dirty = True
  240. def has_key(self, key):
  241. for d in self.dicts:
  242. if key in d:
  243. return True
  244. return False
  245. __contains__ = has_key
  246. def get(self, key, otherwise=None):
  247. for d in self.dicts:
  248. if key in d:
  249. return d[key]
  250. return otherwise
  251. def update(self, other_dict):
  252. "Like dict.update(). Pushes an entire dictionary's keys and values onto the context."
  253. if not hasattr(other_dict, '__getitem__'):
  254. raise TypeError('other_dict must be a mapping (dictionary-like) object.')
  255. self.dicts[0].update(other_dict)
  256. # self.dicts = [other_dict] + self.dicts
  257. self.dirty = True
  258. return other_dict
  259. def to_dict(self):
  260. if not self.dirty:
  261. return self.result
  262. else:
  263. d = {}
  264. for i in reversed(self.dicts):
  265. d.update(i)
  266. self.result = d
  267. self.dirty = False
  268. return d
  269. __nodes__['block'] = BlockNode
  270. class Out(object):
  271. encoding = 'utf-8'
  272. def __init__(self):
  273. self.buf = StringIO.StringIO()
  274. def _str(self, text):
  275. if not isinstance(text, (str, unicode)):
  276. text = str(text)
  277. if isinstance(text, unicode):
  278. return text.encode(self.encoding)
  279. else:
  280. return text
  281. def write(self, text, escape=True):
  282. s = self._str(text)
  283. if escape:
  284. self.buf.write(cgi.escape(s))
  285. else:
  286. self.buf.write(s)
  287. def xml(self, text):
  288. self.write(self._str(text), escape=False)
  289. # def json(self, text):
  290. # from datawrap import dumps
  291. # self.write(dumps(text))
  292. #
  293. def getvalue(self):
  294. return self.buf.getvalue()
  295. class Template(object):
  296. def __init__(self, text='', vars=None, env=None, dirs=None,
  297. default_template=None, use_temp=False, compile=None, skip_error=False, encoding='utf-8'):
  298. self.text = text
  299. self.filename = None
  300. self.vars = vars or {}
  301. if not isinstance(env, Context):
  302. env = Context(env)
  303. self.env = env
  304. self.dirs = dirs or '.'
  305. self.default_template = default_template
  306. self.use_temp = use_temp
  307. self.compile = compile
  308. self.writer = 'out.write'
  309. self.content = Content()
  310. self.stack = [self.content]
  311. self.depend_files = [] #used for template dump file check
  312. self.callbacks = []
  313. self.exec_env = {}
  314. self.root = self
  315. self.skip_error = skip_error
  316. self.encoding = encoding
  317. for k, v in __nodes__.iteritems():
  318. if hasattr(v, 'init'):
  319. v.init(self)
  320. def add_callback(self, callback):
  321. if not callback in self.root.callbacks:
  322. self.root.callbacks.append(callback)
  323. def add_exec_env(self, name, obj):
  324. self.root.exec_env[name] = obj
  325. def add_root(self, root):
  326. self.root = root
  327. def set_filename(self, filename):
  328. fname = get_templatefile(filename, self.dirs, self.default_template)
  329. if not fname:
  330. raise TemplateException, "Can't find the template %s" % filename
  331. self.filename = fname
  332. def _get_parameters(self, value):
  333. def _f(*args, **kwargs):
  334. return args, kwargs
  335. d = self.env.to_dict()
  336. d['_f'] = _f
  337. try:
  338. args, kwargs = eval("_f(%s)" % value, self.vars, d)
  339. except:
  340. if self.skip_error:
  341. return (None,), {}
  342. else:
  343. raise
  344. return args, kwargs
  345. def parse(self):
  346. text = self.text
  347. in_tag = False
  348. extend = None #if need to process extend node
  349. for i in r_tag.split(text):
  350. if i:
  351. if len(self.stack) == 0:
  352. raise TemplateException, "The 'end' tag is unmatched, please check if you have more '{{end}}'"
  353. top = self.stack[-1]
  354. if in_tag:
  355. line = i[2:-2].strip()
  356. if not line:
  357. continue
  358. if line.startswith('T='):
  359. name, value = 'T=', line[2:].strip()
  360. elif line.startswith('T<<'):
  361. name, value = 'T<<', line[3:].strip()
  362. elif line.startswith('='):
  363. name, value = '=', line[1:].strip()
  364. elif line.startswith('<<'):
  365. name, value = '<<', line[2:].strip()
  366. else:
  367. v = line.split(' ', 1)
  368. if len(v) == 1:
  369. name, value = v[0], ''
  370. else:
  371. name, value = v
  372. if name in __nodes__:
  373. node_cls = __nodes__[name]
  374. #this will pass top template instance and top content instance to node_cls
  375. node = node_cls(value.strip(), self.content)
  376. if node.block:
  377. top.add(node)
  378. self.stack.append(node)
  379. else:
  380. buf = str(node)
  381. if buf:
  382. top.add(buf)
  383. elif name == 'super':
  384. t = self.stack[-1]
  385. if isinstance(t, BlockNode):
  386. node = SuperNode(t, self.content)
  387. top.add(node)
  388. elif name == 'end':
  389. self.stack.pop()
  390. elif name == '=':
  391. buf = "%s(%s)\n" % (self.writer, value)
  392. top.add(buf)
  393. elif name == 'BEGIN_TAG':
  394. buf = "%s('{{')\n" % self.writer
  395. top.add(buf)
  396. elif name == 'END_TAG':
  397. buf = "%s('}}')\n" % self.writer
  398. top.add(buf)
  399. elif name == '<<':
  400. buf = "%s(%s, escape=False)\n" % (self.writer, value)
  401. top.add(buf)
  402. elif name == 'T=':
  403. if not self._parse_template(top, value):
  404. buf = "%s(%s)\n" % (self.writer, value)
  405. top.add(buf)
  406. elif name == 'T<<':
  407. if not self._parse_template(top, value):
  408. buf = "%s(%s, escape=False)\n" % (self.writer, value)
  409. top.add(buf)
  410. elif name == 'include':
  411. self._parse_include(top, value)
  412. elif name == 'embed':
  413. self._parse_text(top, value)
  414. elif name == 'extend':
  415. extend = value
  416. else:
  417. if line and in_tag:
  418. top.add(line+'\n')
  419. else:
  420. buf = "%s(%r, escape=False)\n" % (self.writer, i)
  421. top.add(buf)
  422. in_tag = not in_tag
  423. if extend:
  424. self._parse_extend(extend)
  425. if self.encoding:
  426. pre = '#coding=%s\n' % self.encoding
  427. else:
  428. pre = ''
  429. return reindent(pre + str(self.content))
  430. def _parse_template(self, content, var):
  431. if var in self.vars:
  432. v = self.vars[var]
  433. else:
  434. return False
  435. #add v.__template__ support
  436. if hasattr(v, '__template__'):
  437. text = str(v.__template__(var))
  438. if text:
  439. t = Template(text, self.vars, self.env, self.dirs)
  440. t.parse()
  441. t.add_root(self)
  442. content.merge(t.content)
  443. return True
  444. return False
  445. def _parse_text(self, content, var):
  446. try:
  447. text = str(eval(var, self.vars, self.env.to_dict()))
  448. except:
  449. if self.skip_error:
  450. text = ''
  451. else:
  452. raise
  453. if text:
  454. t = Template(text, self.vars, self.env, self.dirs)
  455. t.parse()
  456. t.add_root(self)
  457. content.merge(t.content)
  458. def _parse_include(self, content, value):
  459. self.env.push()
  460. try:
  461. args, kwargs = self._get_parameters(value)
  462. filename = args[0]
  463. self.env.update(kwargs)
  464. fname = get_templatefile(filename, self.dirs)
  465. if not fname:
  466. raise TemplateException, "Can't find the template %s" % filename
  467. self.depend_files.append(fname)
  468. f = open(fname, 'rb')
  469. text = f.read()
  470. f.close()
  471. t = Template(text, self.vars, self.env, self.dirs)
  472. t.add_root(self)
  473. t.parse()
  474. content.merge(t.content)
  475. finally:
  476. self.env.pop()
  477. def _parse_extend(self, value):
  478. self.env.push()
  479. try:
  480. args, kwargs = self._get_parameters(value)
  481. filename = args[0]
  482. self.env.update(kwargs)
  483. fname = get_templatefile(filename, self.dirs)
  484. if not fname:
  485. raise TemplateException, "Can't find the template %s" % filename
  486. self.depend_files.append(fname)
  487. f = open(fname, 'rb')
  488. text = f.read()
  489. f.close()
  490. t = Template(text, self.vars, self.env, self.dirs)
  491. t.add_root(self)
  492. t.parse()
  493. self.content.clear_content()
  494. t.content.merge(self.content)
  495. self.content = t.content
  496. finally:
  497. self.env.pop()
  498. def get_parsed_code(self):
  499. if self.use_temp:
  500. f = get_temp_template(self.filename)
  501. if os.path.exists(f):
  502. fin = file(f, 'r')
  503. modified = False
  504. files = [self.filename]
  505. line = fin.readline()
  506. if line.startswith('#uliweb-template-files:'):
  507. files.extend(line[1:].split())
  508. else:
  509. fin.seek(0)
  510. for x in files:
  511. if os.path.getmtime(x) > os.path.getmtime(f):
  512. modified = True
  513. break
  514. if not modified:
  515. text = fin.read()
  516. fin.close()
  517. return True, f, text
  518. if self.filename and not self.text:
  519. self.text = file(self.filename, 'rb').read()
  520. return False, self.filename, self.parse()
  521. def __call__(self):
  522. use_temp_flag, filename, code = self.get_parsed_code()
  523. if not use_temp_flag and self.use_temp:
  524. f = get_temp_template(filename)
  525. try:
  526. fo = file(f, 'wb')
  527. fo.write('#uliweb-template-files:%s\n' % ' '.join(self.depend_files))
  528. fo.write(code)
  529. fo.close()
  530. except:
  531. pass
  532. return self._run(code, filename or 'template')
  533. def _run(self, code, filename):
  534. def f(_vars, _env):
  535. def defined(v, default=None):
  536. _v = default
  537. if v in _vars:
  538. _v = _vars[v]
  539. elif v in _env:
  540. _v = _env[v]
  541. return _v
  542. return defined
  543. e = {}
  544. if isinstance(self.env, Context):
  545. new_e = self.env.to_dict()
  546. else:
  547. new_e = self.env
  548. e.update(new_e)
  549. e.update(self.vars)
  550. out = Out()
  551. e['out'] = out
  552. e['Out'] = Out
  553. e['xml'] = out.xml
  554. e['_vars'] = self.vars
  555. e['defined'] = f(self.vars, self.env)
  556. e['_env'] = e
  557. e.update(self.exec_env)
  558. if isinstance(code, (str, unicode)):
  559. if self.compile:
  560. code = self.compile(code, filename, 'exec', e)
  561. else:
  562. code = compile(code, filename, 'exec')
  563. exec code in e
  564. text = out.getvalue()
  565. for f in self.callbacks:
  566. text = f(text, self, self.vars, e)
  567. return text
  568. def template_file(filename, vars=None, env=None, dirs=None, default_template=None, compile=None):
  569. t = Template('', vars, env, dirs, default_template, use_temp=__options__['use_temp_dir'], compile=compile)
  570. t.set_filename(filename)
  571. return t()
  572. def template(text, vars=None, env=None, dirs=None, default_template=None):
  573. t = Template(text, vars, env, dirs, default_template)
  574. return t()
  575. def test():
  576. """
  577. >>> print template("Hello, {{=name}}", {'name':'uliweb'})
  578. Hello, uliweb
  579. >>> print template("Hello, {{ =name}}", {'name':'uliweb'})
  580. Hello, uliweb
  581. >>> print template("Hello, {{ = name}}", {'name':'uliweb'})
  582. Hello, uliweb
  583. >>> print template("Hello, {{=name}}", {'name':'<h1>Uliweb</h1>'})
  584. Hello, &lt;h1&gt;Uliweb&lt;/h1&gt;
  585. >>> print template("Hello, {{<<name}}", {'name':'<h1>Uliweb</h1>'})
  586. Hello, <h1>Uliweb</h1>
  587. >>> print template('''{{import datetime}}{{=datetime.date( # this breaks
  588. ... 2009,1,8)}}''')
  589. 2009-01-08
  590. """
  591. if __name__ == '__main__':
  592. print template("Hello, {{=name}}", {'name':'uliweb'})