Templates¶
Use this when your app answers with HTML pages. Korvex renders them with its own template engine. No extra package.
from korvex import App
app = App()
app.static("/static", "static") # the folder static/ holds css/app.css
views = app.templates("templates") # a folder of .html files
def load_post(id: int) -> dict: # your code: read the post from your database
return {"id": id, "title": "Hello", "draft": False, "tags": ["a", "b", "c"]}
@app.get("/posts/{id}")
def show(id: int):
post = load_post(id)
return views.response("post.html", {"post": post})
templates/base.html:
<!doctype html>
<title>{% block title %}Blog{% endblock %}</title>
<link rel="stylesheet" href="{{ static_url('css/app.css') }}">
<main>{% block content %}{% endblock %}</main>
{% include "footer.html" %}
templates/post.html:
{% extends "base.html" %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<h1>{{ post.title|upper }}</h1>
{% if post.draft %}
<p>Draft</p>
{% elif post.tags|length > 2 %}
<p>Popular</p>
{% endif %}
<ul>
{% for tag in post.tags %}
<li>{{ loop.index }}/{{ loop.length }} {{ tag }}</li>
{% else %}
<li>No tags</li>
{% endfor %}
</ul>
{% endblock %}
templates/footer.html is any HTML, like <footer>Acme</footer>.
views.response(name, context) renders the template with a dict of variables
and returns an HTML response. It takes status= and headers= too. Templates are compiled the
first time they are used, then cached.
Syntax¶
{{ expression }}prints a value.{# comment #}prints nothing.{% if %},{% elif %},{% else %},{% endif %}.{% for x in items %}, with an optional{% else %}for an empty list.for k, v in d.items()unpacks. Inside a loop:loop.index(from 1),loop.index0,loop.first,loop.last,loop.length.{% set total = price * 3 %}: visible from there to the end of the template, also after thefororifit was set in.{% extends "base.html" %}(first tag) and{% block name %}...{% endblock %}. In a child template, anything outside the blocks is ignored.{% include "nav.html" %}. The name is a string in quotes.{%- ... -%}and{{- ... -}}remove the whitespace before or after.
An expression can use names, a.b, a[k], a[1:3], strings, numbers,
True, False, None, lists, dicts, comparisons, and, or, not,
in, + - * / // %, x if cond else y, calls, and filters
(value|filter or value|filter(arg)). a.b reads the key b of a dict,
the attribute b of anything else.
Not supported: macros, is tests other than is None, comprehensions,
lambdas. A syntax error names the file and the line.
Filters¶
| Filter | Does |
|---|---|
safe |
Do not escape: the value is trusted HTML. |
escape, e |
Escape the value. |
upper, lower |
Change the case. |
default(x) |
x when the value is None or undefined. |
length |
Number of items or characters. |
join(sep) |
Join a list, escaping each item. |
truncate(n, end="...") |
Cut to n characters at a word boundary. |
tojson |
The value as JSON, safe in a <script> and in a single-quoted attribute (data-x='{{ x|tojson }}'), not a double-quoted one. |
urlencode |
Percent-encode a text, or a dict as a query string. |
Add your own, or a value every template can read:
views.filters["money"] = lambda cents: f"{cents / 100:.2f} EUR"
views.globals["site_name"] = "Acme"
{{ price|money }} and {{ site_name }} now work. url_for and
static_url are already there:
<a href="{{ url_for('show', id=post.id) }}">{{ post.title }}</a>
Escaping¶
- Every printed value is HTML-escaped, unless it is a
Markup(fromkorvex) or goes through|safe.Noneprints as an empty string. - In
{% if %}and{% elif %}, a missing name, key or attribute counts asNone(false), and an ordering comparison such aspost.n > 3is false. Elsewhere it is an error, with the file and line:post.html, line 5: 'post.draft' is undefined, unless you use{{ post.draft|default(False) }}. - To give data to JavaScript, use
tojson:
<script>const items = {{ items|tojson }};</script>
- Escaping does not make a URL safe:
href="javascript:..."stays a script. Only put URLs you trust inhrefandsrc, and use|urlencodefor a value in a query string:<a href="/search?q={{ q|urlencode }}">.
Trust¶
Templates are code, written by you. Names and attributes that start with an underscore are refused, but this is not a sandbox. Do not render templates written by your users.
format and format_map are refused too: they can reach attributes from inside
the string; use a filter instead.
Reload¶
views = app.templates("templates", reload=True)
With reload=True, a template is compiled again when it, or a template it
extends or includes, changes on disk. Use it in development. It is off by
default: nothing checks the files on each render.
Static files with a hash¶
static_url() gives the URL of a file served by app.static(), with a hash of
its content:
<link rel="stylesheet" href="{{ static_url('css/app.css') }}">
<!-- /static/css/app.3f2a1c9b.css -->
- The server answers that URL with
Cache-Control: public, max-age=31536000, immutable, because the URL changes when the file does. Browsers keep it for a year. - The plain URL (
/static/css/app.css) still works, without that header. - With several
app.static()mounts, pick one withstatic_url('logo.svg', name="assets"). - In a handler, call
app.static_url("css/app.css"). It raisesFileNotFoundErrorfor a missing file andLookupErrorfor an unknown mount.
Testing¶
from korvex.testing import TestClient
with TestClient(app) as client:
assert "<h1>HELLO</h1>" in client.get("/posts/1").text
html = views.render("post.html", {"post": load_post(1)}) # the page as a str
Speed¶
4 to 6 times faster than Jinja2 in benches/bench_templates.py. See
Performance.
Next: Sessions and CSRF.