Custom errors in Jinja templates
Posted on Sun 23 August 2015 in Code
Jinja is a pretty great templating engine for Python. Unlike the likes of Mustache or Handlebars, it doesn’t try to constrain overmuch the kind of logic that the programmer can put in their templates. Most Python expressions, for example, are supported, as are variables and functions (or macros in Jinja’s parlance).
One conspicously absent feature is throwing exceptions directly from template code. (It’s obviously possible from custom filters or tags). If we had it, we could add some more robust error checking to the template input values or macro paramaters.
Example
Let’s say you have extracted a {% macro %}
for rendering a button,
like the one from Twitter Bootstrap:
{% macro button(text=none, style='default', type='button') %}
<button class="btn btn-{{ style }}" type="{{ type }}">
{% if text is not none %}
{{ text }}
{% else %}
{{ caller() }}
{% endif %}
</button>
{% endmacro %}
The benefit of this conditional definition is the relative ease of creating buttons with either just a text label:
{{ button("Load More...") }}
or some more complicated markup:
{% call button(style='success', type='submit') %}
<i class="fa fa-check"></i>Finish
{% endcall %}
Supplying both, however, is an ambiguity that the macro currently “resolves” by preferring text
over a {% call %}
block. It should rather be an error instead:
{% if caller and caller is defined and text is not none %}
{% error "Cannot supply text= parameter to button() when invoking it through {% call %}" %}
{% endif %}
But {% error %}
isn’t a tag that Jinja supports by default. To make it available,
we need to implement the logic ourselves.
Adding new Jinja tags
Don’t sweat, though! It won’t be necessary to fork Jinja itself and modify its core code. Custom tags may just as well be added with extensions, which is a dedicated Jinja mechanism.
An extension is just a Python class that declares tags
it intends to support and implements a parse
method:
from jinja2.ext import Extension
class ErrorExtension(Extension):
tags = frozenset(['error'])
def parse(self, parser):
# ...
What can be a little tricky is that parse
method itself shouldn’t take any direct action. Instead, it shall return
a piece of AST — abstract syntax tree — that Jinja will include
in the compiled template. All regular Jinja features are thus available, but the power of extensions doesn’t end here.
Handling the {% error %} tag
The argument to parse
is a parser object, which we can use to extract tokens from the template source code.
The error
tag identifier is one such token, and the string following it is another. As it turns out,
we are only tenuously interested in the former, but we definitely want to pick the latter, because it’s the error message
we’ll raise an exception with:
tag = parser.stream.next()
message = parser.parse_expression()
As it was mentioned previously, the parse
method itself won’t be raising this exception immediately. If we did that,
no template with the {% error %}
tag would ever parse successfully! What we need instead is an AST node
that throws the exception when it’s evaluated. This way, it will be deferred until the template
is being rendered and its execution point reaches the {% error "..." %}
stanza.
Jinja already has a correct node type handy: it’s named CallBlock
,
and it represents a statement that {% call %}
s given method of the Extension
class:
from jinja2.nodes import CallBlock, Const
# (in ErrorExtension.parse)
node = CallBlock(
self.call_method('_exec_error', [message, Const(tag.lineno)]),
[], [], [])
node.set_lineno(tag.lineno)
return node
This method, _exec_error
, should do the crux of what this extension is about: raise an exception.
Let’s define a distinct error class for it, to tell user errors apart from those thrown by Jinja itself:
from jinja2 import TemplateAssertionError
class ErrorExtension(Extension):
# ...
def _exec_error(self, message, lineno, caller):
raise TemplateUserError(message, lineno)
class TemplateUserError(TemplateAssertionError):
pass
Using the extension
The last step is to tell Jinja to use our extension when compiling and rendering templates.
For that, we simply add it to the Environment
:
jinja_env.add_extension(ErrorExtension)
If you are using the Flask framework,
jinja_env
is an attribute on the Flask
application object.
To see the complete code sample for ErrorExtension
,
have a look at this gist.