371 lines
10 KiB
Python
371 lines
10 KiB
Python
|
"""PyPlate : a simple Python-based templating program
|
||
|
|
||
|
PyPlate parses a file and replaces directives (in double square brackets [[ ... ]])
|
||
|
by various means using a given dictionary of variables. Arbitrary Python code
|
||
|
can be run inside many of the directives, making this system highly flexible.
|
||
|
|
||
|
Usage:
|
||
|
# Load and parse template file
|
||
|
template = pyplate.Template("output") (filename or string)
|
||
|
# Execute it with a dictionary of variables
|
||
|
template.execute_file(output_stream, locals())
|
||
|
|
||
|
PyPlate defines the following directives:
|
||
|
[[...]] evaluate the arbitrary Python expression and insert the
|
||
|
result into the output
|
||
|
|
||
|
[[# ... #]] comment.
|
||
|
|
||
|
[[exec ...]] execute arbitrary Python code in the sandbox namespace
|
||
|
|
||
|
[[if ...]] conditional expressions with usual Python semantics
|
||
|
[[elif ...]]
|
||
|
[[else]]
|
||
|
[[end]]
|
||
|
|
||
|
[[for ... in ...]] for-loop with usual Python semantics
|
||
|
[[end]]
|
||
|
|
||
|
[[def ...(...)]] define a "function" out of other templating elements
|
||
|
[[end]]
|
||
|
|
||
|
[[call ...]] call a templating function (not a regular Python function)
|
||
|
"""
|
||
|
|
||
|
#
|
||
|
# Copyright (C) 2002 Michael Droettboom
|
||
|
#
|
||
|
# This program is free software; you can redistribute it and/or
|
||
|
# modify it under the terms of the GNU General Public License
|
||
|
# as published by the Free Software Foundation; either version 2
|
||
|
# of the License, or (at your option) any later version.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License
|
||
|
# along with this program; if not, write to the Free Software
|
||
|
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||
|
#
|
||
|
|
||
|
from __future__ import nested_scopes
|
||
|
import sys, string, re, io
|
||
|
|
||
|
re_directive = re.compile("\[\[(.*)\]\]")
|
||
|
re_for_loop = re.compile("for (.*) in (.*)")
|
||
|
re_if = re.compile("if (.*)")
|
||
|
re_elif = re.compile("elif (.*)")
|
||
|
re_def = re.compile("def (.*?)\((.*)\)")
|
||
|
re_call = re.compile("call (.*?)\((.*)\)")
|
||
|
re_exec = re.compile("exec (.*)")
|
||
|
re_comment = re.compile("#(.*)#")
|
||
|
|
||
|
############################################################
|
||
|
# Template parser
|
||
|
class ParserException(Exception):
|
||
|
def __init__(self, lineno, s):
|
||
|
Exception.__init__(self, "line %d: %s" % (lineno, s))
|
||
|
|
||
|
class Template:
|
||
|
def __init__(self, filename=None):
|
||
|
if filename != None:
|
||
|
try:
|
||
|
self.parse_file(filename)
|
||
|
except:
|
||
|
self.parse_string(filename)
|
||
|
|
||
|
def parse_file(self, filename):
|
||
|
file = open(filename, 'r')
|
||
|
self.parse(file)
|
||
|
file.close()
|
||
|
|
||
|
def parse_string(self, template):
|
||
|
if sys.version_info >= (3,0):
|
||
|
file = io.StringIO(template)
|
||
|
else:
|
||
|
file = io.StringIO(template.decode('utf-8'))
|
||
|
self.parse(file)
|
||
|
file.close()
|
||
|
|
||
|
def parse(self, file):
|
||
|
self.file = file
|
||
|
self.line = self.file.read()
|
||
|
self.lineno = 0
|
||
|
self.functions = {}
|
||
|
self.tree = TopLevelTemplateNode(self)
|
||
|
|
||
|
def parser_get(self):
|
||
|
if self.line == '':
|
||
|
return None
|
||
|
return self.line
|
||
|
|
||
|
def parser_eat(self, chars):
|
||
|
self.lineno = self.lineno + self.line[:chars].count("\n")
|
||
|
self.line = self.line[chars:]
|
||
|
|
||
|
def parser_exception(self, s):
|
||
|
raise ParserException(self.lineno, s)
|
||
|
|
||
|
def execute_file(self, filename, data):
|
||
|
file = open(filename, 'w')
|
||
|
self.execute(file, data)
|
||
|
file.close()
|
||
|
|
||
|
def execute_string(self, data):
|
||
|
s = io.StringIO()
|
||
|
self.execute(s, data)
|
||
|
return s.getvalue()
|
||
|
|
||
|
def execute_stdout(self, data):
|
||
|
self.execute(sys.stdout, data)
|
||
|
|
||
|
def execute(self, stream=sys.stdout, data={}):
|
||
|
self.tree.execute(stream, data)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return repr(self.tree)
|
||
|
|
||
|
|
||
|
############################################################
|
||
|
# NODES
|
||
|
class TemplateNode:
|
||
|
def __init__(self, parent, s):
|
||
|
self.parent = parent
|
||
|
self.s = s
|
||
|
self.node_list = []
|
||
|
while 1:
|
||
|
new_node = TemplateNodeFactory(parent)
|
||
|
if self.add_node(new_node):
|
||
|
break
|
||
|
|
||
|
def add_node(self, node):
|
||
|
if node == 'end':
|
||
|
return 1
|
||
|
elif node != None:
|
||
|
self.node_list.append(node)
|
||
|
else:
|
||
|
raise self.parent.parser_exception(
|
||
|
"[[%s]] does not have a matching [[end]]" % self.s)
|
||
|
|
||
|
def execute(self, stream, data):
|
||
|
for node in self.node_list:
|
||
|
node.execute(stream, data)
|
||
|
|
||
|
def __repr__(self):
|
||
|
r = "<" + self.__class__.__name__ + " "
|
||
|
for i in self.node_list:
|
||
|
r = r + repr(i)
|
||
|
r = r + ">"
|
||
|
return r
|
||
|
|
||
|
class TopLevelTemplateNode(TemplateNode):
|
||
|
def __init__(self, parent):
|
||
|
TemplateNode.__init__(self, parent, '')
|
||
|
|
||
|
def add_node(self, node):
|
||
|
if node != None:
|
||
|
self.node_list.append(node)
|
||
|
else:
|
||
|
return 1
|
||
|
|
||
|
class ForTemplateNode(TemplateNode):
|
||
|
def __init__(self, parent, s):
|
||
|
TemplateNode.__init__(self, parent, s)
|
||
|
match = re_for_loop.match(s)
|
||
|
if match == None:
|
||
|
raise self.parent.parser_exception(
|
||
|
"[[%s]] is not a valid for-loop expression" % self.s)
|
||
|
else:
|
||
|
self.vars_temp = match.group(1).split(",")
|
||
|
self.vars = []
|
||
|
for v in self.vars_temp:
|
||
|
self.vars.append(v.strip())
|
||
|
#print self.vars
|
||
|
self.expression = match.group(2)
|
||
|
|
||
|
def execute(self, stream, data):
|
||
|
remember_vars = {}
|
||
|
for var in self.vars:
|
||
|
if var in data:
|
||
|
remember_vars[var] = data[var]
|
||
|
for list in eval(self.expression, globals(), data):
|
||
|
if is_sequence(list):
|
||
|
for index, value in enumerate(list):
|
||
|
data[self.vars[index]] = value
|
||
|
else:
|
||
|
data[self.vars[0]] = list
|
||
|
TemplateNode.execute(self, stream, data)
|
||
|
for key, value in remember_vars.items():
|
||
|
data[key] = value
|
||
|
|
||
|
class IfTemplateNode(TemplateNode):
|
||
|
def __init__(self, parent, s):
|
||
|
self.else_node = None
|
||
|
TemplateNode.__init__(self, parent, s)
|
||
|
match = re_if.match(s)
|
||
|
if match == None:
|
||
|
raise self.parent.parser_exception(
|
||
|
"[[%s]] is not a valid if expression" % self.s)
|
||
|
else:
|
||
|
self.expression = match.group(1)
|
||
|
|
||
|
def add_node(self, node):
|
||
|
if node == 'end':
|
||
|
return 1
|
||
|
elif isinstance(node, ElseTemplateNode):
|
||
|
self.else_node = node
|
||
|
return 1
|
||
|
elif isinstance(node, ElifTemplateNode):
|
||
|
self.else_node = node
|
||
|
return 1
|
||
|
elif node != None:
|
||
|
self.node_list.append(node)
|
||
|
else:
|
||
|
raise self.parent.parser_exception(
|
||
|
"[[%s]] does not have a matching [[end]]" % self.s)
|
||
|
|
||
|
def execute(self, stream, data):
|
||
|
if eval(self.expression, globals(), data):
|
||
|
TemplateNode.execute(self, stream, data)
|
||
|
elif self.else_node != None:
|
||
|
self.else_node.execute(stream, data)
|
||
|
|
||
|
class ElifTemplateNode(IfTemplateNode):
|
||
|
def __init__(self, parent, s):
|
||
|
self.else_node = None
|
||
|
TemplateNode.__init__(self, parent, s)
|
||
|
match = re_elif.match(s)
|
||
|
if match == None:
|
||
|
self.parent.parser_exception(
|
||
|
"[[%s]] is not a valid elif expression" % self.s)
|
||
|
else:
|
||
|
self.expression = match.group(1)
|
||
|
|
||
|
class ElseTemplateNode(TemplateNode):
|
||
|
pass
|
||
|
|
||
|
class FunctionTemplateNode(TemplateNode):
|
||
|
def __init__(self, parent, s):
|
||
|
TemplateNode.__init__(self, parent, s)
|
||
|
match = re_def.match(s)
|
||
|
if match == None:
|
||
|
self.parent.parser_exception(
|
||
|
"[[%s]] is not a valid function definition" % self.s)
|
||
|
self.function_name = match.group(1)
|
||
|
self.vars_temp = match.group(2).split(",")
|
||
|
self.vars = []
|
||
|
for v in self.vars_temp:
|
||
|
self.vars.append(v.strip())
|
||
|
#print self.vars
|
||
|
self.parent.functions[self.function_name] = self
|
||
|
|
||
|
def execute(self, stream, data):
|
||
|
pass
|
||
|
|
||
|
def call(self, args, stream, data):
|
||
|
remember_vars = {}
|
||
|
for index, var in enumerate(self.vars):
|
||
|
if var in data:
|
||
|
remember_vars[var] = data[var]
|
||
|
data[var] = args[index]
|
||
|
TemplateNode.execute(self, stream, data)
|
||
|
for key, value in remember_vars.items():
|
||
|
data[key] = value
|
||
|
|
||
|
class LeafTemplateNode(TemplateNode):
|
||
|
def __init__(self, parent, s):
|
||
|
self.parent = parent
|
||
|
self.s = s
|
||
|
|
||
|
def execute(self, stream, data):
|
||
|
stream.write(self.s)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "<" + self.__class__.__name__ + ">"
|
||
|
|
||
|
class CommentTemplateNode(LeafTemplateNode):
|
||
|
def execute(self, stream, data):
|
||
|
pass
|
||
|
|
||
|
class ExpressionTemplateNode(LeafTemplateNode):
|
||
|
def execute(self, stream, data):
|
||
|
if sys.version_info >= (3,0):
|
||
|
stream.write(str(eval(self.s, globals(), data)))
|
||
|
else:
|
||
|
stream.write(str(eval(self.s, globals(), data)).decode('utf-8'))
|
||
|
|
||
|
class ExecTemplateNode(LeafTemplateNode):
|
||
|
def __init__(self, parent, s):
|
||
|
LeafTemplateNode.__init__(self, parent, s)
|
||
|
match = re_exec.match(s)
|
||
|
if match == None:
|
||
|
self.parent.parser_exception(
|
||
|
"[[%s]] is not a valid statement" % self.s)
|
||
|
self.s = match.group(1)
|
||
|
|
||
|
def execute(self, stream, data):
|
||
|
exec(self.s, globals(), data)
|
||
|
pass
|
||
|
|
||
|
class CallTemplateNode(LeafTemplateNode):
|
||
|
def __init__(self, parent, s):
|
||
|
LeafTemplateNode.__init__(self, parent, s)
|
||
|
match = re_call.match(s)
|
||
|
if match == None:
|
||
|
self.parent.parser_exception(
|
||
|
"[[%s]] is not a valid function call" % self.s)
|
||
|
self.function_name = match.group(1)
|
||
|
self.vars = "(" + match.group(2).strip() + ",)"
|
||
|
|
||
|
def execute(self, stream, data):
|
||
|
self.parent.functions[self.function_name].call(
|
||
|
eval(self.vars, globals(), data), stream, data)
|
||
|
|
||
|
|
||
|
############################################################
|
||
|
# Node factory
|
||
|
template_factory_type_map = {
|
||
|
'if' : IfTemplateNode,
|
||
|
'for' : ForTemplateNode,
|
||
|
'elif' : ElifTemplateNode,
|
||
|
'else' : ElseTemplateNode,
|
||
|
'def' : FunctionTemplateNode,
|
||
|
'call' : CallTemplateNode,
|
||
|
'exec' : ExecTemplateNode }
|
||
|
template_factory_types = template_factory_type_map.keys()
|
||
|
|
||
|
def TemplateNodeFactory(parent):
|
||
|
src = parent.parser_get()
|
||
|
|
||
|
if src == None:
|
||
|
return None
|
||
|
match = re_directive.search(src)
|
||
|
if match == None:
|
||
|
parent.parser_eat(len(src))
|
||
|
return LeafTemplateNode(parent, src)
|
||
|
elif src == '' or match.start() != 0:
|
||
|
parent.parser_eat(match.start())
|
||
|
return LeafTemplateNode(parent, src[:match.start()])
|
||
|
else:
|
||
|
directive = match.group()[2:-2].strip()
|
||
|
parent.parser_eat(match.end())
|
||
|
if directive == 'end':
|
||
|
return 'end'
|
||
|
elif re_comment.match(directive):
|
||
|
return CommentTemplateNode(parent, directive)
|
||
|
else:
|
||
|
for i in template_factory_types:
|
||
|
if directive[0:len(i)] == i:
|
||
|
return template_factory_type_map[i](parent, directive)
|
||
|
return ExpressionTemplateNode(parent, directive)
|
||
|
|
||
|
def is_sequence(object):
|
||
|
try:
|
||
|
test = object[0:0]
|
||
|
except:
|
||
|
return False
|
||
|
else:
|
||
|
return True
|