forked from enlightenment/efl
1259 lines
48 KiB
Python
1259 lines
48 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Small, simple and powerful template-engine for Python.
|
|
|
|
A template-engine for Python, which is very simple, easy to use, small,
|
|
fast, powerful, modular, extensible, well documented and pythonic.
|
|
|
|
See documentation for a list of features, template-syntax etc.
|
|
|
|
:Version: 0.3.2
|
|
:Requires: Python >=2.6 / 3.x
|
|
|
|
:Usage:
|
|
see class ``Template`` and examples below.
|
|
|
|
:Example:
|
|
|
|
Note that the examples are in Python 2; they also work in
|
|
Python 3 if you replace u"..." by "...", unicode() by str()
|
|
and partly "..." by b"...".
|
|
|
|
quickstart::
|
|
>>> t = Template("hello @!name!@")
|
|
>>> print(t(name="marvin"))
|
|
hello marvin
|
|
|
|
quickstart with a template-file::
|
|
# >>> t = Template(filename="mytemplate.tmpl")
|
|
# >>> print(t(name="marvin"))
|
|
# hello marvin
|
|
|
|
generic usage::
|
|
>>> t = Template(u"output is in Unicode \\xe4\\xf6\\xfc\\u20ac")
|
|
>>> t #doctest: +ELLIPSIS
|
|
<...Template instance at 0x...>
|
|
>>> t()
|
|
u'output is in Unicode \\xe4\\xf6\\xfc\\u20ac'
|
|
>>> unicode(t)
|
|
u'output is in Unicode \\xe4\\xf6\\xfc\\u20ac'
|
|
|
|
with data::
|
|
>>> t = Template("hello @!name!@", data={"name":"world"})
|
|
>>> t()
|
|
u'hello world'
|
|
>>> t(name="worlds")
|
|
u'hello worlds'
|
|
|
|
# >>> t(note="data must be Unicode or ASCII", name=u"\\xe4")
|
|
# u'hello \\xe4'
|
|
|
|
escaping::
|
|
>>> t = Template("hello escaped: @!name!@, unescaped: $!name!$")
|
|
>>> t(name='''<>&'"''')
|
|
u'hello escaped: <>&'", unescaped: <>&\\'"'
|
|
|
|
result-encoding::
|
|
# encode the unicode-object to your encoding with encode()
|
|
>>> t = Template(u"hello \\xe4\\xf6\\xfc\\u20ac")
|
|
>>> result = t()
|
|
>>> result
|
|
u'hello \\xe4\\xf6\\xfc\\u20ac'
|
|
>>> result.encode("utf-8")
|
|
'hello \\xc3\\xa4\\xc3\\xb6\\xc3\\xbc\\xe2\\x82\\xac'
|
|
>>> result.encode("ascii")
|
|
Traceback (most recent call last):
|
|
...
|
|
UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-9: ordinal not in range(128)
|
|
>>> result.encode("ascii", 'xmlcharrefreplace')
|
|
'hello äöü€'
|
|
|
|
Python-expressions::
|
|
>>> Template('formatted: @! "%8.5f" % value !@')(value=3.141592653)
|
|
u'formatted: 3.14159'
|
|
>>> Template("hello --@!name.upper().center(20)!@--")(name="world")
|
|
u'hello -- WORLD --'
|
|
>>> Template("calculate @!var*5+7!@")(var=7)
|
|
u'calculate 42'
|
|
|
|
blocks (if/for/macros/...)::
|
|
>>> t = Template("<!--(if foo == 1)-->bar<!--(elif foo == 2)-->baz<!--(else)-->unknown(@!foo!@)<!--(end)-->")
|
|
>>> t(foo=2)
|
|
u'baz'
|
|
>>> t(foo=5)
|
|
u'unknown(5)'
|
|
|
|
>>> t = Template("<!--(for i in mylist)-->@!i!@ <!--(else)-->(empty)<!--(end)-->")
|
|
>>> t(mylist=[])
|
|
u'(empty)'
|
|
>>> t(mylist=[1,2,3])
|
|
u'1 2 3 '
|
|
|
|
>>> t = Template("<!--(for i,elem in enumerate(mylist))--> - @!i!@: @!elem!@<!--(end)-->")
|
|
>>> t(mylist=["a","b","c"])
|
|
u' - 0: a - 1: b - 2: c'
|
|
|
|
>>> t = Template('<!--(macro greetings)-->hello <strong>@!name!@</strong><!--(end)--> @!greetings(name=user)!@')
|
|
>>> t(user="monty")
|
|
u' hello <strong>monty</strong>'
|
|
|
|
exists::
|
|
>>> t = Template('<!--(if exists("foo"))-->YES<!--(else)-->NO<!--(end)-->')
|
|
>>> t()
|
|
u'NO'
|
|
>>> t(foo=1)
|
|
u'YES'
|
|
>>> t(foo=None) # note this difference to 'default()'
|
|
u'YES'
|
|
|
|
default-values::
|
|
# non-existing variables raise an error
|
|
>>> Template('hi @!optional!@')()
|
|
Traceback (most recent call last):
|
|
...
|
|
TemplateRenderError: Cannot eval expression 'optional'. (NameError: name 'optional' is not defined)
|
|
|
|
>>> t = Template('hi @!default("optional","anyone")!@')
|
|
>>> t()
|
|
u'hi anyone'
|
|
>>> t(optional=None)
|
|
u'hi anyone'
|
|
>>> t(optional="there")
|
|
u'hi there'
|
|
|
|
# the 1st parameter can be any eval-expression
|
|
>>> t = Template('@!default("5*var1+var2","missing variable")!@')
|
|
>>> t(var1=10)
|
|
u'missing variable'
|
|
>>> t(var1=10, var2=2)
|
|
u'52'
|
|
|
|
# also in blocks
|
|
>>> t = Template('<!--(if default("opt1+opt2",0) > 0)-->yes<!--(else)-->no<!--(end)-->')
|
|
>>> t()
|
|
u'no'
|
|
>>> t(opt1=23, opt2=42)
|
|
u'yes'
|
|
|
|
>>> t = Template('<!--(for i in default("optional_list",[]))-->@!i!@<!--(end)-->')
|
|
>>> t()
|
|
u''
|
|
>>> t(optional_list=[1,2,3])
|
|
u'123'
|
|
|
|
|
|
# but make sure to put the expression in quotation marks, otherwise:
|
|
>>> Template('@!default(optional,"fallback")!@')()
|
|
Traceback (most recent call last):
|
|
...
|
|
TemplateRenderError: Cannot eval expression 'default(optional,"fallback")'. (NameError: name 'optional' is not defined)
|
|
|
|
setvar::
|
|
>>> t = Template('$!setvar("i", "i+1")!$@!i!@')
|
|
>>> t(i=6)
|
|
u'7'
|
|
|
|
>>> t = Template('''<!--(if isinstance(s, (list,tuple)))-->$!setvar("s", '"\\\\\\\\n".join(s)')!$<!--(end)-->@!s!@''')
|
|
>>> t(isinstance=isinstance, s="123")
|
|
u'123'
|
|
>>> t(isinstance=isinstance, s=["123", "456"])
|
|
u'123\\n456'
|
|
|
|
:Author: Roland Koebler (rk at simple-is-better dot org)
|
|
:Copyright: Roland Koebler
|
|
:License: MIT/X11-like, see __license__
|
|
|
|
:RCS: $Id: pyratemp.py,v 1.22 2013/09/17 07:44:13 rk Exp $
|
|
"""
|
|
from __future__ import unicode_literals
|
|
|
|
__version__ = "0.3.2"
|
|
__author__ = "Roland Koebler <rk at simple-is-better dot org>"
|
|
__license__ = """Copyright (c) Roland Koebler, 2007-2013
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
IN THE SOFTWARE."""
|
|
|
|
#=========================================
|
|
|
|
import os, re, sys, types
|
|
if sys.version_info[0] >= 3:
|
|
import builtins
|
|
unicode = str
|
|
long = int
|
|
else:
|
|
import __builtin__ as builtins
|
|
from codecs import open
|
|
|
|
#=========================================
|
|
# some useful functions
|
|
|
|
#----------------------
|
|
# string-position: i <-> row,col
|
|
|
|
def srow(string, i):
|
|
"""Get line numer of ``string[i]`` in `string`.
|
|
|
|
:Returns: row, starting at 1
|
|
:Note: This works for text-strings with ``\\n`` or ``\\r\\n``.
|
|
"""
|
|
return string.count('\n', 0, max(0, i)) + 1
|
|
|
|
def scol(string, i):
|
|
"""Get column number of ``string[i]`` in `string`.
|
|
|
|
:Returns: column, starting at 1 (but may be <1 if i<0)
|
|
:Note: This works for text-strings with ``\\n`` or ``\\r\\n``.
|
|
"""
|
|
return i - string.rfind('\n', 0, max(0, i))
|
|
|
|
def sindex(string, row, col):
|
|
"""Get index of the character at `row`/`col` in `string`.
|
|
|
|
:Parameters:
|
|
- `row`: row number, starting at 1.
|
|
- `col`: column number, starting at 1.
|
|
:Returns: ``i``, starting at 0 (but may be <1 if row/col<0)
|
|
:Note: This works for text-strings with '\\n' or '\\r\\n'.
|
|
"""
|
|
n = 0
|
|
for _ in range(row-1):
|
|
n = string.find('\n', n) + 1
|
|
return n+col-1
|
|
|
|
#----------------------
|
|
|
|
def dictkeyclean(d):
|
|
"""Convert all keys of the dict `d` to strings.
|
|
"""
|
|
new_d = {}
|
|
for k, v in d.items():
|
|
new_d[str(k)] = v
|
|
return new_d
|
|
|
|
#----------------------
|
|
|
|
def dummy(*_, **__):
|
|
"""Dummy function, doing nothing.
|
|
"""
|
|
pass
|
|
|
|
def dummy_raise(exception, value):
|
|
"""Create an exception-raising dummy function.
|
|
|
|
:Returns: dummy function, raising ``exception(value)``
|
|
"""
|
|
def mydummy(*_, **__):
|
|
raise exception(value)
|
|
return mydummy
|
|
|
|
#=========================================
|
|
# escaping
|
|
|
|
(NONE, HTML, LATEX, MAIL_HEADER) = range(0, 4)
|
|
ESCAPE_SUPPORTED = {"NONE":None, "HTML":HTML, "LATEX":LATEX, "MAIL_HEADER":MAIL_HEADER}
|
|
|
|
def escape(s, format=HTML):
|
|
"""Replace special characters by their escape sequence.
|
|
|
|
:Parameters:
|
|
- `s`: unicode-string to escape
|
|
- `format`:
|
|
|
|
- `NONE`: nothing is replaced
|
|
- `HTML`: replace &<>'" by &...;
|
|
- `LATEX`: replace \#$%&_{}~^
|
|
- `MAIL_HEADER`: escape non-ASCII mail-header-contents
|
|
:Returns:
|
|
the escaped string in unicode
|
|
:Exceptions:
|
|
- `ValueError`: if `format` is invalid.
|
|
|
|
:Uses:
|
|
MAIL_HEADER uses module email
|
|
"""
|
|
#Note: If you have to make sure that every character gets replaced
|
|
# only once (and if you cannot achieve this with the following code),
|
|
# use something like "".join([replacedict.get(c,c) for c in s])
|
|
# which is about 2-3 times slower (but maybe needs less memory).
|
|
#Note: This is one of the most time-consuming parts of the template.
|
|
if format is None or format == NONE:
|
|
pass
|
|
elif format == HTML:
|
|
s = s.replace("&", "&") # must be done first!
|
|
s = s.replace("<", "<")
|
|
s = s.replace(">", ">")
|
|
s = s.replace('"', """)
|
|
s = s.replace("'", "'")
|
|
elif format == LATEX:
|
|
s = s.replace("\\", "\\x") #must be done first!
|
|
s = s.replace("#", "\\#")
|
|
s = s.replace("$", "\\$")
|
|
s = s.replace("%", "\\%")
|
|
s = s.replace("&", "\\&")
|
|
s = s.replace("_", "\\_")
|
|
s = s.replace("{", "\\{")
|
|
s = s.replace("}", "\\}")
|
|
s = s.replace("\\x","\\textbackslash{}")
|
|
s = s.replace("~", "\\textasciitilde{}")
|
|
s = s.replace("^", "\\textasciicircum{}")
|
|
elif format == MAIL_HEADER:
|
|
import email.header
|
|
try:
|
|
s.encode("ascii")
|
|
return s
|
|
except UnicodeEncodeError:
|
|
return email.header.make_header([(s, "utf-8")]).encode()
|
|
else:
|
|
raise ValueError('Invalid format (only None, HTML, LATEX and MAIL_HEADER are supported).')
|
|
return s
|
|
|
|
#=========================================
|
|
|
|
#-----------------------------------------
|
|
# Exceptions
|
|
|
|
class TemplateException(Exception):
|
|
"""Base class for template-exceptions."""
|
|
pass
|
|
|
|
class TemplateParseError(TemplateException):
|
|
"""Template parsing failed."""
|
|
def __init__(self, err, errpos):
|
|
"""
|
|
:Parameters:
|
|
- `err`: error-message or exception to wrap
|
|
- `errpos`: ``(filename,row,col)`` where the error occured.
|
|
"""
|
|
self.err = err
|
|
self.filename, self.row, self.col = errpos
|
|
TemplateException.__init__(self)
|
|
def __str__(self):
|
|
if not self.filename:
|
|
return "line %d, col %d: %s" % (self.row, self.col, str(self.err))
|
|
else:
|
|
return "file %s, line %d, col %d: %s" % (self.filename, self.row, self.col, str(self.err))
|
|
|
|
class TemplateSyntaxError(TemplateParseError, SyntaxError):
|
|
"""Template syntax-error."""
|
|
pass
|
|
|
|
class TemplateIncludeError(TemplateParseError):
|
|
"""Template 'include' failed."""
|
|
pass
|
|
|
|
class TemplateRenderError(TemplateException):
|
|
"""Template rendering failed."""
|
|
pass
|
|
|
|
#-----------------------------------------
|
|
# Loader
|
|
|
|
class LoaderString:
|
|
"""Load template from a string/unicode.
|
|
|
|
Note that 'include' is not possible in such templates.
|
|
"""
|
|
def __init__(self, encoding='utf-8'):
|
|
self.encoding = encoding
|
|
|
|
def load(self, s):
|
|
"""Return template-string as unicode.
|
|
"""
|
|
if isinstance(s, unicode):
|
|
u = s
|
|
else:
|
|
u = s.decode(self.encoding)
|
|
return u
|
|
|
|
class LoaderFile:
|
|
"""Load template from a file.
|
|
|
|
When loading a template from a file, it's possible to including other
|
|
templates (by using 'include' in the template). But for simplicity
|
|
and security, all included templates have to be in the same directory!
|
|
(see ``allowed_path``)
|
|
"""
|
|
def __init__(self, allowed_path=None, encoding='utf-8'):
|
|
"""Init the loader.
|
|
|
|
:Parameters:
|
|
- `allowed_path`: path of the template-files
|
|
- `encoding`: encoding of the template-files
|
|
:Exceptions:
|
|
- `ValueError`: if `allowed_path` is not a directory
|
|
"""
|
|
if allowed_path and not os.path.isdir(allowed_path):
|
|
raise ValueError("'allowed_path' has to be a directory.")
|
|
self.path = allowed_path
|
|
self.encoding = encoding
|
|
|
|
def load(self, filename):
|
|
"""Load a template from a file.
|
|
|
|
Check if filename is allowed and return its contens in unicode.
|
|
|
|
:Parameters:
|
|
- `filename`: filename of the template without path
|
|
:Returns:
|
|
the contents of the template-file in unicode
|
|
:Exceptions:
|
|
- `ValueError`: if `filename` contains a path
|
|
"""
|
|
if filename != os.path.basename(filename):
|
|
raise ValueError("No path allowed in filename. (%s)" %(filename))
|
|
filename = os.path.join(self.path, filename)
|
|
|
|
f = open(filename, 'r', encoding=self.encoding)
|
|
u = f.read()
|
|
f.close()
|
|
|
|
return u
|
|
|
|
#-----------------------------------------
|
|
# Parser
|
|
|
|
class Parser(object):
|
|
"""Parse a template into a parse-tree.
|
|
|
|
Includes a syntax-check, an optional expression-check and verbose
|
|
error-messages.
|
|
|
|
See documentation for a description of the parse-tree.
|
|
"""
|
|
# template-syntax (original)
|
|
# _comment_start = "#!"
|
|
# _comment_end = "!#"
|
|
# _sub_start = "$!"
|
|
# _sub_end = "!$"
|
|
# _subesc_start = "@!"
|
|
# _subesc_end = "!@"
|
|
# _block_start = "<!--("
|
|
# _block_end = ")-->"
|
|
|
|
# template-syntax (eolian)
|
|
_comment_start = "#!"
|
|
_comment_end = "!#"
|
|
_sub_start = "${" # "$!"
|
|
_sub_end = "}$" # "!$"
|
|
_subesc_start = "${!" # "@!"
|
|
_subesc_end = "!}$" # "!@"
|
|
_block_start = "<!--("
|
|
_block_end = ")-->"
|
|
|
|
# template-syntax (Jinja2 style)
|
|
# _comment_start = "<!--" # "#!"
|
|
# _comment_end = "-->" # "!#"
|
|
# _sub_start = "{{" # "$!"
|
|
# _sub_end = "}}" # "!$"
|
|
# _subesc_start = "{!" # "@!"
|
|
# _subesc_end = "!}" # "!@"
|
|
# _block_start = "{% " # "<!--("
|
|
# _block_end = " %}" # ")-->"
|
|
|
|
# build regexps
|
|
# comment
|
|
# single-line, until end-tag or end-of-line.
|
|
_strComment = r"""%s(?P<content>.*?)(?P<end>%s|\n|$)""" \
|
|
% (re.escape(_comment_start), re.escape(_comment_end))
|
|
_reComment = re.compile(_strComment, re.M)
|
|
|
|
# escaped or unescaped substitution
|
|
# single-line ("|$" is needed to be able to generate good error-messges)
|
|
_strSubstitution = r"""
|
|
(
|
|
%s\s*(?P<sub>.*?)\s*(?P<end>%s|$) #substitution
|
|
|
|
|
%s\s*(?P<escsub>.*?)\s*(?P<escend>%s|$) #escaped substitution
|
|
)
|
|
""" % (re.escape(_sub_start), re.escape(_sub_end),
|
|
re.escape(_subesc_start), re.escape(_subesc_end))
|
|
_reSubstitution = re.compile(_strSubstitution, re.X|re.M)
|
|
|
|
# block
|
|
# - single-line, no nesting.
|
|
# or
|
|
# - multi-line, nested by whitespace indentation:
|
|
# * start- and end-tag of a block must have exactly the same indentation.
|
|
# * start- and end-tags of *nested* blocks should have a greater indentation.
|
|
# NOTE: A single-line block must not start at beginning of the line with
|
|
# the same indentation as the enclosing multi-line blocks!
|
|
# Note that " " and "\t" are different, although they may
|
|
# look the same in an editor!
|
|
_s = re.escape(_block_start)
|
|
_e = re.escape(_block_end)
|
|
_strBlock = r"""
|
|
^(?P<mEnd>[ \t]*)%send%s(?P<meIgnored>.*)\r?\n? # multi-line end (^ <!--(end)-->IGNORED_TEXT\n)
|
|
|
|
|
(?P<sEnd>)%send%s # single-line end (<!--(end)-->)
|
|
|
|
|
(?P<sSpace>[ \t]*) # single-line tag (no nesting)
|
|
%s(?P<sKeyw>\w+)[ \t]*(?P<sParam>.*?)%s
|
|
(?P<sContent>.*?)
|
|
(?=(?:%s.*?%s.*?)??%send%s) # (match until end or i.e. <!--(elif/else...)-->)
|
|
|
|
|
# multi-line tag, nested by whitespace indentation
|
|
^(?P<indent>[ \t]*) # save indentation of start tag
|
|
%s(?P<mKeyw>\w+)\s*(?P<mParam>.*?)%s(?P<mIgnored>.*)\r?\n
|
|
(?P<mContent>(?:.*\n)*?)
|
|
(?=(?P=indent)%s(?:.|\s)*?%s) # match indentation
|
|
""" % (_s, _e,
|
|
_s, _e,
|
|
_s, _e, _s, _e, _s, _e,
|
|
_s, _e, _s, _e)
|
|
_reBlock = re.compile(_strBlock, re.X|re.M)
|
|
|
|
# "for"-block parameters: "var(,var)* in ..."
|
|
_strForParam = r"""^(?P<names>\w+(?:\s*,\s*\w+)*)\s+in\s+(?P<iter>.+)$"""
|
|
_reForParam = re.compile(_strForParam)
|
|
|
|
# allowed macro-names
|
|
_reMacroParam = re.compile(r"""^\w+$""")
|
|
|
|
|
|
def __init__(self, loadfunc=None, testexpr=None, escape=HTML):
|
|
"""Init the parser.
|
|
|
|
:Parameters:
|
|
- `loadfunc`: function to load included templates
|
|
(i.e. ``LoaderFile(...).load``)
|
|
- `testexpr`: function to test if a template-expressions is valid
|
|
(i.e. ``EvalPseudoSandbox().compile``)
|
|
- `escape`: default-escaping (may be modified by the template)
|
|
:Exceptions:
|
|
- `ValueError`: if `testexpr` or `escape` is invalid.
|
|
"""
|
|
if loadfunc is None:
|
|
self._load = dummy_raise(NotImplementedError, "'include' not supported, since no 'loadfunc' was given.")
|
|
else:
|
|
self._load = loadfunc
|
|
|
|
if testexpr is None:
|
|
self._testexprfunc = dummy
|
|
else:
|
|
try: # test if testexpr() works
|
|
testexpr("i==1")
|
|
except Exception as err:
|
|
raise ValueError("Invalid 'testexpr'. (%s)" %(err))
|
|
self._testexprfunc = testexpr
|
|
|
|
if escape not in ESCAPE_SUPPORTED.values():
|
|
raise ValueError("Unsupported 'escape'. (%s)" %(escape))
|
|
self.escape = escape
|
|
self._includestack = []
|
|
|
|
def parse(self, template):
|
|
"""Parse a template.
|
|
|
|
:Parameters:
|
|
- `template`: template-unicode-string
|
|
:Returns: the resulting parse-tree
|
|
:Exceptions:
|
|
- `TemplateSyntaxError`: for template-syntax-errors
|
|
- `TemplateIncludeError`: if template-inclusion failed
|
|
- `TemplateException`
|
|
"""
|
|
self._includestack = [(None, template)] # for error-messages (_errpos)
|
|
return self._parse(template)
|
|
|
|
def _errpos(self, fpos):
|
|
"""Convert `fpos` to ``(filename,row,column)`` for error-messages."""
|
|
filename, string = self._includestack[-1]
|
|
return filename, srow(string, fpos), scol(string, fpos)
|
|
|
|
def _testexpr(self, expr, fpos=0):
|
|
"""Test a template-expression to detect errors."""
|
|
try:
|
|
self._testexprfunc(expr)
|
|
except SyntaxError as err:
|
|
raise TemplateSyntaxError(err, self._errpos(fpos))
|
|
|
|
def _parse_sub(self, parsetree, text, fpos=0):
|
|
"""Parse substitutions, and append them to the parse-tree.
|
|
|
|
Additionally, remove comments.
|
|
"""
|
|
curr = 0
|
|
for match in self._reSubstitution.finditer(text):
|
|
start = match.start()
|
|
if start > curr:
|
|
parsetree.append(("str", self._reComment.sub('', text[curr:start])))
|
|
|
|
if match.group("sub") is not None:
|
|
if not match.group("end"):
|
|
raise TemplateSyntaxError("Missing closing tag '%s' for '%s'."
|
|
% (self._sub_end, match.group()), self._errpos(fpos+start))
|
|
if len(match.group("sub")) > 0:
|
|
self._testexpr(match.group("sub"), fpos+start)
|
|
parsetree.append(("sub", match.group("sub")))
|
|
else:
|
|
assert(match.group("escsub") is not None)
|
|
if not match.group("escend"):
|
|
raise TemplateSyntaxError("Missing closing tag '%s' for '%s'."
|
|
% (self._subesc_end, match.group()), self._errpos(fpos+start))
|
|
if len(match.group("escsub")) > 0:
|
|
self._testexpr(match.group("escsub"), fpos+start)
|
|
parsetree.append(("esc", self.escape, match.group("escsub")))
|
|
|
|
curr = match.end()
|
|
|
|
if len(text) > curr:
|
|
parsetree.append(("str", self._reComment.sub('', text[curr:])))
|
|
|
|
def _parse(self, template, fpos=0):
|
|
"""Recursive part of `parse()`.
|
|
|
|
:Parameters:
|
|
- template
|
|
- fpos: position of ``template`` in the complete template (for error-messages)
|
|
"""
|
|
# blank out comments
|
|
# (So that its content does not collide with other syntax, and
|
|
# because removing them completely would falsify the character-
|
|
# position ("match.start()") of error-messages)
|
|
template = self._reComment.sub(lambda match: self._comment_start+" "*len(match.group(1))+match.group(2), template)
|
|
|
|
# init parser
|
|
parsetree = []
|
|
curr = 0 # current position (= end of previous block)
|
|
block_type = None # block type: if,for,macro,raw,...
|
|
block_indent = None # None: single-line, >=0: multi-line
|
|
|
|
# find blocks
|
|
for match in self._reBlock.finditer(template):
|
|
start = match.start()
|
|
# process template-part before this block
|
|
if start > curr:
|
|
self._parse_sub(parsetree, template[curr:start], fpos)
|
|
|
|
# analyze block syntax (incl. error-checking and -messages)
|
|
keyword = None
|
|
block = match.groupdict()
|
|
pos__ = fpos + start # shortcut
|
|
if block["sKeyw"] is not None: # single-line block tag
|
|
block_indent = None
|
|
keyword = block["sKeyw"]
|
|
param = block["sParam"]
|
|
content = block["sContent"]
|
|
if block["sSpace"]: # restore spaces before start-tag
|
|
if len(parsetree) > 0 and parsetree[-1][0] == "str":
|
|
parsetree[-1] = ("str", parsetree[-1][1] + block["sSpace"])
|
|
else:
|
|
parsetree.append(("str", block["sSpace"]))
|
|
pos_p = fpos + match.start("sParam") # shortcuts
|
|
pos_c = fpos + match.start("sContent")
|
|
elif block["mKeyw"] is not None: # multi-line block tag
|
|
block_indent = len(block["indent"])
|
|
keyword = block["mKeyw"]
|
|
param = block["mParam"]
|
|
content = block["mContent"]
|
|
pos_p = fpos + match.start("mParam")
|
|
pos_c = fpos + match.start("mContent")
|
|
ignored = block["mIgnored"].strip()
|
|
if ignored and ignored != self._comment_start:
|
|
raise TemplateSyntaxError("No code allowed after block-tag.", self._errpos(fpos+match.start("mIgnored")))
|
|
elif block["mEnd"] is not None: # multi-line block end
|
|
if block_type is None:
|
|
raise TemplateSyntaxError("No block to end here/invalid indent.", self._errpos(pos__) )
|
|
if block_indent != len(block["mEnd"]):
|
|
raise TemplateSyntaxError("Invalid indent for end-tag.", self._errpos(pos__) )
|
|
ignored = block["meIgnored"].strip()
|
|
if ignored and ignored != self._comment_start:
|
|
raise TemplateSyntaxError("No code allowed after end-tag.", self._errpos(fpos+match.start("meIgnored")))
|
|
block_type = None
|
|
elif block["sEnd"] is not None: # single-line block end
|
|
if block_type is None:
|
|
raise TemplateSyntaxError("No block to end here/invalid indent.", self._errpos(pos__))
|
|
if block_indent is not None:
|
|
raise TemplateSyntaxError("Invalid indent for end-tag.", self._errpos(pos__))
|
|
block_type = None
|
|
else:
|
|
raise TemplateException("FATAL: Block regexp error. Please contact the author. (%s)" % match.group())
|
|
|
|
# analyze block content (mainly error-checking and -messages)
|
|
if keyword:
|
|
keyword = keyword.lower()
|
|
if 'for' == keyword:
|
|
if block_type is not None:
|
|
raise TemplateSyntaxError("Missing block-end-tag before new block at '%s'." %(match.group()), self._errpos(pos__))
|
|
block_type = 'for'
|
|
cond = self._reForParam.match(param)
|
|
if cond is None:
|
|
raise TemplateSyntaxError("Invalid 'for ...' at '%s'." %(param), self._errpos(pos_p))
|
|
names = tuple(n.strip() for n in cond.group("names").split(","))
|
|
self._testexpr(cond.group("iter"), pos_p+cond.start("iter"))
|
|
parsetree.append(("for", names, cond.group("iter"), self._parse(content, pos_c)))
|
|
elif 'if' == keyword:
|
|
if block_type is not None:
|
|
raise TemplateSyntaxError("Missing block-end-tag before new block at '%s'." %(match.group()), self._errpos(pos__))
|
|
if not param:
|
|
raise TemplateSyntaxError("Missing condition for 'if' at '%s'." %(match.group()), self._errpos(pos__))
|
|
block_type = 'if'
|
|
self._testexpr(param, pos_p)
|
|
parsetree.append(("if", param, self._parse(content, pos_c)))
|
|
elif 'elif' == keyword:
|
|
if block_type != 'if':
|
|
raise TemplateSyntaxError("'elif' may only appear after 'if' at '%s'." %(match.group()), self._errpos(pos__))
|
|
if not param:
|
|
raise TemplateSyntaxError("Missing condition for 'elif' at '%s'." %(match.group()), self._errpos(pos__))
|
|
self._testexpr(param, pos_p)
|
|
parsetree.append(("elif", param, self._parse(content, pos_c)))
|
|
elif 'else' == keyword:
|
|
if block_type not in ('if', 'for'):
|
|
raise TemplateSyntaxError("'else' may only appear after 'if' or 'for' at '%s'." %(match.group()), self._errpos(pos__))
|
|
if param:
|
|
raise TemplateSyntaxError("'else' may not have parameters at '%s'." %(match.group()), self._errpos(pos__))
|
|
parsetree.append(("else", self._parse(content, pos_c)))
|
|
elif 'macro' == keyword:
|
|
if block_type is not None:
|
|
raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos(pos__))
|
|
block_type = 'macro'
|
|
# make sure param is "\w+" (instead of ".+")
|
|
if not param:
|
|
raise TemplateSyntaxError("Missing name for 'macro' at '%s'." %(match.group()), self._errpos(pos__))
|
|
if not self._reMacroParam.match(param):
|
|
raise TemplateSyntaxError("Invalid name for 'macro' at '%s'." %(match.group()), self._errpos(pos__))
|
|
#remove last newline
|
|
if len(content) > 0 and content[-1] == '\n':
|
|
content = content[:-1]
|
|
if len(content) > 0 and content[-1] == '\r':
|
|
content = content[:-1]
|
|
parsetree.append(("macro", param, self._parse(content, pos_c)))
|
|
|
|
# parser-commands
|
|
elif 'raw' == keyword:
|
|
if block_type is not None:
|
|
raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos(pos__))
|
|
if param:
|
|
raise TemplateSyntaxError("'raw' may not have parameters at '%s'." %(match.group()), self._errpos(pos__))
|
|
block_type = 'raw'
|
|
parsetree.append(("str", content))
|
|
elif 'include' == keyword:
|
|
if block_type is not None:
|
|
raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos(pos__))
|
|
if param:
|
|
raise TemplateSyntaxError("'include' may not have parameters at '%s'." %(match.group()), self._errpos(pos__))
|
|
block_type = 'include'
|
|
try:
|
|
u = self._load(content.strip())
|
|
except Exception as err:
|
|
raise TemplateIncludeError(err, self._errpos(pos__))
|
|
self._includestack.append((content.strip(), u)) # current filename/template for error-msg.
|
|
p = self._parse(u)
|
|
self._includestack.pop()
|
|
parsetree.extend(p)
|
|
elif 'set_escape' == keyword:
|
|
if block_type is not None:
|
|
raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos(pos__))
|
|
if param:
|
|
raise TemplateSyntaxError("'set_escape' may not have parameters at '%s'." %(match.group()), self._errpos(pos__))
|
|
block_type = 'set_escape'
|
|
esc = content.strip().upper()
|
|
if esc not in ESCAPE_SUPPORTED:
|
|
raise TemplateSyntaxError("Unsupported escape '%s'." %(esc), self._errpos(pos__))
|
|
self.escape = ESCAPE_SUPPORTED[esc]
|
|
else:
|
|
raise TemplateSyntaxError("Invalid keyword '%s'." %(keyword), self._errpos(pos__))
|
|
curr = match.end()
|
|
|
|
if block_type is not None:
|
|
raise TemplateSyntaxError("Missing end-tag.", self._errpos(pos__))
|
|
|
|
if len(template) > curr: # process template-part after last block
|
|
self._parse_sub(parsetree, template[curr:], fpos+curr)
|
|
|
|
return parsetree
|
|
|
|
#-----------------------------------------
|
|
# Evaluation
|
|
|
|
# some checks
|
|
assert len(eval("dir()", {'__builtins__':{'dir':dir}})) == 1, \
|
|
"FATAL: 'eval' does not work as expected (%s)."
|
|
assert compile("0 .__class__", "<string>", "eval").co_names == ('__class__',), \
|
|
"FATAL: 'compile' does not work as expected."
|
|
|
|
class EvalPseudoSandbox:
|
|
"""An eval-pseudo-sandbox.
|
|
|
|
The pseudo-sandbox restricts the available functions/objects, so the
|
|
code can only access:
|
|
|
|
- some of the builtin Python-functions, which are considered "safe"
|
|
(see safe_builtins)
|
|
- some additional functions (exists(), default(), setvar(), escape())
|
|
- the passed objects incl. their methods.
|
|
|
|
Additionally, names beginning with "_" are forbidden.
|
|
This is to prevent things like '0 .__class__', with which you could
|
|
easily break out of a "sandbox".
|
|
|
|
Be careful to only pass "safe" objects/functions to the template,
|
|
because any unsafe function/method could break the sandbox!
|
|
For maximum security, restrict the access to as few objects/functions
|
|
as possible!
|
|
|
|
:Warning:
|
|
Note that this is no real sandbox! (And although I don't know any
|
|
way to break out of the sandbox without passing-in an unsafe object,
|
|
I cannot guarantee that there is no such way. So use with care.)
|
|
|
|
Take care if you want to use it for untrusted code!!
|
|
"""
|
|
|
|
safe_builtins = {
|
|
"True" : True,
|
|
"False" : False,
|
|
"None" : None,
|
|
|
|
"abs" : builtins.abs,
|
|
"chr" : builtins.chr,
|
|
"divmod" : builtins.divmod,
|
|
"hash" : builtins.hash,
|
|
"hex" : builtins.hex,
|
|
"isinstance": builtins.isinstance,
|
|
"len" : builtins.len,
|
|
"max" : builtins.max,
|
|
"min" : builtins.min,
|
|
"oct" : builtins.oct,
|
|
"ord" : builtins.ord,
|
|
"pow" : builtins.pow,
|
|
"range" : builtins.range,
|
|
"round" : builtins.round,
|
|
"sorted" : builtins.sorted,
|
|
"sum" : builtins.sum,
|
|
"unichr" : builtins.chr,
|
|
"zip" : builtins.zip,
|
|
|
|
"bool" : builtins.bool,
|
|
"bytes" : builtins.bytes,
|
|
"complex" : builtins.complex,
|
|
"dict" : builtins.dict,
|
|
"enumerate" : builtins.enumerate,
|
|
"float" : builtins.float,
|
|
"int" : builtins.int,
|
|
"list" : builtins.list,
|
|
"long" : long,
|
|
"reversed" : builtins.reversed,
|
|
"set" : builtins.set,
|
|
"str" : builtins.str,
|
|
"tuple" : builtins.tuple,
|
|
"unicode" : unicode,
|
|
|
|
"dir" : builtins.dir,
|
|
}
|
|
if sys.version_info[0] < 3:
|
|
safe_builtins["unichr"] = builtins.unichr
|
|
|
|
def __init__(self):
|
|
self._compile_cache = {}
|
|
self.vars_ptr = None
|
|
self.eval_allowed_builtins = self.safe_builtins.copy()
|
|
self.register("__import__", self.f_import)
|
|
self.register("exists", self.f_exists)
|
|
self.register("default", self.f_default)
|
|
self.register("setvar", self.f_setvar)
|
|
self.register("escape", self.f_escape)
|
|
|
|
def register(self, name, obj):
|
|
"""Add an object to the "allowed eval-builtins".
|
|
|
|
Mainly useful to add user-defined functions to the pseudo-sandbox.
|
|
"""
|
|
self.eval_allowed_builtins[name] = obj
|
|
|
|
def _check_code_names(self, code, expr):
|
|
"""Check if the code tries to access names beginning with "_".
|
|
|
|
Used to prevent sandbox-breakouts via new-style-classes, like
|
|
``"".__class__.__base__.__subclasses__()``.
|
|
|
|
:Raises:
|
|
NameError if expression contains forbidden names.
|
|
"""
|
|
for name in code.co_names:
|
|
if name[0] == '_' and name != '_[1]': # _[1] is necessary for [x for x in y]
|
|
raise NameError("Name '%s' is not allowed in '%s'." % (name, expr))
|
|
# recursively check sub-codes (e.g. lambdas)
|
|
for const in code.co_consts:
|
|
if isinstance(const, types.CodeType):
|
|
self._check_code_names(const, expr)
|
|
|
|
def compile(self, expr):
|
|
"""Compile a Python-eval-expression.
|
|
|
|
- Use a compile-cache.
|
|
- Raise a `NameError` if `expr` contains a name beginning with ``_``.
|
|
|
|
:Returns: the compiled `expr`
|
|
:Exceptions:
|
|
- `SyntaxError`: for compile-errors
|
|
- `NameError`: if expr contains a name beginning with ``_``
|
|
"""
|
|
if expr not in self._compile_cache:
|
|
c = compile(expr, "", "eval")
|
|
self._check_code_names(c, expr)
|
|
self._compile_cache[expr] = c
|
|
return self._compile_cache[expr]
|
|
|
|
def eval(self, expr, variables):
|
|
"""Eval a Python-eval-expression.
|
|
|
|
Sets ``self.vars_ptr`` to ``variables`` and compiles the code
|
|
before evaluating.
|
|
"""
|
|
sav = self.vars_ptr
|
|
self.vars_ptr = variables
|
|
|
|
try:
|
|
x = eval(self.compile(expr), {"__builtins__": self.eval_allowed_builtins}, variables)
|
|
except NameError:
|
|
# workaround for lambdas like ``sorted(..., key=lambda x: my_f(x))``
|
|
vars2 = {"__builtins__": self.eval_allowed_builtins}
|
|
vars2.update(variables)
|
|
x = eval(self.compile(expr), vars2)
|
|
|
|
self.vars_ptr = sav
|
|
return x
|
|
|
|
def f_import(self, name, *_, **__):
|
|
"""``import``/``__import__()`` for the sandboxed code.
|
|
|
|
Since "import" is insecure, the PseudoSandbox does not allow to
|
|
import other modules. But since some functions need to import
|
|
other modules (e.g. "datetime.datetime.strftime" imports "time"),
|
|
this function replaces the builtin "import" and allows to use
|
|
modules which are already accessible by the sandboxed code.
|
|
|
|
:Note:
|
|
- This probably only works for rather simple imports.
|
|
- For security, it may be better to avoid such (complex) modules
|
|
which import other modules. (e.g. use time.localtime and
|
|
time.strftime instead of datetime.datetime.strftime,
|
|
or write a small wrapper.)
|
|
|
|
:Example:
|
|
|
|
>>> from datetime import datetime
|
|
>>> import pyratemp
|
|
>>> t = pyratemp.Template('@!mytime.strftime("%H:%M:%S")!@')
|
|
|
|
# >>> print(t(mytime=datetime.now()))
|
|
# Traceback (most recent call last):
|
|
# ...
|
|
# ImportError: import not allowed in pseudo-sandbox; try to import 'time' yourself and pass it to the sandbox/template
|
|
|
|
>>> import time
|
|
>>> print(t(mytime=datetime.strptime("13:40:54", "%H:%M:%S"), time=time))
|
|
13:40:54
|
|
|
|
# >>> print(t(mytime=datetime.now(), time=time))
|
|
# 13:40:54
|
|
"""
|
|
if self.vars_ptr is not None and name in self.vars_ptr and isinstance(self.vars_ptr[name], types.ModuleType):
|
|
return self.vars_ptr[name]
|
|
else:
|
|
raise ImportError("import not allowed in pseudo-sandbox; try to import '%s' yourself (and maybe pass it to the sandbox/template)" % name)
|
|
|
|
def f_exists(self, varname):
|
|
"""``exists()`` for the sandboxed code.
|
|
|
|
Test if the variable `varname` exists in the current namespace.
|
|
|
|
This only works for single variable names. If you want to test
|
|
complicated expressions, use i.e. `default`.
|
|
(i.e. `default("expr",False)`)
|
|
|
|
:Note: the variable-name has to be quoted! (like in eval)
|
|
:Example: see module-docstring
|
|
"""
|
|
return (varname in self.vars_ptr)
|
|
|
|
def f_default(self, expr, default=None):
|
|
"""``default()`` for the sandboxed code.
|
|
|
|
Try to evaluate an expression and return the result or a
|
|
fallback-/default-value; the `default`-value is used
|
|
if `expr` does not exist/is invalid/results in None.
|
|
|
|
This is very useful for optional data.
|
|
|
|
:Parameter:
|
|
- expr: "eval-expression"
|
|
- default: fallback-value if eval(expr) fails or is None.
|
|
:Returns:
|
|
the eval-result or the "fallback"-value.
|
|
|
|
:Note: the eval-expression has to be quoted! (like in eval)
|
|
:Example: see module-docstring
|
|
"""
|
|
try:
|
|
r = self.eval(expr, self.vars_ptr)
|
|
if r is None:
|
|
return default
|
|
return r
|
|
#TODO: which exceptions should be catched here?
|
|
except (NameError, LookupError, TypeError, AttributeError):
|
|
return default
|
|
|
|
def f_setvar(self, name, expr):
|
|
"""``setvar()`` for the sandboxed code.
|
|
|
|
Set a variable.
|
|
|
|
:Example: see module-docstring
|
|
"""
|
|
self.vars_ptr[name] = self.eval(expr, self.vars_ptr)
|
|
return ""
|
|
|
|
def f_escape(self, s, format="HTML"):
|
|
"""``escape()`` for the sandboxed code.
|
|
"""
|
|
if isinstance(format, (str, unicode)):
|
|
format = ESCAPE_SUPPORTED[format.upper()]
|
|
return escape(unicode(s), format)
|
|
|
|
#-----------------------------------------
|
|
# basic template / subtemplate
|
|
|
|
class TemplateBase:
|
|
"""Basic template-class.
|
|
|
|
Used both for the template itself and for 'macro's ("subtemplates") in
|
|
the template.
|
|
"""
|
|
|
|
def __init__(self, parsetree, renderfunc, data=None):
|
|
"""Create the Template/Subtemplate/Macro.
|
|
|
|
:Parameters:
|
|
- `parsetree`: parse-tree of the template/subtemplate/macro
|
|
- `renderfunc`: render-function
|
|
- `data`: data to fill into the template by default (dictionary).
|
|
This data may later be overridden when rendering the template.
|
|
:Exceptions:
|
|
- `TypeError`: if `data` is not a dictionary
|
|
"""
|
|
#TODO: parameter-checking?
|
|
self.parsetree = parsetree
|
|
if isinstance(data, dict):
|
|
self.data = data
|
|
elif data is None:
|
|
self.data = {}
|
|
else:
|
|
raise TypeError('"data" must be a dict (or None).')
|
|
self.current_data = data
|
|
self._render = renderfunc
|
|
|
|
def __call__(self, **override):
|
|
"""Fill out/render the template.
|
|
|
|
:Parameters:
|
|
- `override`: objects to add to the data-namespace, overriding
|
|
the "default"-data.
|
|
:Returns: the filled template (in unicode)
|
|
:Note: This is also called when invoking macros
|
|
(i.e. ``$!mymacro()!$``).
|
|
"""
|
|
self.current_data = self.data.copy()
|
|
self.current_data.update(override)
|
|
u = "".join(self._render(self.parsetree, self.current_data))
|
|
self.current_data = self.data # restore current_data
|
|
return _dontescape(u) # (see class _dontescape)
|
|
|
|
def __unicode__(self):
|
|
"""Alias for __call__()."""
|
|
return self.__call__()
|
|
def __str__(self):
|
|
"""Alias for __call__()."""
|
|
return self.__call__()
|
|
|
|
#-----------------------------------------
|
|
# Renderer
|
|
|
|
class _dontescape(unicode):
|
|
"""Unicode-string which should not be escaped.
|
|
|
|
If ``isinstance(object,_dontescape)``, then don't escape the object in
|
|
``@!...!@``. It's useful for not double-escaping macros, and it's
|
|
automatically used for macros/subtemplates.
|
|
|
|
:Note: This only works if the object is used on its own in ``@!...!@``.
|
|
It i.e. does not work in ``@!object*2!@`` or ``@!object + "hi"!@``.
|
|
"""
|
|
__slots__ = []
|
|
|
|
|
|
class Renderer(object):
|
|
"""Render a template-parse-tree.
|
|
|
|
:Uses: `TemplateBase` for macros
|
|
"""
|
|
|
|
def __init__(self, evalfunc, escapefunc):
|
|
"""Init the renderer.
|
|
|
|
:Parameters:
|
|
- `evalfunc`: function for template-expression-evaluation
|
|
(i.e. ``EvalPseudoSandbox().eval``)
|
|
- `escapefunc`: function for escaping special characters
|
|
(i.e. `escape`)
|
|
"""
|
|
#TODO: test evalfunc
|
|
self.evalfunc = evalfunc
|
|
self.escapefunc = escapefunc
|
|
|
|
def _eval(self, expr, data):
|
|
"""evalfunc with error-messages"""
|
|
try:
|
|
return self.evalfunc(expr, data)
|
|
#TODO: any other errors to catch here?
|
|
except (TypeError, NameError, LookupError, AttributeError, SyntaxError) as err:
|
|
raise TemplateRenderError("Cannot eval expression '%s'. (%s: %s)" %(expr, err.__class__.__name__, err))
|
|
|
|
def render(self, parsetree, data):
|
|
"""Render a parse-tree of a template.
|
|
|
|
:Parameters:
|
|
- `parsetree`: the parse-tree
|
|
- `data`: the data to fill into the template (dictionary)
|
|
:Returns: the rendered output-unicode-string
|
|
:Exceptions:
|
|
- `TemplateRenderError`
|
|
"""
|
|
_eval = self._eval # shortcut
|
|
output = []
|
|
do_else = False # use else/elif-branch?
|
|
|
|
if parsetree is None:
|
|
return ""
|
|
for elem in parsetree:
|
|
if "str" == elem[0]:
|
|
output.append(elem[1])
|
|
elif "sub" == elem[0]:
|
|
output.append(unicode(_eval(elem[1], data)))
|
|
elif "esc" == elem[0]:
|
|
obj = _eval(elem[2], data)
|
|
#prevent double-escape
|
|
if isinstance(obj, _dontescape) or isinstance(obj, TemplateBase):
|
|
output.append(unicode(obj))
|
|
else:
|
|
output.append(self.escapefunc(unicode(obj), elem[1]))
|
|
elif "for" == elem[0]:
|
|
do_else = True
|
|
(names, iterable) = elem[1:3]
|
|
try:
|
|
loop_iter = iter(_eval(iterable, data))
|
|
except TypeError:
|
|
raise TemplateRenderError("Cannot loop over '%s'." % iterable)
|
|
for i in loop_iter:
|
|
do_else = False
|
|
if len(names) == 1:
|
|
data[names[0]] = i
|
|
else:
|
|
data.update(zip(names, i)) #"for a,b,.. in list"
|
|
output.extend(self.render(elem[3], data))
|
|
elif "if" == elem[0]:
|
|
do_else = True
|
|
if _eval(elem[1], data):
|
|
do_else = False
|
|
output.extend(self.render(elem[2], data))
|
|
elif "elif" == elem[0]:
|
|
if do_else and _eval(elem[1], data):
|
|
do_else = False
|
|
output.extend(self.render(elem[2], data))
|
|
elif "else" == elem[0]:
|
|
if do_else:
|
|
do_else = False
|
|
output.extend(self.render(elem[1], data))
|
|
elif "macro" == elem[0]:
|
|
data[elem[1]] = TemplateBase(elem[2], self.render, data)
|
|
else:
|
|
raise TemplateRenderError("Invalid parse-tree (%s)." %(elem))
|
|
|
|
return output
|
|
|
|
#-----------------------------------------
|
|
# template user-interface (putting it all together)
|
|
|
|
class Template(TemplateBase):
|
|
"""Template-User-Interface.
|
|
|
|
:Usage:
|
|
::
|
|
t = Template(...) (<- see __init__)
|
|
output = t(...) (<- see TemplateBase.__call__)
|
|
|
|
:Example:
|
|
see module-docstring
|
|
"""
|
|
|
|
def __init__(self, string=None,filename=None,parsetree=None, encoding='utf-8', data=None, escape=HTML,
|
|
loader_class=LoaderFile,
|
|
parser_class=Parser,
|
|
renderer_class=Renderer,
|
|
eval_class=EvalPseudoSandbox,
|
|
escape_func=escape):
|
|
"""Load (+parse) a template.
|
|
|
|
:Parameters:
|
|
- `string,filename,parsetree`: a template-string,
|
|
filename of a template to load,
|
|
or a template-parsetree.
|
|
(only one of these 3 is allowed)
|
|
- `encoding`: encoding of the template-files (only used for "filename")
|
|
- `data`: data to fill into the template by default (dictionary).
|
|
This data may later be overridden when rendering the template.
|
|
- `escape`: default-escaping for the template, may be overwritten by the template!
|
|
- `loader_class`
|
|
- `parser_class`
|
|
- `renderer_class`
|
|
- `eval_class`
|
|
- `escapefunc`
|
|
"""
|
|
if [string, filename, parsetree].count(None) != 2:
|
|
raise ValueError('Exactly 1 of string,filename,parsetree is necessary.')
|
|
|
|
tmpl = None
|
|
# load template
|
|
if filename is not None:
|
|
incl_load = loader_class(os.path.dirname(filename), encoding).load
|
|
tmpl = incl_load(os.path.basename(filename))
|
|
if string is not None:
|
|
incl_load = dummy_raise(NotImplementedError, "'include' not supported for template-strings.")
|
|
tmpl = LoaderString(encoding).load(string)
|
|
|
|
# eval (incl. compile-cache)
|
|
templateeval = eval_class()
|
|
|
|
# parse
|
|
if tmpl is not None:
|
|
p = parser_class(loadfunc=incl_load, testexpr=templateeval.compile, escape=escape)
|
|
parsetree = p.parse(tmpl)
|
|
del p
|
|
|
|
# renderer
|
|
renderfunc = renderer_class(templateeval.eval, escape_func).render
|
|
|
|
#create template
|
|
TemplateBase.__init__(self, parsetree, renderfunc, data)
|
|
|
|
|
|
#=========================================
|