Assertions in Jinja templates
This post captures one way of implementing assertions in Jinja templates. The assertion mechanism is inspired by this StackOverflow answer: How to raise an exception in a Jinja2 macro? and is based on the Jinja Extensions feature.
The following code becomes possible in Jinja template:
{%- assert variable is string -%}
{%- assert
link.__class__.__name__ == "FileReference",
"Expected FileReference, got: "~link
-%}
Jinja supports a number of checks, all of which are usable with this approach.
The code of the extension is as follows.
from typing import Any, Optional
from jinja2 import (
Environment,
FileSystemLoader,
StrictUndefined,
TemplateRuntimeError,
nodes,
)
from jinja2.ext import Extension
from strictdoc import environment
# The solution was inspired by the StackOverflow question:
# How to raise an exception in a Jinja2 macro?
# https://stackoverflow.com/questions/21778252
class AssertExtension(Extension):
# This is our keyword(s):
tags = {"assert"}
def __init__(self, environment): # pylint: disable=redefined-outer-name
super().__init__(environment)
self.current_line = None
self.current_file = None
def parse(self, parser):
lineno = next(parser.stream).lineno
self.current_line = lineno
self.current_file = parser.filename
condition_node = parser.parse_expression()
if parser.stream.skip_if("comma"):
context_node = parser.parse_expression()
else:
context_node = nodes.Const(None)
return nodes.CallBlock(
self.call_method(
"_assert", [condition_node, context_node], lineno=lineno
),
[],
[],
[],
lineno=lineno,
)
def _assert(
self, condition: bool, context_or_none: Optional[Any], caller
): # pylint: disable=unused-argument
if not condition:
error_message = (
f"Assertion error in the Jinja template: "
f"{self.current_file}:{self.current_line}."
)
if context_or_none:
error_message += f" Message: {context_or_none}"
raise TemplateRuntimeError(error_message)
return ""
jinja_environment = Environment(
loader=FileSystemLoader("path-to-templates"),
undefined=StrictUndefined,
extensions=[AssertExtension],
)
# use jinja_environment...
Note that in this code, StrictUndefined is also used to make Jinja raise exceptions when an undefined variable is referenced from a Jinja template. The assertions build the next level of more precise checks on top of StrictUndefined.
I have come to the idea of writing this extension because of several visual regressions that I found in my project. Without assertions, a number of errors can be easily introduced in Jinja templates, and these errors can be quite difficult to detect.
I would be happy to learn about your experience with making Jinja a safer markup language. Feel free to drop me a line.