TECH NOTES
by Stanislav Pankevich
Assertions in Jinja templates

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.