How can I make a simple counter with Jinja2 templates?

I have two for loops, both alike in dignity. I'd like to have a counter incremented during each inner iteration.

For example, consider this template:

from jinja2 import Template
print Template("""
{% set count = 0 -%}
{% for i in 'a', 'b', 'c' -%} {% for j in 'x', 'y', 'z' -%} i={{i}}, j={{j}}, count={{count}} {% set count = count + 1 -%} {% endfor -%}
{% endfor -%}
""").render()

Shouldn't this print count=0 through count=8? Nope, it doesn't.

i=a, j=x, count=0
i=a, j=y, count=1
i=a, j=z, count=2
i=b, j=x, count=0
i=b, j=y, count=1
i=b, j=z, count=2
i=c, j=x, count=0
i=c, j=y, count=1
i=c, j=z, count=2

What gives?

Note: I can't simply save the outer loop variable to calculate the counter because, in my software, the number of inner iterations is variable.

2

5 Answers

With variable inner group sizes, this will work:

from jinja2 import Template
items = [ ['foo', 'bar'], ['bax', 'quux', 'ketchup', 'mustard'], ['bacon', 'eggs'], ]
print Template("""
{% set counter = 0 -%}
{% for group in items -%} {% for item in group -%} item={{ item }}, count={{ counter + loop.index0 }} {% endfor -%} {% set counter = counter + group|length %}
{% endfor -%}
""").render(items=items)

...which prints:

item=foo, count=0 item=bar, count=1
item=bax, count=2 item=quux, count=3 item=ketchup, count=4 item=mustard, count=5
item=bacon, count=6 item=eggs, count=7

I guess variables declared outside up more than one level of scope can't be assigned to or something.

It does look like a bug, but how about moving some of that calculation outside the template?

from jinja2 import Template
outer_items = list(enumerate("a b c".split()))
inner_items = list(enumerate("x y z".split()))
print Template("""
{% for outer, i in outer_items -%} {% for inner, j in inner_items -%} {% set count = outer * num_outer + inner -%} i={{i}}, j={{j}}, count={{count}} {% endfor -%}
{% endfor -%}
""").render(outer_items=outer_items, inner_items=inner_items, num_outer=len(outer_items))

Output:

i=a, j=x, count=0 i=a, j=y, count=1 i=a, j=z, count=2 i=b, j=x, count=3 i=b, j=y, count=4 i=b, j=z, count=5 i=c, j=x, count=6 i=c, j=y, count=7 i=c, j=z, count=8
2

To solve use cases like this one, I wrote a small environment filter that counts occurences of a key.

Here's de code (with doc test) of myfilters.py:

#coding: utf-8
from collections import defaultdict
from jinja2 import environmentfilter
from jinja2.utils import soft_unicode
@environmentfilter
def inc_filter(env, key, value=1, result='value', reset=False): """ Count ocurrences of key. Stores the counter on Jinja's environment. >>> class Env: pass >>> env = Env() >>> inc_filter(env, 'x') 1 >>> inc_filter(env, 'x') 2 >>> inc_filter(env, 'y') 1 >>> inc_filter(env, 'x') 3 >>> inc_filter(env, 'x', reset=True) 1 >>> inc_filter(env, 'x') 2 >>> inc_filter(env, 'x', value=0, reset=True) 0 >>> inc_filter(env, 'x', result=None) >>> inc_filter(env, 'x', result=False) u'' >>> inc_filter(env, 'x', result='key') 'x' >>> inc_filter(env, 'x') 4 """ if not hasattr(env, 'counters'): env.counters = defaultdict(int) if reset: env.counters[key] = 0 env.counters[key] += value if result == 'key': return key elif result == 'value': return env.counters[key] elif result == None: return None else: return soft_unicode('')
## Module doctest
if __name__ == '__main__': import doctest doctest.testmod() 

Setup your environment registering our custom filter:

#coding: utf-8
from jinja2 import Environment, FileSystemLoader
from myfilters import inc_filter
env = Environment(loader=loader=FileSystemLoader('path'))
env.filters['inc'] = inc_filter
t = env.get_template('yourtemplate.txt')
items = [ ['foo', 'bar'], ['bax', 'quux', 'ketchup', 'mustard'], ['bacon', 'eggs'], ]
res = t.render(items=items)

And on your template, use it like this:

{% for group in items -%} {% for item in group -%} item={{ item }}, count={{ 'an_identifier'|inc }} {% endfor -%}
{% endfor -%}

...which prints:

item=foo, count=0 item=bar, count=1
item=bax, count=2 item=quux, count=3 item=ketchup, count=4 item=mustard, count=5
item=bacon, count=6 item=eggs, count=7
0

There is builtin global function cycler() providing loop-independent value cycling. Using the same idea you can define your own counter() function like this:

env=Environment(...) # create environment
env.globals['counter']=_Counter # define global function
env.get_template(...).render(...) # render template

Here is the class that implements the function:

class _Counter(object): def __init__(self, start_value=1): self.value=start_value def current(self): return self.value def next(self): v=self.value self.value+=1 return v

And here is how to use it:

{% set cnt=counter(5) %}
item #{{ cnt.next() }}
item #{{ cnt.next() }}
item #{{ cnt.next() }}
item #{{ cnt.next() }}

It is gonna render:

item #5
item #6
item #7
item #8

No need to add a counter. You can access the outer loop's index like this:

{% for i in 'a', 'b', 'c' -%} {% set outerloop = loop %} {% for j in 'x', 'y', 'z' -%} i={{i}}, j={{j}}, count={{outerloop.index0 * loop|length + loop.index0}} {% endfor -%}
{% endfor -%}

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

You Might Also Like