Some Background on Jinja
Jinja allows loops, conditionals and a very Unix/Shell like ability to pipe the contents of variables from function/filter to the next to evaluate and transform variables. One of the simplest forms that helps to explain the syntax is
{{ variable | default("variable had no value") }}
In the above case, the value defined by variable is piped to the default filter after the pipe. The default filter does what it's name implies, if the value passed is None or Undefined it applies the passed arg as the return.
There are many other good examples of Jinja filters and loops so I'm not going to try to cover that material here.
{% for value in python_list | default([]) %}
{{ value }}
{% endfor %}
As you can see from the example above and combining it with knowledge about default you can see that the loop executes and in the event python_list is undefined the for statement is protected against failure since an empty list will be substituted for the undefined.
The issue is how to protect against other data types, if python_list was a dictionary how would this behave, or any other python type.
Jinja includes some type tests, but they sometimes are simple as but the creators of the Jinja typing were apparently thinking about language independent tests when they created them. A great resource for Jinja filters is https://www.webforefront.com/django/usebuiltinjinjafilters.html which discusses the primates
{% if value is string %}
{{ value }} is a string
{% endif %}
The complication starts because a string "is string" also tests positive as "is sequence" and "is iterable" because a string is a list of bytes that python treats as both an array string[0] and a simple variable
What's missing from the filters are items like "is boolean" "is list" and "is dict".
{% if value is sameas true or value is sameas false %}
{{ value }} is a boolean
{% endif %}
{% endif %}
In the above case, the value defined by variable is piped to the default filter after the pipe. The default filter does what it's name implies, if the value passed is None or Undefined it applies the passed arg as the return.
There are many other good examples of Jinja filters and loops so I'm not going to try to cover that material here.
Testing Types in Jinja
In order to create templates that are not brittle and fail in unexpected ways testing that variables are as expected before getting into more complex code is always advisable. A simple precaution might look like{% for value in python_list | default([]) %}
{{ value }}
{% endfor %}
As you can see from the example above and combining it with knowledge about default you can see that the loop executes and in the event python_list is undefined the for statement is protected against failure since an empty list will be substituted for the undefined.
The issue is how to protect against other data types, if python_list was a dictionary how would this behave, or any other python type.
Jinja includes some type tests, but they sometimes are simple as but the creators of the Jinja typing were apparently thinking about language independent tests when they created them. A great resource for Jinja filters is https://www.webforefront.com/django/usebuiltinjinjafilters.html which discusses the primates
'is string' (definitive)
'is mapping' (definitive)
'is number' (non-definitive)
'is iterable' (overloaded)
'is sequence' (overloaded)
'is sameas' (tests inheritance)
As a result of the non-deterministic nature of the tests described above a bit more logic must be applied to determine Python types if you want to use the Jinja filters (vs in-line python)
Below are the best conditionals I've found to type an element, tho if you were to structure it as a If/Then style case block you can simplify the tests a bit because of precedence. These are meant to be stand-alone "fully-qualified" tests.
'is mapping' (definitive)
'is number' (non-definitive)
'is iterable' (overloaded)
'is sequence' (overloaded)
'is sameas' (tests inheritance)
As a result of the non-deterministic nature of the tests described above a bit more logic must be applied to determine Python types if you want to use the Jinja filters (vs in-line python)
Below are the best conditionals I've found to type an element, tho if you were to structure it as a If/Then style case block you can simplify the tests a bit because of precedence. These are meant to be stand-alone "fully-qualified" tests.
Testing for Strings
Testing for strings is the singular simple test with Jinja without the need for any additional conditionals to be used. All string objects are 'is string' and non-strings are 'not is string'
{{ value }} is a string
{% endif %}
The complication starts because a string "is string" also tests positive as "is sequence" and "is iterable" because a string is a list of bytes that python treats as both an array string[0] and a simple variable
What's missing from the filters are items like "is boolean" "is list" and "is dict".
Testing for a Dict
Testing for Dictionary objects is also simplified since they are the only type i'm aware of that tests true for 'is mapping'
{% if value is mapping %}
{{ value }} is a mapping
{% endif %}
{% if value is mapping %}
{{ value }} is a mapping
{% endif %}
Testing for a Boolean
Testing for a Boolean is a bit more complex, since technically a Boolean is also a number (and Boolean's test truthy for 'is number'). It turns out that any Boolean will test 'is sameas' for its own state so:
{{ value }} is a boolean
{% endif %}
Satisfies the test for a boolean
Testing for a Number
Testing for a Number should be straight-forward 'is number' but since as discussed above since Boolean's test true for 'is number' the true test of a number is expressed as "is number and not is boolean"
{% if value is number and value is not sameas true and value is not sameas false %}
{{ value }} is a boolean
{% endif %}
Lastly, testing for a list is a compound conditional since lists are both sequences and iterable but are not mappings or strings. So to test for a list the proper representation looks like{{ value }} is a boolean
{% endif %}
Satisfies the test for a number
Testing for a List
{% if value is sequence and value is not mapping and value is not string %}
{{ value }} is a list{% endif %}
Summary and Example Code
Below is python code demonstrating the above principles in an easy to replicate manner. OrderedDict is used to produce results in a repeatable manner, it was not necessary.import yaml
from collections import OrderedDict from jinja2 import Template as Jinja_Template
sample_data = """ a: 1 b: one c: "2" d: "three" e: - 1 - 2 - 3 f: one: test two: further testing 3: even more testing g: True h: False """ sample_template = """ Testing Jinja Types
{%- for key,value in sample_data.items() %} Processing {{key}}={{value}} {% if value is sameas true %} is True{% endif -%} {% if value is sameas false %} is False{% endif -%} {% if value is number %} is number{% endif -%} {% if value is mapping %} is mapping{% endif -%} {% if value is sequence %} is sequence{% endif -%} {% if value is iterable %} is iterable{% endif -%} {% if value is string %} is string{% endif -%}
{% endfor %} Testing Python Types
{%- for key,value in sample_data.items() %} Processing {{key}}={{value}} {% if value is string %} its a String{% endif -%} {% if value is mapping %} its a Dict{% endif -%} {% if value is sameas true or value is sameas false %} its a Boolean{% endif -%} {% if value is number and value is not sameas true and value is not sameas false%} its a Number{% endif -%} {% if value is sequence and value is not mapping and value is not string %} its a List{% endif -%}
{% endfor %} """ sample_data = yaml.load(sample_data) print(yaml.dump(sample_data, default_flow_style=False)) sample_data = OrderedDict( sorted(sample_data.items(), key=lambda x: x[0]) ) templater = Jinja_Template(sample_template) print(templater.render(sample_data=sample_data))
Output from the above:
Testing Jinja Types
Processing a=1 is number
Processing b=one is sequence is iterable is string
Processing c=2 is sequence is iterable is string
Processing d=three is sequence is iterable is string
Processing e=[1, 2, 3] is sequence is iterable
Processing f={3: 'even more testing', 'two': 'further testing', 'one': 'test'} is mapping is sequence is iterable
Processing g=True is True is number
Processing h=False is False is number Testing Python Types
Processing a=1 its a Number
Processing b=one its a String
Processing c=2 its a String
Processing d=three its a String
Processing e=[1, 2, 3] its a List
Processing f={3: 'even more testing', 'two': 'further testing', 'one': 'test'} its a Dict
Processing g=True its a Boolean
Processing h=False its a Boolean