Can a Jinja variable's scope extend beyond in an inner block?

TemplatesVariablesScopeJinja2

Templates Problem Overview


I have the following Jinja template:

{% set mybool = False %}
{% for thing in things %}
	<div class='indent1'>
        <ul>
            {% if current_user %}
              {% if current_user.username == thing['created_by']['username'] %}
			    {% set mybool = True %}
				<li>mybool: {{ mybool }}</li> <!-- prints True -->
                <li><a href='#'>Edit</a></li>
              {% endif %}
            {% endif %}
            <li>Flag</li>
        </ul>
    </div>
	<hr />
{% endfor %}

{% if not mybool %}
    <!-- always prints this -->
    <p>mybool is false!</p>
{% else %}
  <p>mybool is true!</p>
{% endif %}

If the condition is met in the for loop, I'd like to change mybool to true so I can display mybool is true! below. However, it looks like the scope of the inner mybool is limited to the if statement, so the desired mybool is never set.

How can I set the "global" mybool so I can use it in the last if statement?

EDIT

I've found some suggestions (only the cached page views correctly), but they don't seem to work. Perhaps they're deprecated in Jinja2...

EDIT

Solution provided below. I am still curious why the suggestions above do not work though. Does anyone know for sure that they were deprecated?

Templates Solutions


Solution 1 - Templates

One way around this limitation is to enable the "do" expression-statement extension and use an array instead of boolean:

{% set exists = [] %}
{% for i in range(5) %}
      {% if True %}
          {% do exists.append(1) %}
      {% endif %}
{% endfor %}
{% if exists %}
    <!-- exists is true -->
{% endif %}

To enable Jinja's "do" expression-statement extension: e = jinja2.Environment(extensions=["jinja2.ext.do",])

Solution 2 - Templates

Answer to a related question: I wanted to have a global counter of the number of times I entered a certain if-block in the template, and ended up with the below.

At the top of the template:

{% set counter = ['1'] %}

In the if-block I want to count:

{% if counter.append('1') %}{% endif %}

When displaying the count:

{{ counter|length }}

The string '1' can be replaced with any string or digit, I believe. It is still a hack, but not a very large one.

Solution 3 - Templates

Update 2018

As of Jinja 2.10 (8th Nov 2017) there is a namespace() object to address this particular problem. See the official Assignments documentation for more details and an example; the class documentation then illustrates how to assign several values to a namespace.

Solution 4 - Templates

Here's the general case for anyone wanting to use the namespace() object to have a variable persist outside of a for loop.

{% set accumulator = namespace(total=0) %}
{% for i in range(0,3) %}
    {% set accumulator.total = i + accumulator.total %}
    {{accumulator.total}}
 {% endfor %}`          {# 0 1 3 #}
 {{accumulator.total}}  {# 3 (accumulator.total persisted past the end of the loop) #}

Solution 5 - Templates

You can solve your problem using this hack (without extensions):

import jinja2

env = jinja2.Environment()
print env.from_string("""
{% set mybool = [False] %}
{% for thing in things %}
    <div class='indent1'>
        <ul>
            {% if current_user %}
              {% if current_user.username == thing['created_by']['username'] %}
                {% set _ = mybool.append(not mybool.pop()) %}
                <li>mybool: {{ mybool[0] }}</li> <!-- prints True -->
                <li><a href='#'>Edit</a></li>
              {% endif %}
            {% endif %}
            <li>Flag</li>
        </ul>
    </div>
    <hr />
{% endfor %}

{% if not mybool[0] %}
    <!-- always prints this -->
    <p>mybool is false!</p>
{% else %}
  <p>mybool is true!</p>
{% endif %}
""").render(current_user={'username':'me'},things=[{'created_by':{'username':'me'}},{'created_by':{'username':'you'}}])

Solution 6 - Templates

When writing a contextfunction() or something similar you may have noticed that the context tries to stop you from modifying it.

If you have managed to modify the context by using an internal context API you may have noticed that changes in the context don’t seem to be visible in the template. The reason for this is that Jinja uses the context only as primary data source for template variables for performance reasons.

If you want to modify the context write a function that returns a variable instead that one can assign to a variable by using set:

{% set comments = get_latest_comments() %}

Source

Solution 7 - Templates

Had a need to find the max num of entries in an object (object) from a list (objects_from_db),

This did not work for reasons known in jinja2 and variable scope.

 {% set maxlength = 0 %}
 {% for object in objects_from_db %}
     {% set ilen = object.entries | length %}
     {% if maxlength < ilen %}
         {% set maxlength = ilen %}
     {% endif %}
 {% endfor %}

Here's what works:

 {% set mlength = [0]%}
 {% for object in objects_from_db %}
     {% set ilen = object.entries | length %}
     {% if mlength[0] < ilen %}
         {% set _ = mlength.pop() %}
         {% set _ = mlength.append(ilen)%}
     {% endif %}
 {% endfor %}
 {% set maxlength = mlength[0] %}

Hope this helps someone else trying to figure out the same.

Solution 8 - Templates

Found this great article that describes a little hack. It's not possible to change value of a jinja variable in a different scope, but it's possible to modify a global dictionary values:

# works because dictionary pointer cannot change, but entries can 

{% set users = ['alice','bob','eve'] %} 
{% set foundUser = { 'flag': False } %} 

initial-check-on-global-foundUser: 
  cmd.run: 
    name: echo initial foundUser = {{foundUser.flag}} 

{% for user in users %} 
{%- if user == "bob" %} 
{%-   if foundUser.update({'flag':True}) %}{%- endif %} 
{%- endif %} 
echo-for-{{user}}: 
  cmd.run: 
    name: echo my name is {{user}}, has bob been found? {{foundUser.flag}} 
{% endfor %} 

final-check-on-global-foundUser: 
  cmd.run: 
    name: echo final foundUser = {{foundUser.flag}}

I've also found very helpful this syntax to set the value without actually using set:

{%-   if foundUser.update({'flag':True}) %}{%- endif %} 

It actually checks the result of an update operation on a dictionary (note to self).

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMatt NorrisView Question on Stackoverflow
Solution 1 - TemplatesGarrettView Answer on Stackoverflow
Solution 2 - TemplatesGodsmithView Answer on Stackoverflow
Solution 3 - TemplatesJensView Answer on Stackoverflow
Solution 4 - TemplatesjeffmjackView Answer on Stackoverflow
Solution 5 - TemplatesAlvaro FuentesView Answer on Stackoverflow
Solution 6 - TemplatesShankar CabusView Answer on Stackoverflow
Solution 7 - TemplatesPaddy VView Answer on Stackoverflow
Solution 8 - TemplatesMaciejgView Answer on Stackoverflow