2018/08/23

Testing for data types in Jinja Templates

Writing templates for Jinja is an artform all of its very own. Jinja is the Python variant of the HandleBars templater that combines Python code styling with Mustache templating from Java.

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.

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.

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'

{% 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".

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 %}

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:

{% if value is sameas true or value is sameas false %}
{{ 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 %}


Satisfies the test for a number

Testing for a List

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



{% 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