Source code for httk.httkweb.templateengine_httk
#
# The high-throughput toolkit (httk)
# Copyright (C) 2012-2018 Rickard Armiento
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Closely inspired from
# https://makina-corpus.com/blog/metier/2016/the-worlds-simplest-python-template-engine
# https://github.com/ebrehault/superformatter
from __future__ import print_function
import string, os, sys, codecs
import inspect
import ast
# Retain python2 compatibility without a dependency on httk.core
if sys.version[0] == "2":
# Note:
# The "html" module is not a builtin in Python 2.
# If it happens to be installed, we still do not
# want to use it since it is old (last updated in 2011,
# version 1.16). Use the builtin cgi module to get the
# escape funtion instead.
from cgi import escape
unicode_type=unicode # noqa: F821
else:
from html import escape
unicode_type=str
from httk.httkweb.helpers import UnquotedStr
[docs]
class HttkTemplateFormatter(string.Formatter):
[docs]
def format_field(self, value, spec, quote=None, args = None, kwargs = None):
if spec == 'unquoted' or spec.startswith('unquoted:'):
output = self.format_field(value, spec[len('unquoted::'):],quote=False, args=args, kwargs=kwargs)
return output
elif spec == 'quote' or spec.startswith('quote:'):
output = self.format_field(value, spec[len('quote::'):],quote=True, args=args, kwargs=kwargs)
return output
elif spec.startswith('repeat:'):
template = spec.partition('::')[-1]
new_kwargs = dict(kwargs) if kwargs is not None else {}
if 'item' in new_kwargs:
new_kwargs['items'] = [new_kwargs['item']] + new_kwargs['items'] if 'items' in new_kwargs else []
if 'index' in new_kwargs:
new_kwargs['indices'] = [new_kwargs['index']] + new_kwargs['indices'] if 'indices' in new_kwargs else []
if 'index1' in new_kwargs: ## TODO: Came from elastic_contants, what is index1?
new_kwargs['indices'] = [new_kwargs['index1']] + new_kwargs['indices'] if 'indices' in new_kwargs else []
def update_and_return(update):
new_kwargs.update(update)
return new_kwargs
if value is None:
raise Exception("HttkTemplateFormatter: asked to loop over None for spec: "+str(spec))
elif type(value) is dict:
return ''.join([self.format(template,**(update_and_return({'item':value[i], 'index':i, 'index1':i+1}))) for i in value])
else:
return ''.join([self.format(template,**(update_and_return({'item':value[i], 'index':i, 'index1':i+1}))) for i in range(len(value))])
elif spec == 'call' or spec.startswith('call:'):
callargs, _sep, newspec = spec.partition("::")
callargs = callargs.split(":")
callargs = [self.get_field(x[1:-1],quote=quote,args=args,kwargs=kwargs)[0] if (x.startswith('{') and x.endswith('}')) else x for x in callargs]
# If the variable type is int/float, callargs most likely cannot
# be str. Convert callargs to int/float.
if hasattr(value,"__repr__") and \
("of float object" in value.__repr__() or
"of int object" in value.__repr__()):
for i in range(1,len(callargs)):
try:
if callargs[i] == 'nan':
callargs[i] = float('nan')
else:
callargs[i] = ast.literal_eval(callargs[i])
except ValueError:
pass
try:
result = value(*callargs[1:])
except Exception as e:
if '::' in spec:
raise Exception("Templateengine_httk: tried to render: '"+str(spec)+"' as call to function "+str(value)+" triggered exception: "+str(e)+ ". Note: string to render contains '::', which could have been caused by an ':item:' with an item equal the empty string")
else:
raise Exception("Templateengine_httk: tried to render: '"+str(spec)+"' as call to function "+str(value)+" triggered exception: "+str(e))
return self.format_field(result, newspec, quote=quote, args=args, kwargs=kwargs)
elif spec.startswith('getitem:') or spec.startswith('getattr:'):
x, _dummy, newspec = spec.partition(':')[2].partition('::')
# Check if there is a :call attached to the value that we are getting with
# getitem.
call_func = None
if ":call" in x:
call_func = x.partition(".")[-1].split(":")[0]
x = x.partition(".")[0]
idx = self.get_field(x[1:-1],args=args,kwargs=kwargs)[0] if (x.startswith('{') and x.endswith('}')) else x
try:
val = value[idx] if spec.startswith('getitem:') else getattr(value,idx)
except TypeError:
try:
val = value[int(idx)] if spec.startswith('getitem:') else ''
except TypeError:
return ''
# TODO: Check if the unicode conversion really is necessary here,
# or if we can make sure val is always unicode.
if newspec == '' and call_func is None:
return unicode_type(val)
else:
# Check if there is a call we want to do with the value that we got with getitem:
if call_func is not None:
val = getattr(val, call_func)
newspec = "call" + spec.partition(":call")[-1]
return self.format_field(val, newspec, quote=quote, args=args, kwargs=kwargs)
elif spec.startswith('if:') or spec.startswith('if-not:') or \
spec.startswith('if-set:') or spec.startswith('if-unset:'):
outcome = (spec.startswith('if:') and value) or \
(spec.startswith('if-not:') and not value) or \
(spec.startswith('if-set:') and value is not None) or \
(spec.startswith('if-unset:') and value is None)
# Implement optional "else" functionality:
if "::else::" in spec:
if not outcome:
template = spec.partition('::else::')[-1]
else:
template = spec.partition('::else::')[0].partition('::')[-1]
else:
if not outcome:
return ''
template = spec.partition('::')[-1]
return self.format(template, **kwargs)
elif value==None:
return ""
else:
output = super(HttkTemplateFormatter, self).format_field(value, spec)
if quote is None:
try:
if isinstance(value,UnquotedStr):
quote = False
else:
quote = True
except TypeError:
quote = True
if quote and (not hasattr(self,'quote') or self.quote == True):
output = escape(output,quote=True)
#output = output.replace(":", ":")
output = output.replace("'", "'")
#if type(value) != unicode and type(value) != str:
# return output + "("+str(type(value))+":"+str(quote)+":"+str(e)+")"
#else:
return output
[docs]
def get_field(self, field_name, args, kwargs):
# Handle a key not found
# In Python 3 the 'menuitems' field_name should be 'menuitems-list'?
# if six.PY3:
# if field_name == "menuitems":
# field_name = "menuitems-list"
try:
val = super(HttkTemplateFormatter, self).get_field(field_name, args, kwargs)
# if six.PY3:
# if val[0] == "i":
# val = ("index", field_name)
# Python 3, 'super().get_field(field_name, args, kwargs)' works
except (KeyError, AttributeError) as e:
#TODO: Allow a debug config that inserts "["+str(e)+"]" as the value
val = None, field_name
return val
[docs]
def vformat(self, format_string, args, kwargs, used_args=None, recursion_depth=None):
if used_args is None:
used_args = set()
result = self.vformat(format_string, args, kwargs, used_args, 10)
self.check_unused_args(used_args, args, kwargs)
return result
else:
if recursion_depth < 0:
raise ValueError('Max string recursion exceeded')
result = []
for literal_text, field_name, format_spec, conversion in \
self.parse(format_string):
# output the literal text
if literal_text:
result.append(literal_text)
# if there's a field, output it
if field_name is not None:
# this is some markup, find the object and do
# the formatting
# given the field_name, find the object it references
# and the argument it came from
obj, arg_used = self.get_field(field_name, args, kwargs)
used_args.add(arg_used)
# do any conversion on the resulting object
obj = self.convert_field(obj, conversion)
# expand the format spec, if needed
format_spec = self.vformat(format_spec, args, kwargs, used_args, recursion_depth-1)
# format the object and append to the result
result.append(self.format_field(obj, format_spec, args=args, kwargs=kwargs))
return ''.join(result)
[docs]
class TemplateEngineHttk(object):
def __init__(self, template_dir, template_filename, base_template_filename = None):
self.template_dir = template_dir
self.template_filename = template_filename
self.filename = os.path.join(template_dir, template_filename)
self.dependency_filenames = [self.filename]
if base_template_filename is not None:
self.base_filename = os.path.join(self.template_dir, base_template_filename)
self.dependency_filenames += [self.base_filename]
else:
self.base_filename = None
self.httk_tf = HttkTemplateFormatter()
[docs]
def apply(self, content = None, data = None, *subcontent):
if data == None:
data = {}
else:
data = dict(data)
self.httk_tf.data = data
with codecs.open(self.filename,encoding='utf-8') as f:
template = f.read()
data['content'] = content
data['subcontent'] = subcontent
output = self.httk_tf.format(template, **data)
if self.base_filename is not None:
with codecs.open(self.base_filename, encoding='utf-8') as f:
base_template = f.read()
data['content'] = UnquotedStr(output)
del data['subcontent']
output = self.httk_tf.format(base_template, **data)
return output
# data = dict(self.global_data)
# data['content'] = page._rendered_content
# if hasattr(page,_rendered_subcontent) and page._rendered_subcontent is not None:
# data['subcontent'] = page._rendered_subcontent
# content = self.httk_tf.format(template,data)
# data['content'] = content
# del data['subcontent']
# return self.httk_tf.format(base_template,data)
if __name__ == "__main__":
tf = HttkTemplateFormatter()
# Turn off auto-quoting
tf.quote = False
class ExampleStrRepr(object):
def __str__(self):
return 'str'
def __repr__(self):
return 'repr'
class ExampleAttribute(object):
x = 'tree'
y = [{'i':'one', 'j':'two'},'z']
class ExampleFormatter(object):
def __format__(self, f):
if (f == 'test'):
return "This is an ExampleFormatter test"
return 'I am the ExampleFormatter'
# Many examples adapted from https://pyformat.info/
print()
print("== Simple strings")
print(tf.format("Strings: '{a}' '{b}'",a="one", b="two"))
print(tf.format("Align: '{a:>10}' '{a:10}' '{a:_<10}' '{a:^10}'",a='test'))
print(tf.format("Truncate: '{a:.10}'",a='this string is too long'))
print(tf.format("Truncate and pad: '{a:20.10}'",a='this string is too long'))
print(tf.format("Str or repr: '{0!s}' '{0!r}' '{0}'",ExampleStrRepr()))
from datetime import datetime
print(tf.format("Std class support: '{a:%Y-%m-%d %H:%M}'",a=datetime(2010,1,2,3,4)))
print(tf.format("Own Class support: '{a:test}' '{a}'",a=ExampleFormatter()))
print()
print("== Simple numbers")
print(tf.format("Integers: '{a}' '{a:d}'",a=42))
print(tf.format("Floats: '{a:f}'",a=3.141592653589793))
print(tf.format("Number padding:'{a:06.2f}'",a=3.141592653589793))
print(tf.format("Sign: '{a:+d}' '{b:+d}' '{a:=+5d}'",a=42,b=-42))
print()
print("== Element access")
print(tf.format("Dicts: '{a[one]}' '{a[two]}'",a={'one':'1', 'two':'2'}))
print(tf.format("Lists: '{a[1]}' '{a[2]}'",a=[1,2,3,4,5]))
print(tf.format("Attributes: '{a.x}'",a=ExampleAttribute))
print(tf.format("Combitations: '{a.y[0][i]}'",a=ExampleAttribute))
print()
print("== Parameterization")
print(tf.format("Alignment and width: '{a:{align}{width}}'".format(a='test', align='^', width='10')))
print(tf.format("Precision: '{a:.{prec}} = {b:.{prec}f}'".format(a='Test', b=2.7182, prec=3)))
print(tf.format("Embedded: '{a:{prec}} = {b:{prec}}'".format(a='Long text here', b=2.7182, prec='.3')))
print(tf.format("Width and precision: '{:{width}.{prec}f}'".format(2.7182, width=5, prec=2)))
print(tf.format("Embedded class: '{a:{dfmt} {tfmt}}'".format(a=datetime(2010,1,2,3,4), dfmt='%Y-%m-%d', tfmt='%H:%M')))
print()
print("********** Functionality from superformatter (with a slight format change for repeat) **************")
print()
print(tf.format("Loops: '{chapters:repeat::Chapter {{item}}, }'", chapters=["I", "II", "III", "IV"]))
print(tf.format("Calls: '{name.upper:call}'", name="eric"))
print(tf.format("Tests: '{t:if::hello}' '{f:if::hello}' '{name:if::hello}' '{empty:if::hello}'", name="eric", empty="", t=True, f=False))
print(tf.format("Test and nesting: 'Action: Back / Logout {manager:if::/ Delete {id}}'", manager=True, id=34))
print()
print("********** New functionality here **************")
# Hint: escaped {{ }} are needed to call varibale contents inside nested formatting, which always occur after ::,
# (If used without nesting, then the content is replaced on the level above, which, e.g., does not contain the item from a loop.)
print()
print("== Loops")
print(tf.format("Indexed loops: '{chapters:repeat::Chapter {{index}}={{item}},}'", chapters=["I", "II", "III", "IV"]))
print(tf.format("Nested loops: '{l:repeat::{{k:repeat::{{{{item}}}}({{item}})}}, }'", l=[1,2,3,4],k=["a","b","c","d"]))
print(tf.format("Nested formatting: '{b:repeat:: {{a:.{{item}}f}},}'", a=3.141592653589793, b=["2","4","8","16"]))
print()
print("== Indirection")
print(tf.format("Item access indirection: '{a:getitem:{b}}' '{c:getitem:{d}}'",a={'x':1, 'y':2}, b='x', c=["10","11","12","13"], d=2))
print(tf.format("Attribute access indirection: '{a:getattr:{b}}'",a=ExampleAttribute, b='x'))
print(tf.format("Attribute access indirection with formatting: '{a:getattr:{b}::>10}'",a=ExampleAttribute, b='x'))
print()
print("== Calls")
print(tf.format("Calls with args: '{name.rstrip:call:,four}'", name="one,two,three,four"))
print(tf.format("Calls with formatting: '{name.rstrip:call:,four::>20}'", name="one,two,three,four"))
print(tf.format("Calls with argument indirection: '{name.rstrip:call:{remove}::>20}'", name="one,two,three,four", remove=",four"))
print()
print("== Conditionals")
print(tf.format("With formatting: '{t:if::hello {{name:>10}}}'", name="eric", empty="", t=True, f=False))
print(tf.format("Negation: '{t:if-not::hello}' '{f:if-not::hello}'", empty="", t=True, f=False))
print(tf.format("Unset: '{empty:if::hello}' '{empty:if-unset::hello}' '{empty:if-set::hello}' '{unset:if-unset::hello}'", empty=""))
print()
print("== Advanced")
print(tf.format("Item access indirection inside loops: '{a:repeat:: {{a:getitem:{{{{index}}}}}}}'",a=[5,6,7,8]))
# print(tf.format("Item access indirection inside loops: '{a:repeat:: {{item.__mul__:call:100}}}'",a=[5,6,7,8]))
print()
print("== Quoting")
tf.quote = True
print(tf.format("Autoquoting: '{a}'",a="<i need to be quoted, \"indeed\" said the cat's hat>"))
print(tf.format("Avoid quoting: '{a:unquoted}'",a="<i need to be quoted, \"indeed\" said the cat's hat>"))
print(tf.format("Quoting + formatting: '{a:unquoted::>70}'",a="<i need to be quoted, \"indeed\" said the cat's hat>"))
print(tf.format("Unquoted string: '{a} '",a=UnquotedStr("<i need to be quoted, \"indeed\" said the cat's hat>")))
print(tf.format("Overriding unquoted string: '{a:quote}'",a=UnquotedStr("<i need to be quoted, \"indeed\" said the cat's hat>")))
print()
print("== Conditionals with \"else\" fallback")
# Changin previous example that used x=2 and then called __gt__ to instead use a float.
# I don't see how that can ever have worked in Python 2, as __gt__ as far as I can see
# was never supported.
print(tf.format("If-else 1.1: '{x.__gt__:call:0::if::positive::else::negative}'", x=2.0))
print(tf.format("If-else 1.2: '{x.__gt__:call:0::if::positive::else::negative}'", x=-2.0))
print(tf.format("If-else 2.1: '{t:if::hello {{name}}::else::hello Unknown}'", t=True, f=False, name="eric"))
print(tf.format("If-else 2.2: '{f:if::hello {{name}}::else::hello Unknown}'", t=True, f=False, name="eric"))
print()
print("== 1-indexed loops")
print(tf.format("1-indexed loops: '{chapters:repeat::Chapter {{index1}}={{item}},}'", chapters=["I", "II", "III", "IV"]))
print()
print("== Conditional + Calls + Formatting")
print(tf.format("x=float: '{x:if-set::{{x.__mul__:call:1000::.1f}}::else::None!}'", x=3.1415926))
print(tf.format("x=None: '{x:if-unset::None!::else::{{x.__mul__:call:1000}}}'", x=None))
print()
print("== Getitem + Calls")
print(tf.format("Getitem + Call: '{a:getitem:{b}.__mul__:call:100}'",a={'x':1, 'y':2}, b='x'))
print(tf.format("Getitem + Call + Formatting: '{a:getitem:{b}.__mul__:call:100::.8f}'",a={'x':1, 'y':2}, b='x'))
print(tf.format("Repeat + Getitem + Call + Formatting: '{a:repeat::{{b:getitem:{{{{index}}}}.__truediv__:call:100}}, }'", a=[1,2,3], b=[100,200,300]))