Data Manipulation - Advanced

All you need to know about Jinja to make the most out of the Data Manipulation step.

What is Jinja?

Jinja is a templating engine that allows text formatting, its syntax is inspired by Django and Python.

Jinja templates are simply text files .... A template contains variables and/or expressions, which get replaced with values when a template is rendered; and tags, which control the logic of the template.

You may find Jinja's official documentation: here

Basic functions

Manipulate inputs with simple statements (filters and expressions) to get the desired output.

For example, get the list of prices of all products, round to 2 decimals.

products | map(attribute='item.price') | map('round', 2) | list

You will only need to know about a few small things:

Attributes access

You can access attributes with a dot or brackets (for variables)

foo = { bar: 'hey' }
foo.bar # => 'hey'

var1 = 'bar'
foo[var1] # => 'hey'

Assignments

Assignments use the set tag and can have multiple targets:

{% set prices = products | map(attribute='item.price') | list %}
{% set navigation = [('index.html', 'Index'), ('about.html', 'About')] %}
{% set key, value = 'my_key', 'my_value' %}

Expressions

These work very similarly to regular Python; even if you’re not working with Python you should feel comfortable with it.

  • Literals
    The simplest form of expressions are literals. Literals are representations of Python objects such as strings and numbers. The following literals exist:
    • "Hello World"Everything between two double or single quotes is a string. They are useful whenever you need a string in the template (e.g. as arguments to function calls and filters, or just to extend or include a template).
    • 42 / 123_456Integers are whole numbers without a decimal part. The ‘_’ character can be used to separate groups for legibility.
    • 42.23 / 42.1e2 / 123_456.789Floating point numbers can be written using a ‘.’ as a decimal mark. They can also be written in scientific notation with an upper or lower case ‘e’ to indicate the exponent part. The ‘_’ character can be used to separate groups for legibility, but cannot be used in the exponent part.
    • ['list', 'of', 'objects']Everything between two brackets is a list. Lists are useful for storing sequential data to be iterated over.
    • ('tuple', 'of', 'values')Tuples are like lists that cannot be modified (“immutable”). If a tuple only has one item, it must be followed by a comma (('1-tuple',)). Tuples are usually used to represent items of two or more elements. See the list example above for more details.
    • {'dict': 'of', 'key': 'and', 'value': 'pairs'}A dict in Python is a structure that combines keys and values. Keys must be unique and always have exactly one value.
    • true / false true is always true and false is always false.

      Note: The special constants true, false, and none are indeed lowercase. Because that caused confusion in the past, (True used to expand to an undefined variable that was considered false), all three can now also be written in title case (True, False, and None). However, for consistency, (all identifiers are lowercase) you should use the lowercase versions.

  • Math
    Allows to calculate with values. The following operators are supported:
    • +Adds two objects together. Usually, the objects are numbers, but if both are strings or lists, you can concatenate them this way. This, however, is not the preferred way to concatenate strings! For string concatenation, have a look-see at the ~ operator. {{ 1 + 1 }} is 2.
    • -Subtract the second number from the first one. {{ 3 - 2 }} is 1.
    • /Divide two numbers. The return value will be a floating point number. {{ 1 / 2 }} is {{ 0.5 }}.
    • //Divide two numbers and return the truncated integer result. {{ 20 // 7 }} is 2.
    • %Calculate the remainder of an integer division. {{ 11 % 7 }} is 4.
    • *Multiply the left operand with the right one. {{ 2 * 2 }} would return 4. This can also be used to repeat a string multiple times. {{ '=' * 80 }} would print a bar of 80 equal signs.
    • **Raise the left operand to the power of the right operand. {{ 2**3 }} would return 8. Unlike Python, chained pow is evaluated left to right. {{ 3**3**3 }} is evaluated as (3**3)**3, but would be evaluated as 3**(3**3) in Python. Use parentheses to be explicit about what order you want.
  • Comparisons
    • ==Compares two objects for equality.
    • !=Compares two objects for inequality.
    • > true if the left hand side is greater than the right hand side.
    • >= true if the left hand side is greater or equal to the right hand side.
    • < true if the left hand side is lower than the right hand side.
    • <= true if the left hand side is lower or equal to the right hand side.
  • Logic
    • andReturn true if the left and the right operand are true.
    • orReturn true if the left or the right operand are true.
    • notnegate a statement (see below).
    • (expr)Parentheses group an expression.

      Note: The is and in operators support negation using an infix notation, too: foo is not bar and foo not in bar instead of not foo is bar and not foo in bar. All other expressions require a prefix notation: not (foo and bar).

  • Other operators
    The following operators are very useful but don’t fit into any of the other two categories:
    • inPerform a sequence / mapping containment test. Returns true if the left operand is contained in the right. 1 in [1, 2, 3] would, for example, return true.
    • isPerforms a test.
    • | (pipe, vertical bar)Applies a filter.
    • ~ (tilde)Converts all operands into strings and concatenates them.
      "Hello " ~ name ~ "!" would return Hello John!(if name == ‘John’)
    • ()Call a callable: post.render(). Inside of the parentheses you can use positional arguments and keyword arguments like in Python:
      post.render(user, full=true).
    • . / []Get an attribute of an object. (See Variables)

Filters

Variables can be modified by filters. They are a library of helper functions doing transformation for you so you do not have to do it manually. Filters are separated from the variable by a pipe symbol (|) and may have optional arguments in parentheses. Multiple filters can be chained. The output of one filter is applied to the next.
For example, name | trim | capitalize will remove all white space from the name and capitalize it.
Filters that accept arguments have parentheses around the arguments, just like a function call. For example: listx | join(', ') will join a list with commas.
You can use inline If with filters 'multiple' if array | count > 1 else 'one'
A lot of list filters return iterators (for loops) instead of lists, make sure to use the list filter.

Builtin filters

  • Complete list of Filters
    Jinja Builtin Filters

  • Frequently used filters
    5 filters are highlighted with a [must know] tag. These are the basic filters you want to know and will help you for most data manipulation operations. All the others will be useful but you can look them up later once you need them.

    • default
      Default value if the input does not exist

      'hey' | default('my_variable is not defined')
      => "hey"
      
      None | default('my_variable is not defined')
      => "my_variable is not defined"
      
    • capitalize / title / lower / upper
      Change the case of a string

      'hello' | capitalize
      => "Hello"
      
      'arbitrary code execution' | title
      => "Arbitary Code Execution"
      
      'this is an example' | upper
      => "THIS IS AN EXAMPLE"
      
      'THIS IS AN EXAMPLE' | lower
      => "this is an example"
      
    • map [must know]
      There are two main usage:

      • Extract attributes from objects in a list
      • Apply filter to all items in a list
      products = [
      	{name: 'abc', nested: {price: 2.77}},
      	{name: 'def', nested: {price: 3.82}},
      	{name: 'ghi', nested: {price: 1.39}}
      ] 
      
      products | map(attribute='name') | map('capitalize') | list
      => ['Abc', 'Ghi', 'Def']
      
      products | map(attribute='nested.price') | map('round', 1) | list
      => [2.8, 3.8, 1.4]
      
    • selectattr / rejectattr [must know]
      Filter objects list based on attributes
      Takes an attribute and a test (see tests section) as an argument

      users = [
      	{name: 'abc', active: True, role: 'admin'},
      	{name: 'def', active: False, role: 'admin'},
      	{name: 'ghi', active: True, role: 'builder'}
      ]
      
      users | selectattr('role', '==', 'admin') | list
      => [
      	{name: "abc", active: True, role: "admin"},
      	{name: "def", active: False, role: "admin"}
      ]
      
      users | selectattr('active') | list
      => [
      	{name: "abc", active: True, role: "admin"},
      	{name: "ghi", active: True, role: "builder"}
      ]
      
      # reverse of select
      users | rejectattr('active') | list
      => [
      	{name: "def", active: False}
      ]
      
      # with a regex
      apps = [
        {name: 'bosv1', database: 'bos-pgsql-v1'},
      	{name: 'boav1', database: 'boa-mango'},
      	{name: 'bosv2', database: 'bos-pgsql-v2'}
      ]
      apps | selectattr('database', 'search', '.*pgsql.*') | list
      => [
      	{name: 'bosv1', database: 'bos-pgsql-v1'},
      	{name: 'bosv2', database: 'bos-pgsql-v2'}
      ]
      
    • select / reject [must know]
      Takes a test (see tests section) as an argument

      # simple selects
      numbers | select("odd")
      numbers | select("even")
      numbers | select("divisibleby", 3)
      numbers | select("lessthan", 42)
      
      # Reject is reversed of select
      numbers | reject('odd')
      
      # Fin the number of users who are admin (assuming a map of roles)
      ['admin', 'nope', 'admin'] | select("==", "admin") | list
      => ['admin', 'admin'] | count 
      => 2
      
      # filter undefined variables after a map operation
      [undefined, "hey", "oh"] | select('defined') | list
      => ["hey", "oh"]
      
      # select items containing "v1"
      ["prod-v1", "prod_v2", "item_v1"] | select("in", "v1") | list
      => ["prod-v1", "item_v1"]
      
      # with regex
      ["p-v1_lk", "c-v2", "m-v1-lk"] | select("search", ".*v1(-|_)lk") | list
      => ["p-v1_lock", "m-v1-lock"]
      
    • flatten [must know]
      After a map, selects or more complex data manipulation, it will not be uncommon to end up with lists looking like this: `[[1,2], [3,4]]`
      Use flatten to merge them into a single list

      [[1, 2], [3, 4]] | flatten
      => [1, 2, 3, 4]
      
    • list [must know]
      Convert a value into a list. You will use it often since most useful filters return iterators instead of list and you have to convert them manually back to lists.

    • join

      ['abc', 'def', 'ghi'] | join(', ')
      => "abc, def, ghi"
      
      [{name: 'abc'}, {name: 'def'}] | join(', ', attribute='name')
      => "abc, def"
      
    • dictsort

      for key, value in mydict | dictsort
      => #sort the dict by key, case insensitive
      
      for key, value in mydict | dictsort(reverse=true)
      => #sort the dict by key, case insensitive, reverse order
      
      for key, value in mydict | dictsort(true)
      => #sort the dict by key, case sensitive
      
      for key, value in mydict | dictsort(false, 'value')
      => #sort the dict by value, case insensitive
      
    • format
      Format a string (printf)

      "%s, %s!" | format('Hello', 'world')
      Hello, world!
      
    • groupby

      users = [
      	{name: 'pierre', city: 'paris'},
        {name: 'leo', city: 'marseille'},
      	{name: 'lucy', city: 'paris'}
      ] 
      
      # Useful to use with for loops (for city, items in users | groupby('city'))
      users | groupby("city")
      => [
      		["marseille", [
      				{"city":"marseille", "name": "leo"}
      			]
      		],
      		["paris", [
      				{"city": "paris", "name": "pierre"},
      				{"city": "paris", "name": "lucy"}
      			]
      		]
      	]
      
      apps = [
        {name: 'bos', database: 'bos-pgsql-staging'},
      	{name: 'boa', database: 'bos-pgsql-staging'},
      	{name: 'bos', database: 'bos-pgsql-prod'},
      	{anon: true, database: 'Anon-mvp-pgsql-other'}
      ]
      
      apps | groupby("name", default: 'unknown')
      => [
      	["bos", [
      		{name: 'bos', database: 'bos-pgsql-staging'},
      		{name: 'bos', database: 'bos-pgsql-prod'},
      	],
      	["boa", [
      			{name: 'boa', database: 'bos-pgsql-staging'}
      		]
      	],
      	["unknown", [
      			{anon: true, database: 'Anon-mvp-pgsql-other'}
      		]
      	]
      ]
      
    • int / string

      '43' | int
      => 43
      
      'h43' | int
      => 0
      
      34 | string
      => '34'
      
      {name: 'hello'} | string
      => "{name: 'hello'}"
      
    • first / last

      [1, 2, 3] | first
      => 1
      
      [1, 2, 3] | last
      => 3
      
    • length / count

      [1, 2, 3] | length
      => 3
      
      [1, 3, 5, 10] | count # alias
      => 4
      
    • max / min

      [1, 2, 3] | min
      => 1
      
      [{code: 1}, {code: 2}] | max(attribute='code')
      => 2
      
    • random

      ['a', 'b', 'c'] | random
      => 'c' # random item of the list
      
    • replace

      'Hello World' | replace('Hello', 'Goodbye')
      => "Goodbye World"
      
      # Restrict to the first x occurences
      'aaaaargh' | replace('a', 'd,', 2)
      => d,d,aaargh
      
    • reverse

      ['a', 'b', 'c'] | reverse | list
      => ['c', 'b', 'a']
      
    • round

      42.55 | round
      => 43.0
      
      42.55 | round(1, 'floor')
      => 42.5
      
      42.54 | round(1, 'ceiling')
      => 42.6
      
    • slice

      ['a', 'b', 'c'] | slice(3) | list
      => [['a'], ['b'], ['c']]
      
    • sort

      ['b', 'a'] | sort | list
      => ["a", "b"]
      
      ['a', 'b'] | sort(reverse=true) | list
      => ["b", "a"]
      
      ['a', 'B', 'A'] | sort(case_sensitive=true) | list
      => ["A", "B", "a"]
      
      [{text: 'b'}, {text: 'a'}] | sort(attribute='text') | list
      => [{text: 'a'}, {text: 'b'}]
      
    • sum

      [22, 48] | sum
      => 70
      
      [{price: 22}, {price: 48}] | sum(attribute='price')
      => 70
      
    • trim

      "   hello   " | trim
      => "hello"
      
    • truncate

      "foo bar baz qux" | truncate(9)
      => "foo..."
      
      # Truncate regardless of words
      "foo bar baz qux" | truncate(9, True)
      => "foo ba..."
      
      # Truncate use a default leeway of 5, the fourth parameter.
      "foo bar baz qux" | truncate(11)
      => "foo bar baz qux"
      "foo bar baz qux" | truncate(11, False, '[...]', 0)
      => "foo bar[...]"
      
    • unique

Custom filters

These are additional filters implemented by ViaSay.

Date filters

The following filters enables to convert strings representing date/interval into python datetime/timedelta objects.

  • to_date: Parses a string into a datetime object, takes tzname as parameter.
{% set departure="20230730T1028" | to_date("Europe/Paris") %}
{% set departure=departure.strftime("%H:%M:%S - %d/%m/%Y") %}
# departure will contain the following value: "10:28:00 - 30/07/2023"
  • to_timedelta: Parses a timedelta string into a timedelta object.
{% set time_before_departure="1 day, 17:02:31" | to_timedelta %}
{% set today="20230730T1028" | to_date("Europe/Paris") %}
{% set departure=today + time_before_departure %}
{% set departure=departure.strftime("%H:%M:%S - %d/%m/%Y") %}
# departure will contain the following value:  "03:30:03 - 01/08/2023"
  • timestamp_to_date: Converts an unix timestamp (integer in milliseconds) to a datetime
{% set departure=1691597204000 | timestamp_to_date %}
{% set departure=departure.strftime("%H:%M:%S - %d/%m/%Y") %}
# departure will contain the following value:  "16:06:44 - 09/08/2023"

Warning: You cannot store a datetime in an output variable of the data manipulation step. This will result in an execution error. You need to format it first into a string using the strftime as shown in the examples above.

Tests

Beside filters, there are also so-called “tests” available. Tests can be used to test a variable against a common expression. To test a variable or expression, you add is plus the name of the test after the variable. For example, to find out if a variable is defined, you can do name is defined, which will then return true or false depending on whether name is defined in the current template context.

Tests can accept arguments, too. If the test only takes one argument, you can leave out the parentheses.

List of Builtin Tests

Python native methods

You can use most python native object methods.

For example you can split a string or merge dicts:

"abc, def, ghi".split(', ')
=> ['abc', 'def', 'ghi']

{name: 'pierre'}.update({role: 'admin'})
=> {name: 'pierre', role: 'admin'}

Here are the methods available for the main object types:

Advanced usage

Description of every feature provided by the template engine you can use in the code block. This will allow you to do more advanced manipulation with loops, multiple ifs, functions, etc. (aka imperative programming).

We are using Jinja 3 for templates. If you need references or more details once you’ve read this document, please checkout their official documentation.

It is derived from python so you will see a lot of similarities and you can even use some python methods on some objects.

Delimiters

There are a few kinds of delimiters. The default delimiters are configured as follows:

  • {% ... %} for Statements (do not print any value)
  • {{ ... }} for Expressions (print to the template output)
  • {# ... #} for Comments not included in the template output

In the code block, you can use all of them but the output will be discarded anyway. So you will want to use statements and comments.

In the outputs, values are wrapped in an expression so we can keep things simple.

Assignments

Assignments use the set tag and can have multiple targets:

{% **set** navigation = [('index.html', 'Index'), ('about.html', 'About')] %}
{% **set** key, value = call_something() %}
  • Scoping behavior
    Please keep in mind that it is not possible to set variables inside a block (except if which does not introduce a scope) and have them show up outside of it. This also applies to loops. As a result the following template is not going to do what you might expect:
    {% set iterated = false %}
    {% for item in seq %}
        {{ item }}
        {% set iterated = true %}
    {% endfor %}
    {% if not iterated %} did not iterate {% endif %}
    
    It is not possible to do this. Instead use alternative constructs like the loop else block or the special loop variable:
    {% for item in seq %}
        {{ item }}
    {% else %}
        did not iterate
    {% endfor %}
    
    More complex use cases can be handled using namespace objects which allow propagating of changes across scopes:
    {% set ns = namespace(found=false) %}
    {% for item in items %}
        {% if item.check_something() %}
            {% set ns.found = true %}
        {% endif %}
        * {{ item.title }}
    {% endfor %}
    Found item having something: {{ ns.found }}
    

List of Control Structures

  • If / elif / else
    The if statement is comparable with the Python if statement. In the simplest form, you can use it to test if a variable is defined, not empty and not false:

    {% if users %}
    	{% set text = 'users' %}
    {% endif %}
    

    For multiple branches, elif and else can be used like in Python. You can use more complex Expressions there, too:

    {% if kenny.sick %}
        {% set text = 'Kenny is sick.' %}
    {% elif kenny.dead %}
    		{% set text = 'You killed Kenny!  You ****!!!' %}
    {% else %}
    		{% set text = 'Kenny looks okay --- so far' %}
    {% endif %}
    

    If can also be used as an inline expression:

    {% set text = 'layout' if layout_template is defined else 'default' %}
    

    and for loop filtering:

    {% for user in users if not user.hidden %}
        {% do usernames.append(user) %}
    {% endfor %}
    
  • For

    Loop over each item in a sequence. For example:

    {% for user in users %}
      {% do array.append(user | trim | capitalize) %}
    {% endfor %}
    

    As variables in templates retain their object properties (from python), it is possible to iterate over containers like dict:

    {% for key, value in my_dict.items() %}
    		{% array.append([key, value]) %}
    {% endfor %}
    

    Python dictionaries may not be in the order you want to display them in. If order matters, use the | dictsort filter.

    {% for key, value in my_dict | dictsort %}
        {% array.append([key, value]) %}
    {% endfor %}
    

    Inside of a for-loop block, you can access some special variables:

    VariableDescription
    loop.indexThe current iteration of the loop. (1 indexed)
    loop.index0The current iteration of the loop. (0 indexed)
    loop.revindexThe number of iterations from the end of the loop (1 indexed)
    loop.revindex0The number of iterations from the end of the loop (0 indexed)
    loop.firstTrue if first iteration.
    loop.lastTrue if last iteration.
    loop.lengthThe number of items in the sequence.
    loop.cycleA helper function to cycle between a list of sequences.
    loop.depthIndicates how deep in a recursive loop the rendering currently is. Starts at 1
    loop.depth0Indicates how deep in a recursive loop the rendering currently is. Starts at 0
    loop.previtemThe item from the previous iteration. Undefined during the first iteration.
    loop.nextitemThe item from the following iteration. Undefined during the last iteration.
    loop.changed(*val)True if previously called with a different value (or not called at all).

    Within a for-loop, it’s possible to cycle among a list of strings/variables each time through the loop by using the special loop.cycle helper:

    {% for row in rows %}
    		{% set css_class = loop.cycle('odd', 'even') %}
        {% do array.append([css_class, row]) %}
    {% endfor %}
    

    Unlike in Python, it’s not possible to break or continue in a loop. You can, however, filter the sequence during iteration, which allows you to skip items. The following example skips all the users which are hidden:

    {% for user in users if not user.hidden %}
        {% do filtered_users.append(user) %}
    {% endfor %}
    

    The advantage is that the special loop variable will count correctly; thus not counting the users not iterated over.

    If no iteration took place because the sequence was empty or the filtering removed all the items from the sequence, you can render a default block by using else:

    {% for user i users %}
        {% do array.append(user) %}
    {% else %}
        {% do array.append('empty') %}
    {% endfor %}
    

    Note that, in Python, else blocks are executed whenever the corresponding loop did not break. Since loops cannot break anyway, a slightly different behavior of the else keyword was chosen.

    It is also possible to use loops recursively. This is useful if you are dealing with recursive data such as sitemaps or RDFa. To use loops recursively, you basically have to add the recursive modifier to the loop definition and call the loop variable with the new iterable where you want to recurse.

    The following example implements a sitemap with recursive loops:

    {% for item in sitemap recursive %}
        {% set title = item.title %}</a>
        {% if item.children %}
            {% do loop(item.children) %}
        {% endif %}
    		{% do array.append(title) %}
    {% endfor %}
    

    The loop variable always refers to the closest (innermost) loop. If we have more than one level of loops, we can rebind the variable loop by writing {% set outer_loop = loop %} after the loop that we want to use recursively. Then, we can call it using {{ outer_loop(…) }}

    Please note that assignments in loops will be cleared at the end of the iteration and cannot outlive the loop scope. See Assignments, scope behavior for more information about how to deal with this.

    If all you want to do is check whether some value has changed since the last iteration or will change in the next iteration, you can use previtem and nextitem:

    {% for value in values %}
        {% if loop.previtem is defined and value > loop.previtem %}
            The value just increased!
        {% endif %}
        {{ value }}
        {% if loop.nextitem is defined and loop.nextitem > value %}
            The value will increase even more!
        {% endif %}
    {% endfor %}
    

    If you only care whether the value changed at all, using changed is even easier:

    {% for entry in entries %}
        {% if loop.changed(entry.category) %}
            <h2>{{ entry.category }}</h2>
        {% endif %}
        <p>{{ entry.message }}</p>
    {% endfor %}
    

List of Global Functions

  • range
    Return a list containing an arithmetic progression of integers. range(i, j) returns [i, i+1, i+2, ..., j-1]; start (!) defaults to 0. When step is given, it specifies the increment (or decrement). For example, range(4) and range(0, 4, 1) return [0, 1, 2, 3]. The end point is omitted! These are exactly the valid indices for a list of 4 elements.
    This is useful to repeat a template block multiple times, e.g. to fill a list. Imagine you have 7 users in the list but you want to render three empty items to enforce a height with CSS:
    <ul>
    {% for user in users %}
        <li>{{ user.username }}</li>
    {% endfor %}
    {% for number in range(10 - users | count) %}
        <li class="empty"><span>...</span></li>
    {% endfor %}
    </ul>
    
  • lipsum
    Generates some lorem ipsum for the template. By default, five paragraphs of HTML are generated with each paragraph between 20 and 100 words. If html is False, regular text is returned. This is useful to generate simple contents for layout testing.
  • dict
    A convenient alternative to dict literals. {'foo': 'bar'} is the same as dict(foo='bar').
  • cycler
    Cycle through values by yielding them one at a time, then restarting once the end is reached.
    Similar to loop.cycle, but can be used outside loops or across multiple loops. For example, render a list of folders and files in a list, alternating giving them “odd” and “even” classes.
    Each positional argument will be yielded in the order given for each cycle.
    • properties
      • current
        Return the current item. Equivalent to the item that will be returned next time [next()](https://jinja.palletsprojects.com/en/3.0.x/templates/#jinja-globals.cycler.next) is called.
      • next()
        Return the current item, then advance [current](https://jinja.palletsprojects.com/en/3.0.x/templates/#jinja-globals.cycler.current) to the next item.
      • reset()
        Resets the current item to the first item.
  • joiner
    A tiny helper that can be used to “join” multiple sections. A joiner is passed a string and will return that string every time it’s called, except the first time (in which case it returns an empty string). You can use this to join things:
  • namespace
    Creates a new container that allows attribute assignment using the {% set %} tag:
    {% set ns = namespace() %}
    {% set ns.foo = 'bar' %}
    
    The main purpose of this is to allow carrying a value from within a loop body to an outer scope. Initial values can be provided as a dict, as keyword arguments, or both (same behavior as Python’s dict constructor):
    {% set ns = namespace(found=false) %}
    {% for item in items %}
        {% if item.check_something() %}
            {% set ns.found = true %}
        {% endif %}
         {{ item.title }}
    {% endfor %}
    Found item having something: {{ ns.found }}
    

Macros (functions)

Macros are comparable with functions in regular programming languages. They are useful to put often used idioms into reusable functions to not repeat yourself (“DRY”).

Here’s a small example of a macro that renders a form element:

{% set registered_users = [] %}

{% macro register_users(users, bot='', role='basic') -%}
		{% for user in users %}
		  {% do registered_users.append(user.update({role: role, bot: 'a'})) %}
		{% endfor %}
{%- endmacro %}

{% macro filter_sensitive(text, filter='[SENSITIVE]') -%}
		{# bank cards #}
		{% set filtered = text | regex_replace('^4[0-9]{12}(?:[0-9]{3})?$', filter) %}
		{# phone numbers #}
		{% set filtered = filtered | regex_replace('^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$', filter) %}
		
		{{ filtered }}
{%- endmacro %}

The macro can then be called like a function in the namespace:

register_users([{name: 'pierre'}, {name: 'paul'}], role='admin')
register_users([{name: 'leo'}, {name: 'lucy'}], role='builder', bot='op')

=> registered_users = [
		{name: 'pierre', role: 'admin', bot=''},
		{name: 'paul', role: 'admin', bot=''},
		{name: 'leo', role: 'builder', bot='op'},
		{name: 'lucy', role: 'builder', bot='op'}
]

Inside macros, you have access to three special variables:

  • varargs
    If more positional arguments are passed to the macro than accepted by the macro, they end up in the special varargs variable as a list of values.
  • kwargs
    Like varargs but for keyword arguments. All unconsumed keyword arguments are stored in this special variable.
  • caller
    If the macro was called from a call tag, the caller is stored in this variable as a callable macro.

Macros also expose some of their internal details. The following attributes are available on a macro object:

  • name
    The name of the macro. {{ input.name }} will print input.
  • arguments
    A tuple of the names of arguments the macro accepts.
  • catch_kwargs
    This is true if the macro accepts extra keyword arguments (i.e.: accesses the special kwargs variable).
  • catch_varargs
    This is true if the macro accepts extra positional arguments (i.e.: accesses the special varargs variable).
  • caller
    This is true if the macro accesses the special caller variable and may be called from a call tag.

If a macro name starts with an underscore, it’s not exported and can’t be imported.

Call

In some cases, it can be useful to pass a macro to another macro. For this purpose, you can use the special call block.

It’s also possible to pass arguments back to the call block. This makes it useful as a replacement for loops. Generally speaking, a call block works exactly like a macro without a name.