Template Engine¶
Contents
This is the template that comes with Glass.
Example¶
>>> from glass.template import Template
>>> t = Template('hello {{user}}')
>>> t.render({'user':'horlarwumhe'})
'hello horlarwumhe'
>>> t = Template('hello {{user.upper}}')
>>> t.render({'user':'horlarwumhe'})
'hello HORLARWUMHE'
That is simple introduction on how to use the template.
To use the full capabilites of the template, use Environment() .
Using Environment allows template to be configured.
Such as;
creating custom template loader
creating a filter
creating a tag
caching the template
>>> from glass.template import Environment
>>> env = Environment()
>>> # the env only needs to be created once
>>> s = """
... {% for user in users %}
... {{user.name}} {{user.email}}
... {% endfor %}
...
...
... """
>>> users = [
... {'name':'ola','email':'ola@mail.com'},
... {'name':'wumhe','email':'wumhe@mail.com'}
... ]
>>> t = env.from_string(s)
>>> out = t.render({'users':users})
>>> print(out)
ola ola@mail.com
wumhe wumhe@mail.com
Filters¶
Filters are python function that modify value(s) of variables in the template. Values after | are filters. They take the variable as argument.
see create custom filter to create new template filter.
def lower(name):
return name.lower()
{{user.name |lower}}
using multiple filters;
{{user.name | upper | lower | escape}}
Passing arguments to filters
{{var|func(arg1 arg2 arg3)}}
- Note:
No
,between the function arguments
def func(var,arg1,arg2,arg3):
return var+arg1+arg2+arg3
{{text|split("\n") }}
def split(text,s):
return "".join(text.split(s))
def format_price(price,currency):
return "%s%s"%(currency,price)
{{price|fromat_price("$")}}
{% filter upper %}
everyting here with be in upper case
including {{user}}
{% end %}
but not this, it is outside the filter node
Using Python dict or list in the template.
{{ dict.key }} # dict[key]
{{list.0}} list[0]
{{ list.2.upper }} # list[2].upper()
using block filter tag;
>>> from glass.template import Environment
>>> e = Environment()
>>> s = '''{% filter upper %}
... everyting here with be in upper case
... including {{user}}
... {% end %}
... but not this, it is outside the filter node
... '''
>>> t = e.from_string(s)
>>> out = t.render({"user":'Horlar'})
>>> print(out)
EVERYTING HERE WITH BE IN UPPER CASE
INCLUDING HORLAR
but not this, it is outside the filter node
Loading template from file¶
from glass.template import Environment
env = Environment()
out = env.render_template('index.htm',{'user':'username'})
The default template loader, class FileLoader, will look for the templates in the current working directory and folder templates in the current working directory.
You can set different directory to find templates.
from glass.template import FileLoader
from glass.template import Environment
env = Environment(loader=FileLoader('/path/to/templates'))
# using multiple directories
env = Environment(loader=FileLoader(['/path/to/templates','/path/to/other/template']))
template = env.get_template('index.html')
template.render({})
You can create custom template loader.
The loader class must define two methods, load_template and check_if_modified.
load_template returns the template source to render while check_if_modified returns True if the template has been modified, False if not
>>> class MyLoader:
... def load_template(self,name):
... return templates[name]
... def check_if_modified(self,name):
... # check if the template has been modified or not
...
... return True
...
>>> templates = {'index.html':'this is index','faq.html':'this is faq'}
>>> env = Environment(loader=MyLoader())
>>> env.render_template('index.html',{})
'this is index'
>>> env.render_template('faq.html',{})
'this is faq'
>>> t = env.get_template('index.html')
>>> t.render({})
'this is index'
>>>
Template Caching¶
When rendering a template involve 3 major things.
- tokenizing
lexer(text).tokenize()which returns all tokens in the templateparsing
parser(tokens).parse()parses the tokens and return nodes/ast
- rendering
nodes.render()
Rather than repeating the steps all time, the template is compiled once and the result is cached to speed up the rendering process.
The approach use in this template can explained with this code
caches = {}
template = 'index.html'
file = open('index.html')
tokens = lexer(file.read()).tokenize()
file.close()
nodes = Parser.parse(tokens)
# the template is now compiled, then cache the result
caches[template] = nodes
# anytime, the template needs to be rendered
# the loader that loads this template will be called
# to check if the file has been modified,
#eg, the default loader , FileLoader(), will use os.stat(template).st_mtime
# to check last time it was modified,
# if the loader return False , indicating the file is the same,
#then the cache is check,
nodes = caches.get(template)
if nodes:
# render , without parsing again
return nodes.render({})
else:
#
# parse the template
# and cache the result
create cache class;
class MyCache(dict):
# define set() and get()
def set(self,template,nodes):
self[template] = nodes
def get(self,template):
return super().get(template)
pass the cache as argument to Environment instance.
>>> env = Environment(cache=MyCache())
>>> env.render_template('index.html',{})
Using extends tag¶
<!-- base.html -->
<title> {% block title %}{% endblock %}</title>
{% block css %}
<style> // css code here </style>
{% endblock %}
<body>
{%block content %}
<div> from base.html</div>
{% endblock %}
</body>
base.html file can be extended by other templates and override any block tags.
<!-- file index.html -->
{% extends 'base.html' %}
{% block title %} page title {% endblock %}
{% block content %}
<div>this is content from index.html</div>
{% endblock %}
The index.html will override block title and block content but not block css.
<title> page title </title>
<style> // css code here </style>
<body>
<div>this is content from index.html</div>
</body>
However, if the block tag in the base.html needs to be rendered, you can use super directive. For example, you have javascript code in base.html which is required by index.html.
<!-- base.html -->
<title> {% block title %} {% endblock %} </title>
<body>
{% block content %}
<div> from base.html</div>
{% endblock %}
</body>
{% block js %}
<script src='js/navbar.js'>
//javascript from base.html
</script>
{% endblock %}
If you want to include src='js/navbar.js' in the child template (index.html), use super.
{% extends 'base.html' %}
{% block title %} page title {% endblock %}
{% block content %}
<div> this is content from index.html<div>
{% endblock %}
{% block js super %}
<script src='js/form.js'>
//javascript from index.html
</script>
{% endblock %}
The above example will render block js in both base.html and index.html
<title> page title </title>
<body>
<div> this is content from index.html<div>
</body>
<script src='js/navbar.js'>
//javascript from base.html
</script>
<script src='js/form.js'>
//javascript from index.html
</script>
The above example rendered block js in parent template(base.html) before child template (index.html). If you want to render child template before the parent template, put super at the endblock tag.
{% extends 'base.html' %}
{% block title %} page title {% endblock %}
{% block content %}
<div> this is content from index.html<div>
{% endblock %}
{% block js %}
<script src='js/form.js'>
//javascript from index.html
</script>
{% endblock super %}
Output.
<title> page title </title>
<body>
<div> this is content from index.html</div>
</body>
<script src='js/form.js'>
//javascript from index.html
</script>
<script src='js/navbar.js'>
//javascript from base.html
</script>
Custom Template Filter¶
You can write filter(s) to use in the template(s).
def secret(value):
return value[:5]+'********'
def lower(value):
return value.lower()
def split_words(text,count=3):
return " ".join(text.split()[:count])+'....'
s = '''{% filter secret %}{{email}}{%end%}'''
filters = {'secret':secret,'lower':lower,'split_words':split_words }
env = Environment(filters=filters)
t = env.from_string(s)
out = t.render({'email':'usermail@gmail.com'})
print(out)
# userm********
out = env.from_string("{{email|secret}}")
print(out.render({'email':'usermail@gmail.com'}))
#userm********
t = env.from_string("{{post | split_words}}")
print(t.render({'post':'this is a long text and another text'}))
# this is a....
t = env.from_string("{{post | split_words(5)}}")
print(t.render({'post':'this is a long text and another text'}))
# this is a long text....
or using decorator;
@env.filter('upper')
def func(value):
return value.upper()
@env.filter("split_words")
def split_words(text,count=3):
return " ".join(text.split()[:count])+'....'
Custom Template Tag¶
It is possible to create a tag to add to the bultin tags.
Creating a tag requires creating a function to call when the tag is found. The function takes one argument
glass.template.parser.Parser. The function should return Node object.
The tag can be registered with the code example.
env = Environment()
@env.tag('tagname')
def tag_parser(parser):
# parse the tag here
# or manually register the tag
def tag_parser(parser):
pass
env = Environment(tags={'tagname':tag_parser})
lets create a simple tag that shows current time.
{% time as now %}
{{now}}
Using the Parser() class.
parser.get_next_token() returns next token and remove
the token from token list, while parser.next_token() returns next token without removing it.
>>> from glass.template.main import Parser, Lexer
>>> source = '''
... {% if user.name %}
... Hello {{user.name.title }}
... {% else %}
... Hello Guest
... {% endfor %}
... '''
>>> tokens = Lexer(source).tokenize()
>>> parser = Parser(tokens)
>>> parser.tokens
... [<Token BLOCK {% endfor %}, <Token TEXT Hello Guest,
... <Token BLOCK {% else %}, <Token VAR {{ user.name.title }},
... <Token TEXT Hello, <Token BLOCK {% if user.name %}
... ]
>>> parser.next_token()
<Token BLOCK {% if user.name %}
>>> parser.tokens
... [<Token BLOCK {% endfor %}, <Token TEXT Hello Guest,
... <Token BLOCK {% else %}, <Token VAR {{ user.name.title }},
... <Token TEXT Hello, <Token BLOCK {% if user.name %}
... ]
>>> parser.get_next_token()
<Token BLOCK {% if user.name %}
>>> parser.tokens
... [<Token BLOCK {% endfor %}, <Token TEXT Hello Guest,
... <Token BLOCK {% else %}, <Token VAR {{ user.name.title }},
... <Token TEXT Hello
... ]
>>> parser.get_next_token()
<Token TEXT Hello
>>> parser.tokens
... [<Token BLOCK {% endfor %}, <Token TEXT Hello Guest,
... <Token BLOCK {% else %}, <Token VAR {{ user.name.title }}
...
... ]
>>> parser.get_next_token()
<Token VAR {{ user.name.title }}
>>> parser.tokens
... [<Token BLOCK {% endfor %}, <Token TEXT Hello Guest,
... <Token BLOCK {% else %}
...
... ]
>>> parser.next_token()
<Token BLOCK {% else %}
>>> parser.tokens
... [<Token BLOCK {% endfor %}, <Token TEXT Hello Guest,
... <Token BLOCK {% else %}
...
... ]
>>> parser.get_next_token()
<Token BLOCK {% else %}
>>> parser.tokens
[<Token BLOCK {% endfor %}, <Token TEXT Hello Guest]
>>> parser.next_token()
<Token TEXT Hello Guest
>>> parser.tokens
[<Token BLOCK {% endfor %}, <Token TEXT Hello Guest]
>>> parser.get_next_token()
<Token TEXT Hello Guest
>>> parser.tokens
[<Token BLOCK {% endfor %}]
>>> parser.next_token()
<Token BLOCK {% endfor %}
>>> parser.tokens
[<Token BLOCK {% endfor %}]
>>> parser.get_next_token()
<Token BLOCK {% endfor %}
>>> parser.tokens
[]
>>>
>>> source = '''
... {% if name == 'Firstname Lastname' %}
... Hello
... {% endif %}
...
... '''
>>> parser = Parser(Lexer(source).tokenize())
>>> token = parser.get_next_token()
>>> token
<Token BLOCK {% if name == 'Firstname Lastname' %}
>>> cmd,args = token.clean_tag()
>>> cmd
'if'
>>> args
"name == 'Firstname Lastname'"
>>> args.split()
['name', '==', "'Firstname", "Lastname'"]
>>> token.split_args()
['name', '==', "'Firstname Lastname'"]
>>> # use token.split_args() to split content
...
>>>
# create function to parse the tag
def time_parser(parser):
cmd,args = parser.get_next_token().clean_tag()
# or
# token = parser.get_next_token()
# cmd ,args = token.clean_tag()
### print(cmd,args)
### 'time', 'as now'
###
_, var = args.split()
return TimeNode(var)
# create the tag Node
import datetime as dt
class TimeNode:
def __init__(self,var):
self.var = var
def render(self,context,env=None):
context[self.var] = str(dt.datetime.now())
return ''
Register the tag function.
>>> env = Environment(tags={'time':time_parser})
>>> out = env.from_string('''
... {% time as now %}
... date is {{now}}
...
''')
>>> print(out.render({})
)
date is 2021-04-20 09:26:42.902343
create another tag match tag
{% for user in users %}
{% match user.status %}
{% case 'verified' %}
<b>{{user.name}}</b> is verified
{% case 'pending' %}
<b>{{user.name}}</b> is pending
{% case 'suspend' %}
<b>{{user.name}}</b> is suspended
{% default %}
<b>{{user.name}}</b> status is unknown
{% end %}
{% endfor %}
from glass.template import Environment
from glass.template.nodes import VarNode
env = Environment()
@env.tag('match')
def match_parse(parser):
match_,test = parser.get_next_token().clean_tag()
test = VarNode.parse(test)# return VarNode object
# skip all token till {% case %}, {% default %} or
# {% end %} is reached
parser.skip_untill(('end','case','default'))
cases = case_parse(parser)
default = parse_default(parser)
end,_ = parser.next_token().clean_tag()
if end != 'end':
raise ValueError('match tag expect end')
#skip {% end %} tag
parser.skip_token(1)
return MatchNode(test,cases,default)
def case_parse(parser):
# case tag is not registered, since
# {% case %} cant stand alone, it must be
# part of {% match %} tag
cases = []
cmd,_ = parser.next_token().clean_tag()
while cmd == 'case':
cmd,value = parser.get_next_token().clean_tag()
# or
# token = parser.get_next_token()
# cmd,test = token.clean_tag()
body = parser.parse(stop_at=('end','case','default'))
value = VarNode.parse(value)# return VarNode object
node = CaseNode(value,body)
cmd,_ = parser.next_token().clean_tag()
cases.append(node)
return cases
def parse_default(parser):
cmd,_ = parser.next_token().clean_tag()
if cmd == 'default':
_= parser.get_next_token()
body = parser.parse(stop_at=('end',))
return DefaultNode(body)
create Node object
from glass.template.nodes import Node
class MatchNode(Node):
#{% match test %}
def __init__(self,test,cases,default):
self.test = test
self.cases = cases
self.default = default
def render(self,context,env=None):
test_value = self.test.eval(context,env)
for case in self.cases:
case_value = case.value.eval(context,env)
if test_value == case_value:
return case.render(context,env)
if self.default is not None:
return self.default.render(context,env)
return ''
class CaseNode(Node):
# {% case value %}
def __init__(self,value,body):
self.value = value
self.body = body
def render(self,context,env=None):
return self.body.render(context,env)
class DefaultNode(Node):
# {% default %}
def __init__(self,body):
self.body = body
def render(self,context,env=None):
return self.body.render(context,env)
use the tag;
>>> source = '''
... {% for user in users %}
... {% match user.status %}
... {% case 'verified' %}
... <b>{{user.name}}</b> is verified
... {% case 'pending' %}
... <b>{{user.name}}</b> is pending
... {% case 'suspend' %}
... <b>{{user.name}}</b> is suspended
... {% default %}
... <b>{{user.name}}</b> status is unknown
... {% end %}
... {% endfor %}
... '''
>>> ctx = {'users':[
{'name':'Horlarwumhe','status':'suspend'},
{'name':'Horlar','status':'pending'},
{'name':'Olawumi','status':'verified'},
{'name':'Hor','status':''},
]
}
>>> t = env.from_string(source)
>>> print(t.render(context))
<b>Horlarwumhe</b> is suspended
<b>Horlar</b> is pending
<b>Olawumi</b> is verified
<b>Hor</b> status is unknown
See the Environment API here Environment.
Using With Glass¶
To use the Environment class with Glass, use app.template_env.
see Glass doc on how to use the template engine with Glass app.