Jinja: 2.9 在循环内分配变量时的回归

创建于 2017-01-07  ·  65评论  ·  资料来源: pallets/jinja

2.9:

>>> jinja2.Template('{% set a = -1 %}{% for x in range(5) %}[{{ a }}:{% set a = x %}{{ a }}] {% endfor %}{{ a }}').render()
u'[:0] [:1] [:2] [:3] [:4] -1'

2.8:

>>> jinja2.Template('{% set a = -1 %}{% for x in range(5) %}[{{ a }}:{% set a = x %}{{ a }}] {% endfor %}{{ a }}').render()
u'[-1:0] [0:1] [1:2] [2:3] [3:4] -1'

最初在 IRC 上报道:

jinja2 范围的变化似乎影响了我,我不确定正确的修复方法。 具体问题是这里的年份分配: https :

最有用的评论

changed_from_last听起来不错 - 至少在功能方面,这个名字本身有点别扭 IMO。 也许只是changed就足够清楚了?

我猜第一个元素总是被认为是“改变”的,不管它是什么? 如果这种行为对某人来说不合适,他们总是可以添加not loop.first支票。

所有65条评论

虽然当前的行为是不正确的,但旧的行为也肯定是不正确的。 {% set %}标签从未打算覆盖外部作用域。 我很惊讶它在某些情况下确实如此。 不知道在这里做什么。

我认为正确的行为是像[-1:0] [-1:1] [-1:2] [-1:3] [-1:4] -1这样的输出。

在这种特定情况下,我通过将其重写为使用groupby来解决它。

我觉得这是一个有点常见的用例 - 也是像{% set found = true %}循环中的东西,然后再检查它。 当这停止工作时,它肯定会破坏人们的东西......

我很震惊这以前有效。 这总是有效吗?

显然是的:

>>> import jinja2
>>> jinja2.__version__
'2.0'
>>> jinja2.Template('{% for x in range(5) %}[{{ a }}:{% set a = x %}{{ a }}] {% endfor %}{{ a }}').render()
u'[:0] [0:1] [1:2] [2:3] [3:4] '

伟大的。 因为这里的问题是这绝对不合理。 它应该覆盖哪个变量? 如果中间有一个函数作用域怎么办。 例如:宏或类似的东西。 我现在不知道如何支持这一点。

嗯,假设没有使用nonlocal ,可能类似于 python 的范围)? 即不允许覆盖在外部作用域中定义的内容(即,如果中间有一个宏)但允许覆盖其他范围?

显然在此之前被模板断言错误捕获:

>>> Template('{% set x = 0 %}{% for y in [1, 2, 3] recursive %}{{ x }}{% set x = y %}{% endfor %}').render()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
jinja2.exceptions.TemplateAssertionError: It's not possible to set and access variables derived from an outer scope! (affects: x (line 1)

使用/不使用递归时,行为似乎有所不同(2.8.1 给了我一个 UnboundLocalError 错误,递归和'012' 没有)

未绑定的本地错误应该在 master 上解决。

我不确定如何在这里取得进展。 我真的不喜欢显然可以做这种事情。

随着最后的变化,我将把它关闭为“按预期工作”。 如果有进一步的后果,我们可以再次研究替代方案。

在从 v.2.8.1 升级到 v2.9.4 时,此更改炸毁了我的博客模板后被发送到此问题(请参阅 #656)。

我正在使用它来跟踪循环迭代之间的各种数据是否发生变化。 我之所以能够修复它,是因为我编写了原始模板代码(参见 https://github.com/MinchinWeb/seafoam/commit/8eb760816a06e4f0382816f82586204d1e7734fd 和 https://github.com/MinchinWeb/seafoam/commit/89d528254d9451f94d9f954f94e4f0382816怀疑我本来能够否则。 新代码更难遵循,因为比较现在是在线完成的。 例如,我的旧代码(v.2.8.1):

{%- set last_day = None -%}

{% for article in dates %}
    {# ... #}
    <div class="archives-date">
        {%- if last_day != article.date.day %}
            {{ article.date | strftime('%a %-d') }}
        {% else -%}
            &mdash;
        {%- endif -%}
    </div>
    {%- set last_day = article.date.day %}
{% endfor %}

以及带有内联比较的新代码 (v2.9.4):

{% for article in dates %}
    {# ... #}
    <div class="archives-date">
        {%- if ((article.date.day == dates[loop.index0 - 1].date.day) and
                (article.date.month == dates[loop.index0 - 1].date.month) and 
                (article.date.year == dates[loop.index0 - 1].date.year)) %}
                    &mdash;
        {% else -%}
                    {{ article.date | strftime('%a %-d') }}
        {%- endif -%}
    </div>
{% endfor %}

所以我只想说“功能”(或“hack”,如果你愿意的话)已经使用并且已经被遗漏了。

如果目前范围界定问题太复杂而无法合理解决,是否可以(至少)将某些内容添加到变更日志中,以便减少人们不知道的情况?

我不知道这被如此广泛地滥用:-/ 令人讨厌的是,真的没有办法以任何可靠的方式使这项工作。 但是我想知道我们是否可以在这里隔离常见用例并引入更好的 api。

特别是我们想添加这样的东西( loop.changed_from_last ):

{% for article in dates %}
    <div class="archives-date">
        {%- if loop.changed_from_last(article.date.day) %}
            {{ article.date | strftime('%a %-d') }}
        {% else -%}
            &mdash;
        {%- endif -%}
    </div>
{% endfor %}

changed_from_last听起来不错 - 至少在功能方面,这个名字本身有点别扭 IMO。 也许只是changed就足够清楚了?

我猜第一个元素总是被认为是“改变”的,不管它是什么? 如果这种行为对某人来说不合适,他们总是可以添加not loop.first支票。

也许previous_context只是保存了整个先前的上下文,而不是试图决定用户将用它做什么。

只是previous还是prev ? “上下文”在这种情况下听起来相当混乱(没有双关语)

.....现在我已经可以想象有人要next :/

或者可能是写在范围之外的明确声明: set_outer或其他东西。

我在想这个:

class LoopContextBase(object):

    def __init__(self, recurse=None, depth0=0):
        ...
        self._last_iteration = missing

    def changed(self, *value):
        if self._last_iteration != value:
            self._last_iteration = value
            return True
        return False

@davidism我们不能在不破坏整个范围系统的情况下执行set_outer 。 这将在很大程度上搞砸整个事情。

是的,以为会是这样。 我期待“如果我想知道当前值是否大于前一个值怎么办”或类似的东西。 但我也喜欢changed方法。

作为一个额外的数据点,在我需要将一个特殊的类注入到生成的 HTML 中的情况下,它落在了我的脚下,但仅限于迭代列表的第一个元素,它没有一些特定的属性集。 我无法修改迭代数据。 我不能使用loop.first (尽可能多地使用)或与最后一个元素中的任何内容进行比较以可靠地完成我在这里需要做的事情,这导致了对完美运行的邪恶构造的实验(我不清楚实际上是我在滥用错误)。

此外,我通过第三方插件提供扩展功能,并且无法监管作者如何构建他们的模板,在更新到 Jinja 2.9+ 后导致最终用户突然崩溃。 我现在已经在我的程序中固定了最新的 2.8 版本,以保持向后兼容(正如我的版本控制方案所承诺的那样),直到我找到一种方法让作者更新他们的模板。

有人可以澄清为什么 jinja2 范围不能完全像 python 那样工作吗? 即,jinja2 模板对应于 python 模块,宏对应于函数(并且有自己的作用域),for 循环没有自己的作用域等等。 虽然@mitsuhiko说 jinja 2.8 及以下版本有很多范围界定错误,但我更直观地了解范围界定的工作原理。

对于 Python 程序员来说,第一篇文章(Jinja 2.8)的行为是显而易见的。 同样适用于https://github.com/pallets/jinja/issues/660 ,我不明白为什么 python 库应该实现 javascript 行为(我知道 python 对默认参数的处理并不理想,但任何 python 开发人员都知道其中)。

或者,应该有一个文档来描述 jinja 中的作用域是如何工作的,这样我们就不必猜测了。

另外请不要消极对待我的评论,我非常感谢jinja。

@roganov 对此有几点说明:

有人可以澄清为什么 jinja2 范围不能完全像 python 一样工作

因为 Python 的范围在我看来非常糟糕,尤其是在模板中,因为您很容易意外地覆盖重要信息。

虽然@mitsuhiko说 jinja 2.8 及以下版本有很多范围界定错误,但我更直观地了解范围界定的工作原理。

请注意,这是由于错误导致的异常,它仅在某些有限的情况下起作用并且纯粹是偶然的。 Jinja 始终具有相同的记录范围规则,并且从未记录

或者,应该有一个文档来描述 jinja 中的作用域是如何工作的,这样我们就不必猜测了。

几天前我已经为此改进了文档

如果有一种方法可以将变量显式传递到循环中呢? 也许这样的语法(从文本过滤器借用):

{%- set last_day = None -%}

{% for article in dates | pass_variable ( last_day ) %}
    {# ... #}
    <div class="archives-date">
        {%- if last_day != article.date.day -%}
            {{ article.date | strftime('%a %-d') }}
        {%- else %}
            &mdash;
        {% endif -%}
    </div>
    {%- set last_day = article.date.day %}
{% endfor %}

或者有没有办法做类似于scoped事情,因为它目前适用于宏?


另一个用例是在循环之间维护某种计数器。 例如,总共有多少行? 有多少行符合某些条件? 所选行的某些属性的总和(打印总行)?

过滤器语法在这里是不可能的,因为过滤器已经可以在迭代器上使用了。

一个看起来不错的可能的语法可能是这样的:

{% for article in dates with last_day %}

特别是因为with已经在 J​​inja 中进行了范围界定,例如用作{% with %} 。 OTOH,这里正好相反,因为块会打开一个新的作用域,而不是使外部作用域变量可访问..

不确定这是否是 Jinja 需要的功能。

没有语法会改变这一点,因为它仍然会破坏范围规则。 您不能使用当前的编译或 id 跟踪系统来做到这一点。 即使它适用于这种简单的情况,如果有一个recursive的 for 循环会发生什么。 如果有人在 for 循环中定义了一个宏怎么办。 如果调用块出现在 for 循环中怎么办。

我认为引入一个充当存储命名空间的全局对象会更有意义,然后可以使用 . 例如:

{% set foo = namespace() %}
{% set foo.iterated = false %}
{% for item in seq %}
  {% set foo.iterated = true %}
{% endfor %}

然而,这需要set标签从根本上改变。

@mitsuhiko和那种模式已经与do标签一起使用: http :

FWIW, do的解决方案非常糟糕。 就像人们在 Python 2 中需要nonlocal时使用的解决方法一样......

我完全同意,只是指出它就在那里。 正如亚历克斯指出的那样,很多这些问题都可以改写,无论是使用过滤器还是将一些逻辑放在 Python 中。

另一个(丑陋的)hack 是使用列表、追加和弹出: http :

嗨,我已经使用这段代码大约 2 年了,现在它中断了,并且来自新文档和这个讨论,我认为没有任何其他方法可以做我在这里尝试做的事情。 如果不是,请纠正我。

{% set list = "" %}
{% for name in names %}
{% if name.last is defined %}
{% set list = list + name.last + " " %}
{% if loop.last %}
{{ list.split(' ')|unique }}
{% endfor %}

@pujan14

{{ names|map(attribute="last")|select|unique }}

尽管unique不是内置过滤器,但您始终可以添加另一个过滤器来完成整个操作,因为您已经添加了unique过滤器。

你好,
我知道由于这个原因有很多“丑陋”的例子,但是您能否建议如何以优雅/正确的方式在 jinja 模板中的 for 循环上增加变量? 因为我的代码也被破坏了。

为什么你需要这样做? 另外,请包括您的代码。

{% for state in states.sensor -%}
{% if loop.first %}
{% set devnum = 0 %}
{% endif -%}
{%- if state.state == "online" %}
{% set devnum = devnum + 1 %}
{%- endif -%}
{% if loop.last %}
{{ devnum }}
{% endif -%}
{%- endfor -%}

您能否建议如何以优雅/正确的方式在 jinja 模板中的 for 循环上增加变量?

您根本不应该(或能够)这样做。 所以答案是不要那样做。 然而,我们感兴趣的是为什么模板作者似乎需要这样做。

Jinja 已经枚举了循环。 {{ loop.index0 }}

@davidism我只需要满足条件的那些

编写一个过滤器,按照您需要的方式生成devnum, sensor元组。 或者在 Python 中计算并传递给模板。 或者使用下面的例子。 这适用于我见过的所有其他示例。

@Molodax你可以这样做:

{{ states.sensor|selectattr('state', 'equalto', 'online')|sum }}

你不是说|count吗?

对不起,是的,数数。

@mitsuhiko ,感谢您的支持,不幸的是,它不起作用。
也许,jinja2(过滤器)已经在使用(家庭助理)的项目中实现了一些限制。 但它确实适用于我之前提供的代码。 遗憾。

嗨伙计。 您要求在 2.8 下工作的示例代码,所以这是我的:

{% set count = 0 %}
{% if 'anchors' in group_names %}
nameserver 127.0.0.1
{% set count = count+1 %}
{% endif %}
{% for resolver in resolvers %}
{% if count < 3 %}
{% if resolver|ipv6 and ansible_default_ipv6.address is defined %}
nameserver {{ resolver }}
{% set count = count+1 %}
{% elif resolver|ipv4 and ansible_default_ipv4.address is defined %}
nameserver {{ resolver }}
{% set count = count+1 %}
{% endif %}
{% endif %}
{% endfor %}

如果没有可以在 2 个单独循环中引用的全局“计数”变量,我不知道如何做到这一点。 你有什么建议可以让它在 2.8 和 2.9 下都能工作吗?

@davidism您的解决方案很棒,但我正在努力实现这一目标。
创建 2 个列表如下

{% set list1 = name|default()|map(attribute="last")|select|list %}
{% set list2 = name|default()|map(attribute="age")|select|list %}

然后将它们合并到 list3 中,它应该如下所示,最后在 list3 上应用唯一的(ansible 过滤器)
最后1-年龄1
last2-age2
last3-age3
最后四岁
上个 5 岁
上个 6 岁

根据我收集的地图不适用于多个属性 #554
我正在通过 ansible 使用 jinja2,所以事先在 python 中添加一些东西对我来说不是一个好主意。

@pujan14 @aabdnn @Molodax这个模式是来自官方的 Ansible 文档,还是你想出的或在别处找到的? 无论哪种方式,向 Ansible 报告这一点可能更容易,因为他们了解应该如何使用他们的产品,并且可能会提出更相关的解决方案。

@davidism我上面介绍的模板并非来自任何 Ansible 文档。 Ansible 没有专门记录 Jinja2。 我刚刚通过阅读 Jinja2 文档自己创建了这个模板,并且它起作用了,所以我将它投入生产。 我假设 Jinja2 将变量设为全局变量。

如果它没有正式记录,那么我更倾向于按原样关闭它。 我会重申,Ansible 可能会在这方面为您提供更多帮助。

@davidism我在新的 jinja 2.9 中使用循环
这是一个例子。

{% for name in names %}
{% if loop.first %}
{% set list = "" %}
{% endif %}
{% if name.first is defined and name.last is defined and not name.disabled %}
{% set list = list + name.first|string + "-" + name.last|string %}
{% if loop.last %}
{% for item in list.split(' ')|unique %}
{{ item }}
{% endfor %}
{% else %}
{% set list = list + " " %}{% endif %}
{% endif %}
{% endfor %}

这可能不是最好的方法,但根据我的理解,我没有违反任何范围规则。

在 2.8 及更高版本中有此问题

下面是一个测试用例:

import unittest
from jinja2 import Template

TEMPLATE1 = """{% set a = 1 %}{% for i in items %}{{a}},{% set a = a + 1 %}{% endfor %}"""

class TestTemplate(unittest.TestCase):

  def test_increment(self):
    items = xrange(1,10)
    expected='%s,' % ','.join([str(i) for i in items])
    t = Template(TEMPLATE1)
    result = t.render(items=items)
    self.assertEqual(expected,result)

unittest.main()

请改用loop.index

我认为通过loop对象的属性提供对前一个循环值的访问是唯一好的解决方案。 我刚刚在我们的项目中发现了这个片段,它无法通过检查最后一个对象是否与当前对象不同来解决,并且 groupby 在那里也不起作用,因为它不仅仅是获取密钥的微不足道的项目/属性访问:

{% set previous_date = none %}
{% for item in entries -%}
    {% set date = item.start_dt.astimezone(tz_object).date() %}
    {% if previous_date and previous_date != date -%}
        ...
    {% endif %}
    {% set previous_date = date %}
{%- endfor %}

是的,这听起来像是一个主意。

set(key, value)get(key)方法添加到循环对象怎么样? 然后人们可以在循环中存储他们想要的任何东西。

有同样的想法,但后来找不到任何需要这样做的非丑陋案例。 我已经看到有人要求setdefaultpop和其他类似 dict 的方法。

@davidism @ThiefMaster我想只是有一个可用的存储对象。 像这样:

{% set ns = namespace() %}
{% set ns.value = 42 %}
...{% set ns.value = 23 %}

显然 set 目前不能设置属性,但这可以很容易地扩展。 由于没有对命名空间本身进行重新分配,因此实现起来非常安全。

对我来说似乎很好,它与 Py 2 的nonlocal解决方案相同。或者,自动设置loop.ns ,尽管这在循环外不可用。

我不喜欢命名空间的一点是,当obj不是namespace() ,它会很诱人地执行{% set obj.attr = 42 %} namespace() - 我认为不应该这样做.

否则它看起来是一个有趣的想法,尽管我认为previtem / nextitem / changed()很好地涵盖了“简单”的情况,而不必定义新对象的“噪音”在模板中。

@ThiefMaster是行不通的。 我认为这种工作的方式是属性分配通过环境上的回调,该回调只允许对命名空间对象进行修改。

好的,所以至少没有模板对传递给它们的对象造成意外副作用的风险。

仍然有点担心人们可能会做的事情......

{% macro do_stuff(ns) %}
    {% set ns.foo %}bar{% endset %}
    {% set ns.bar %}foobar{% endset %}
{% endmacro %}

{% set ns = namespace() %}
{{ do_stuff(ns) }}

实际上,我认为像{% namespace ns %}这样的新块比一个可调用的块更好定义一个名为namespace的变量听起来不太可能传递给模板,而它可能只是阻止您在该模板中使用名称空间功能(就像在 Python 中隐藏内置函数一样),感觉有点脏...

您是否有解决此问题的方法,或者我们是否必须等待 2.9.6 中的 previtem/nextitem?
我的一些 saltstack 模板现在坏了。

正如上面不同程度的证明,你甚至可能不需要做你正在做的事情。 否则,是的,如果要使用 2.9,则需要等待。 以前从未支持过,它只是碰巧起作用。

我们不会恢复到旧的行为。 虽然它在简单的情况下工作,但它不正确并且从未被记录为支持。 虽然我知道这是一个重大更改,但它发生在功能版本(第二个数字更改)中,这是我们一直管理这些更改的方式。 如果您需要继续依赖旧行为,请在发布修复程序之前固定版本。

锁定这个是因为该说的都说了。 有关当前正在考虑的修复程序,请参阅 #676 和 #684。

此页面是否有帮助?
0 / 5 - 0 等级