Tipyte (pronounced "tippity") is a BSD-licensed Python templating engine that supports Python versions 2.7 and 3.0+. The syntax used for Tipyte is almost pure Python with a small amount of syntactic sugar to keep the templates from being whitespace sensitive and three template-specific built-in functions.
Although Tipyte templates look suspiciously like Jinja templates, the most significant difference is that Tipyte leans almost entirely on Python for its syntax. Tipyte has no built-in operator overloading which means developers can use normal function calls instead of pipes ("|"), and the vast majority of Python syntax works within Tipyte out of the box which gives it a softer learning curve.
There are four types of templating tags in Tipyte: escaped expression tags, unescaped expression tags, statement tags and comments. The following sample template makes use of all four tags which are described in further detail below:
1 {# Author: Eric Pruitt #}
2 {% is_day_time = 6 <= hour_of_day <= 18 %}
3 <!doctype html>
4 <html lang="en">
5 <html>
6 <head>
6 <meta charset="utf-8">
7 <title>{{ page_title }}</title>
9 <link rel="stylesheet" type="text/css" href="default.css">
10 {% if not is_day_time %}
11 <link rel="stylesheet" type="text/css" href="night-theme.css">
12 {% endif %}
13 {% for name, content in meta_tags.items() %}
14 <meta name="{{ name }}" content="{{ content }}">
15 {% endfor %}
16 </head>
17 <body>
18 <h1>Hello!</h1>
19 <div id="content">
20 Welcome to my website.
21 </div>
22 {= advertisement_footer =}
23 </body>
24 </html>
Comments are denoted by blocks of {# ... #}
. The contents of comment tags are
ignored.
1 {# Author: Eric Pruitt #}
...
Escaped expression tags are denoted by blocks of {{ .... }}
. The result of
the expression within the tag will be rendered and then passed to the
template's escape function. The default escape method for all templates
produces HTML-safe output.
...
7 <meta charset="utf-8">
8 <title>{{ page_title }}</title>
9 <link rel="stylesheet" type="text/css" href="default.css">
...
13 {% for name, content in meta_tags.items() %}
14 <meta name="{{ name }}" content="{{ content }}">
15 {% endfor %}
Using blocks of {= .... =}
will add the result of the expression to the
template verbatim without escaping.
...
21 </div>
22 {= advertisement_footer =}
23 </body>
24 </html>
The contents of expression tags must be an expression; using statements ("import", "with", "while", etc.) will produce a syntax error.
Statement tags are denoted by blocks of {% ... %}
. Anything that Python does
not evaluate as an expression must be enclosed in these tags. Unlike vanilla
Python, Tipyte is not whitespace sensitive. Statement blocks that would
normally require a new level of indentation are instead terminated with
"end..." blocks. For example, {% with ... %}
must be paired with an
{% endwith %}
, {% if ... %}
paired with {% endif %}
, etc. Trailing colons
(":") after "for", "while", "if", etc. are optional in Tipyte.
...
2 {% is_day_time = 6 <= hour_of_day <= 18 %}
...
9 <link rel="stylesheet" type="text/css" href="default.css">
10 {% if not is_day_time %}
11 <link rel="stylesheet" type="text/css" href="night-theme.css">
12 {% endif %}
13 {% for name, content in meta_tags.items() %}
14 <meta name="{{ name }}" content="{{ content }}">
15 {% endfor %}
16 </head>
...
All whitespace outside of tags is preserved by default, but this can be changed by adding "-" immediately after any opening tag or immediately before any closing tag. Assume the following block of code is the entirety of a template:
<h1> {{ "Hello" }} {{ "world!" }} </h1>
The output would be:
<h1> Hello world! </h1>
By adding "-" in a couple of places, ...
<h1> {{- "Hello" }} {{ "world!" -}} </h1>
...the output now becomes:
<h1>Hello world!</h1>
Note that when adding "-" to tags, adjacent newlines will also be removed.
The default scope of templates includes three built-in functions. They are
include
, raw_include
and defined
.
Incorporate path
into template output. If raw
is False
, the file will be
parsed as a template and executed, but if raw
is True
, the contents of the
file will be incorporated into the output verbatim. Normally, the escape
function of the calling template will be used to escape the included template,
but this can be overridden by setting escaper
. If path
is a relative path,
it will be interpreted as being relative to the directory of the calling
template. Note that this function does not return the included data.
Helper function to call include
with raw=True
; the following two
expressions are equivalent:
{% raw_include("file.txt") %}
{% include("file.txt", raw=True) %}
Return boolean value indicating whether or not a variable is defined. The
name
is given as a string.
-
... using a characters other than "{" and "}" for blocks? This isn't supported out-of-the box right now, but only a handful of lines need to be modified to use a different symobl; search for
OPEN_TAGS
andCLOSE_TAGS
in the source code. -
... using literal strings that are the same as the block tags? To insert a literal string that's the same one of the block tags, use
{= ... =}
with a string e.g.{= "{=" =}
.
The two most important functions exposed by Tipyte are template_to_function
and template_traceback
.
Convert template into a callable function. By default, the template output will
be made HTML-safe, but the content escape method can be changed by setting the
escaper
argument.
The resulting function action can be called using two different conventions to pass state into the template. One way to pass state into the function is to provide variable names and values as keyword arguments to the function:
render_inbox = template_to_function("inbox.html")
html = render_inbox(title="Inbox", email="[email protected]")
Once execution is finished, any state defined only within the template is lost. Alternatively, the template function can be called with a dictionary as an argument:
render_inbox = template_to_function("inbox.html")
variables = {
"title": "Inbox",
"email": "[email protected]",
}
html = render_inbox(variables)
Within the template, all members of the dictionary will be accessible as
variables. When template execution is finished, all of the template's state
will be preserved in the dictionary; any modified or newly defined variables
will be reflected in dictionary. Continuing from the example above, if the
template contained a statement like {% name = ... %}
, the dictionary would
contain a new key after the template was rendered:
>>> variables
{'name': 'Jess Doe', 'email': '[email protected]', 'title': 'Home Page'}
To avoid conflicting with definitions used internally by the template system,
no user-defined variable names may start with _template_
. If an error is
raised during template execution, the dictionary may contain internal variables
starting with this prefix.
An exception raised inside of a template will produce a traceback that can be hard to follow. When this function is called within an exception-handling block, it returns a sanitized traceback in the form of a string that correctly maps locations in the stack to the corresponding locations in the templates.
This is what a standard stack trace looks like when an exception is raised within a template:
Traceback (most recent call last):
File "app.py", line 412, in <module>
main()
File "app.py", line 253, in main
print(admin_page())
File ".../tipyte:.py", line 376, in function
exec(compiled_template, dict(), symbols)
File "/._/python-templates/.../admin.html", line 3, in <module>
<div class="container-fluid">
File ".../tipyte.py", line 345, in include
template_to_function(path, escaper=escaper)(symbols)
File ".../tipyte.py", line 376, in function
exec(compiled_template, dict(), symbols)
File "/._/python-templates/.../user-list.html", line 3, in <module>
<ul>
NameError: name 'query' is not defined
This is what the sanitized stack trace returned by this function looks like:
Traceback (most recent call last):
File "app.py", line 412, in <module>
main()
File "app.py", line 253, in main
print(admin_page())
File ".../admin.html", line 5, in <module>
{% include("user-list.html") %}
File ".../user-list.html", line 4, in <module>
{% for username, country, status in query("SELECT * FROM Users") %}
NameError: name 'query' is not defined
Do not use this function when handling a SyntaxError
. Any syntax errors
generated from within a template will already have been modified to include all
the information needed to easily determine where the syntax error is. When the
templates_only
option is set, the only files that will be shown in the
traceback are templates; lines from pure-Python files will be elided. If
templates_only
were set, the following traceback would be returned lieu of
the one above:
Traceback (most recent call last):
File ".../admin.html", line 5, in <module>
{% include("user-list.html") %}
File ".../user-list.html", line 4, in <module>
{% for username, country, status in query("SELECT * FROM Users") %}
NameError: name 'query' is not defined
Example usage:
render_home_page = template_to_function("home.html")
try:
render_home_page(date="January 10th, 2016")
except Exception as error:
if not isinstance(error, SyntaxError):
error.template_traceback = template_traceback()
raise