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 Templ开发者_开发技巧ate("""
{% 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.
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
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
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 -%}
精彩评论