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

Pythonの汎用テンプレートエンジン Jinja の話。JinjaSphinxのバックエンドになってるので、Sphinx をインストールしてれば、あなた、既に手持ちです、お寺神社。

こんなことをしたくなった

 1 {%- set xmlattr_field_is_in = False %} {# おソトで変数初期化しといて... #}
 2 "FIELDS": [
 3     {%- for child in node["childs"] %}
 4     (
 5         '{{child["name"]}}',
 6         '{{child["xmlnode_type"]}}',  # xml node type
 7         ),
 8 
 9     {% if child["xmlnode_type"] == "attr" -%}
10     {%- set xmlattr_field_is_in = True -%} {# 条件満たしたら「真」言いたい... #}
11     {%- endif -%}
12 
13     {%- endfor %}

やりたくなった、だけであって、上のコードは正しく動かない。実は「やりたくなった」時点から、多分ダメだろう、とは思っていた。当然「ローカル変数のセット」にはsetしかないことを確認し、そして大抵のテンプレートエンジンでは必ず変数のスコープ規則が問題になることが頭をよぎり、して案の定。内側のループ内の変数は「ループ内ローカル」になる。当たり前だよね、「set」なんだし。ついつい

1 a = 1
2 for i in range(10):
3     a = i

を連想してしまうけれど、これは全然そうではなくて、例えば C/C++ のこれと同じ:

1 int a = 1;
2 for (int i = 0; i < 10; ++i) {
3     int a = i;
4 }

たとえば同じようなテンプレートエンジン FreeMarker (java製) では、ここにあるように、assignment にちゃんと種類がある。

どうしたもんかな、と思うと大抵 StackOverflow に答えがある。こんなだってさ:

 1 {%- set xmlattr_field_is_in = [False] %}
 2 "FIELDS": [
 3     {%- for child in node["childs"] %}
 4     (
 5         '{{child["name"]}}',
 6         '{{child["xmlnode_type"]}}',  # xml node type
 7         ),
 8 
 9     {% if child["xmlnode_type"] == "attr" -%}
10     {# ワタシは以下のように2行に分けて書いたが、StackOverflow の解では一行で書いてる #}
11     {%- set _ = xmlattr_field_is_in.pop() -%}
12     {%- set _ = xmlattr_field_is_in.append(True) -%}
13     {%- endif -%}
14 
15     {%- endfor %}

これも冷静に読めば当たり前の解で、外側で活きているリストのアイテムを内側で操作出来なくてはテンプレートエンジンを騙る資格なし、よね。けどダルいわ、これ。どうにかしてくれぃ(*)

FreeMarker みたいな仕様とか、Python そのものの「global」みたいなのが良いなぁとは思うけれど、ただ、個人的には

1     {%- var a -%}
2     {%- a = 1 -%}

だったら良かったのに、と思う。

ところでこの仕様に関して、どうやら Jinja 1.2 では出来ていたらしい。Jinja2 公式の issue として挙がってる:

この issue のコメントが笑える。