Jinja2

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

External

Internal

Overview

Jinja2 is a templating engine. Placeholders in the template allow writing code that has a syntax similar to Python. The engine ingests the template and data and renders the final document. Jinja2 provides template inheritance and inclusion. Jinja2 allows definition and import of macros within the template. Templated are compiled to Python code and cached, or they can be compiled ahead of time. Jinja2 provides extensible filters, tests, functions and syntax.

Jinja’s philosophy is that while application logic belongs in Python if possible, it shouldn’t make the template designer’s job difficult by restricting functionality too much.

Programming Model

Find out what the latest version is from https://pypi.org/project/Jinja2/

Then add this to your requirements.txt

jinja2 == 3.1.2

Read the Template from File

from pathlib import Path
from jinja2 import Environment, FileSystemLoader

tdir = Path('/Users/ovidiu/tmp/jinja')
template = Environment(loader=FileSystemLoader(tdir)).get_template('test-template.yaml.j2')

text = template.render(
    variable_1='blue',
    variable_2=5
)

For a template similar to:

color: {{  variable_1 }}
size: {{  variable_2 }}

the rendering looks similar to:

color: blue
size: 5

Template as String in Memory

template_as_text = """
color: {{  variable_1 }}
size: {{  variable_2 }}
"""

template = Environment().from_string(template_as_text)

text = template.render(
    variable_1='blue',
    variable_2=5
)

Variables

Variables can be provided individually as arguments of the render() method, as shown above.

Alternatively, a recursive data structure can be provided as argument:

data = {
    'variable_1': 'blue',
    'variable_2': 5,
}
text = template.render(data)

Template Variable

raw content followed by {{ color }}

If the color variable in the example above is not defined when the template is rendered, it will be rendered as an empty string.

Also see Variables above.

Conditionals

https://jinja.palletsprojects.com/en/latest/templates/#if-expression

The expressions described below can be used in conditionals:

{% if <expression> %}
something will be rendered here
{% elif <expression>  %}
something else will be rendered here
{% else %}
and something else will be rendered here
{% endif %}

Example:

{%- if color == 'blue' -%}
BLUE
{%- elif color == 'red'  -%}
RED
{%- else -%}
UNDEFINED
{%- endif -%}

Loops

my_list:
{%- for i in seq %}
  - '{{ i }}'
{%- endfor %}

where seq is a Python list:

text = Environment(...).get_template(...).render(seq=['a', 'b'])

Macros

https://jinja.palletsprojects.com/en/latest/templates/#macros

Macros are equivalent with functions in a regular programming language:

{%- macro some_macro(color, size) -%}
color: {{ color }}, size: {{ size }}
{%- endmacro -%}

{{- some_macro('blue', 1) }}

Rendering Booleans

If a boolean value from Python will be rendered as having its first character capitalized. This is a problem when rendering YAML or JSON. This is something that works but I am not sure is optimal (need to adjust whitespace):

{% if value is boolean %}{{ value | lower }}
{% else %}{{ value }}
{% endif %}

Rendering Dictionaries

something:
{%- for key, value in a_dict.items() %}
    {{key}}: {{value-}} 
{% endfor %}
somethingelse
text = Environment().from_string(...).render(a_dict={'a': 'A', 'b': 'B', 'c': 'C'})

This will render:

something:
    a: A
    b: B
    c: C
somethingelse

Rendering a List of Dictionaries

{% if a_list_of_maps %}
  some_tag_that_introduces_the_list_of_maps:
{%- for m in a_list_of_maps %}
     - {%- for k, v in m.items() %}
         {{ k }}: {{ v }}
       {%- endfor %}
{%- endfor %}
{%- endif %}

Recursive Rendering of a Deep Data Structure to YAML

Recursive Rendering of a Deep Data Structure to YAML

Expressions

Jinja allows expressions with a syntax similar to Python's. The expressions can be used in conditionals.

Literals

Jinja allows for the the following literals:

  • Double quoted strings: "Blue is a color".
  • Integers: 34.
  • Floating points: 34.1, 43.1e3.
  • Lists: ['this', 'is', 'a', 'list']. Lists are useful for storing sequential data to be iterated over.
  • Tuples: ('this', 'is', 'a', 'tuple').
  • Dictionaries: {'color': 'blue, 'size': 1}
  • Booleans: true, false. They are lowercase, but True and False is also supported, but the documentation recommends using lowercase.
  • None: none. It is lowercase. None is also supported, but the documentation recommends using lowercase.

Math

Supported operators:

  • +: adds objects together. If the objets are numbers, performs mathematical addition. If the objects are strings and list, the operator concatenates the operands.
  • -
  • /
  • //: divide numbers and returns the truncated result.
  • %
  • *: multiplies numbers, but can also be used to repeat a string a number of times:
{{ '-' * 80 }}

Comparisons

==

Use == compare two objects and assess equality.

{%- if color == 'blue' -%}
Painted blue
{%- endif -%}

== can be used to check for a specific boolean value:

{%if color_enabled == false  -%}
the color is enabled
{%- endif %}

!=

Use != compare two objects and assess inequality.

{%- if color != 'blue' -%}
not painted blue
{%- endif -%}

Use != with caution when attempting to detect empty strings. The following example:

{% if color != "" -%}
painted
{%- endif -%}

will render "painted" if color has some value, or if color is explicitly assigned the empty string. If the color variable is not defined, or it is None, the template will render "painted", which is probably not intended. For a comprehensive approach on handling undefined, None and empty variables, see Handling Undefined, None and Empty Variables below.

Other Comparisons

  • >
  • >=
  • <
  • <=

is

is

is defined Conditional

The is defined expression evaluates to true or false whether the variable it was applied to was defined (exists) or not. For more details on usage see Handling Undefined, None and Empty Variables below.

Type Conditional

A built-in test that checks the type may follow is: To check whether a value is a string:

{%if some_var is string  -%}
 ...

To check whether a value is a dictionary:

{%if some_var is mapping  -%}
 ...

To check whether a value is a iterable:

{%if some_var is iterable  -%}
 ...

in

is

and, or, not

  • and return true if the left hand and the right hand operands are true.
  • or return true if the left hand or the right hand operands are true.
  • not
{%if some_var == "a" and some_other_var == "b" -%}
my vars are {{ some_var }}, {{ some_other_var }}
{%- endif %}

(expr)

(expr) Groups an expression.

|

| apply a filter.

~

~ convert all operands into strings and concatenate them.

()

() call a callable. Inside of the parentheses you can use positional arguments and keyword arguments Python-style.

., []

  • . get an attribute of an object.
  • [] index or key operator.

For a complex recursive data structure, such as recursively embedded maps, passed to render(), an arbitrary level key can be accessed with the dot notation and also by key. The template:

color1: {{ m.m1.color }}
color2: {{ m['m2']['color'] }}

rendered with:

m = {
    'm1': {
        'color': 'red'
    },
    'm2': {
        'color': 'blue'
    }
}

will print:

color1: red
color2: blue

Python Methods

Built-in Tests

https://jinja.palletsprojects.com/en/latest/templates/#list-of-builtin-tests
https://www.webforefront.com/django/usebuiltinjinjafilters.html

none()

See Handling Undefined, None and Empty Variables below.

string()

number()

boolean()

{% if some_var is boolean %} ...

mapping()

iterable()

Rendering an Object's __str__() Representation

The __str__() is honored, but if it is a multi-line string, only the first line is correctly indented.

Whitespace Management

A leading dash removes all whitespace (including new lines) between the last non space template character and it. A trailing dash removes all whitespace (including new lines) between it and the next non-whitespace character from the template.

 {{-
 -}}
 {%-
 -%}

Indentation

  some_tag:
    {{ rendered_multiline_configuration | indent(4) }}

The first line must be explicitly indented with the same number of space. The spaces are added after each new line.

Filters

https://jinja.palletsprojects.com/en/latest/templates/#list-of-builtin-filters

upper

"{{ ... | upper }}"

lower

"{{ ... | lower }}"

indent

https://jinja.palletsprojects.com/en/latest/templates/#jinja-filters.indent
some_tag:
    {{ rendered_multiline_configuration | indent(4) }}

The first line must be explicitly indented with the same number of space. The spaces are added after each new line.

Code Examples

Handling Undefined, None and Empty Variables

The expression to check whether a variable has been defined or not is is defined. If we want to strictly test whether the variable is defined, even if its value is None or non-truthy, use is defined:

{%if color is defined  -%}
color: {{ color }} (may display None or a non-truthy value)
{%- else -%}
color variable is not defined
{%- endif %}

Simply using the variable as test expression, Python-style, will evaluate to false for non-defined variables, but also for variables that have been defined and have a None or a non-truthy value.

{%if color  -%}
color: {{ color }}
{%- else -%}
color variable is not defined, or it is None, or it is non-truthy
{%- endif %}

It is a good idea to check whether a variable exists with if <var-name> or if <var-name> is defined because an attempt to use an undefined variable in an expression will raise an exception. It is fine to use undefined variable in a rendering expression, the variable will render as the empty string.

The documentation says that none(var) expression identifies None values, but the execution of the following code with Jinja2 3.1.2 raised TypeError: 'NoneType' object is not callable regardless whether I defined or not color variable when rendering:

{%if none(color)  -%}
NONE
{%- endif %}

Recursive Rendering of a Deep Data Structure to YAML

Recursive Rendering of a Deep Data Structure to YAML