Jinja: 2.9 регрессия при присвоении переменной внутри цикла

Созданный на 7 янв. 2017  ·  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://github.com/kennethlove/alex-gaynor-blog-design/blob/551172/templates/archive.html#L13 -L24

Самый полезный комментарий

changed_from_last звучит хорошо - по крайней мере, с точки зрения функциональности, само название немного неудобно, ИМО. Может быть, достаточно всего 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] '

Большой. Потому что проблема здесь в том, что это абсолютно ненормально. Какую переменную предполагается переопределить? Что делать, если между ними есть область видимости функции. например: макрос или что-то в этом роде. Я не знаю, как это поддержать сейчас.

хм, может быть, похоже на область видимости Python при условии, что не использовалось nonlocal )? т.е. не разрешать переопределение чего-либо, определенного во внешней области (например, если между ними есть макрос), но разрешить ли переопределение по-другому?

По-видимому, до этого была обнаружена ошибка утверждения шаблона:

>>> 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' без)

Несвязанная локальная ошибка должна быть устранена на мастере.

Я не уверен, как здесь прогрессировать. Мне действительно не нравится, что такие вещи, по-видимому, можно было делать.

Последними изменениями я собираюсь закрыть это, так как «работает как задумано». Если это приведет к дальнейшим последствиям, мы можем снова исследовать альтернативы.

Попал в эту проблему (см. # 656) после того, как это изменение взорвало мой шаблон блога при обновлении с v.2.8.1 до v2.9.4.

Я использовал его, чтобы отслеживать, менялись ли различные фрагменты данных между итерацией цикла. Мне удалось это исправить, потому что я написал исходный код шаблона (см. Https://github.com/MinchinWeb/seafoam/commit/8eb760816a06e4f0382816f82586204d1e7734fd и https://github.com/MinchinWeb/seafoam/124d4d6da555) но сомневаюсь, что мог бы иначе. За новым кодом труднее следить, поскольку сравнения теперь выполняются в оперативном режиме. Например, мой старый код (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 %}

Так что я просто хотел сказать , что «особенность» (или «взломать», если вы предпочитаете) используется и уже упущены.

Если проблемы с определением объема слишком сложны, чтобы их можно было осознать в данный момент, можно ли что-то (как минимум) добавить в журнал изменений, чтобы это укусило меньше людей, не подозревающих об этом?

Я не знал, что этим так широко злоупотребляют: - / Досадно, что на самом деле нет никакого надежного способа заставить эту работу работать. Однако мне интересно, можем ли мы выделить здесь общие варианты использования и представить более удобный 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 звучит хорошо - по крайней мере, с точки зрения функциональности, само название немного неудобно, ИМО. Может быть, достаточно всего 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 не имеют собственной области видимости и так далее. Хотя @mituhiko говорит, что в

Для программистов на Python поведение из первого сообщения (Jinja 2.8) очевидно. То же самое относится и к https://github.com/pallets/jinja/issues/660 , я не понимаю, почему библиотека Python должна реализовывать поведение javascript (я понимаю, что обработка аргументов по умолчанию в Python не идеальна, но любой разработчик Python знает из него).

В качестве альтернативы должен быть документ, описывающий, как работает область видимости в jinja, чтобы нам не приходилось гадать.

Также, пожалуйста, не воспринимайте мой комментарий отрицательно, я очень благодарен Джиндзя.

@roganov несколько заметок по этому

Может кто-нибудь пояснить, почему области jinja2 не могут работать точно так же, как работает python

Потому что область видимости Python, на мой взгляд, очень плохая, особенно в шаблонах, потому что вы можете легко случайно переопределить важную информацию.

Хотя @mituhiko говорит, что в

Обратите внимание, что это было исключением из-за ошибки, и это сработало только в некоторых ограниченных ситуациях и чисто случайно. У 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 уже выполняет работу с областью видимости в Jinja, когда используется, например, как {% with %} . OTOH, здесь все будет наоборот, поскольку с блоками открывается новая область видимости, а не доступны переменные внешней области видимости.

Не уверен, нужна ли эта функция Jinja или нет.

Никакой синтаксис не изменит этого, потому что он все равно нарушит правила области видимости. Вы не можете сделать это ни с текущей системой компиляции, ни с системой отслеживания идентификаторов. Даже если это сработает для этого простого случая, что произойдет, если у вас есть цикл for, равный recursive . Что, если кто-то определит макрос в цикле for. Что делать, если в цикле for появляется блок вызова.

Я думаю, что было бы разумнее ввести глобальный объект, который действует как пространство имен хранилища и затем может быть изменен с помощью. Например:

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

Однако для этого потребуется фундаментальное изменение тега set .

@mituhiko, и такой шаблон уже используется с тегом do : http://stackoverflow.com/a/4880398/400617

FWIW, решение с do чрезвычайно ужасно. Точно так же, как обходной путь, который люди используют в Python 2, когда им нужно nonlocal ...

Я полностью согласен, просто указываю, что это где-то там. И, как заметил Алекс, многие из этих проблем можно переписать либо с помощью фильтров, либо путем помещения некоторой логики в Python.

Другой (уродливый) прием - использовать список, добавить и всплыть: http://stackoverflow.com/a/32700975/4276230

Привет, я использую этот код около 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 .

Привет,
Я понимаю, что из-за этого есть много "уродливых" примеров, но не могли бы вы посоветовать, как элегантно / правильно увеличивать переменную в цикле for в шаблоне jinja? Потому что мой код тоже был взломан.

Зачем вообще это нужно делать? Также укажите свой код.

{% 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 -%}

не могли бы вы посоветовать, как элегантно / правильно увеличить переменную в цикле for в шаблоне jinja?

Вы никогда не должны были этого делать (или уметь делать). Так что ответ - не делай этого. Однако нас интересует, почему авторы шаблонов, похоже, в этом нуждаются.

Джинджа уже перечисляет цикл. {{ loop.index0 }}

@davidism мне нужны только те, которые соответствуют условию

Напишите фильтр, который будет выдавать кортежи devnum, sensor именно так, как они вам нужны. Или рассчитайте его на Python и передайте в шаблон. Или воспользуйтесь приведенным ниже примером. Это касается всех других примеров, которые я видел.

@Molodax вы можете сделать это:

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

разве вы не имели в виду |count ?

Извини, да, посчитай.

@mituhiko , спасибо за поддержку, к сожалению, не работает.
Возможно, 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 %}

Я не знаю, как бы я сделал это без глобальной переменной «count», на которую я могу ссылаться в двух отдельных циклах. Есть ли у вас какие-либо предложения, которые позволят этому работать как под 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
last1-age1
last2-age2
last3-age3
last4-age4
last5-age5
last6-age6

Из того, что я собрал, карта не работает с несколькими атрибутами # 554
Я использую jinja2 через ansible, поэтому предварительное добавление чего-либо в python не является хорошей идеей для меня.

@ pujan14 @aabdnn @Molodax этот шаблон

@davidism шаблон, который я представил выше, не

Если это официально не задокументировано, я более склонен закрыть это, как это было изначально. Я повторяю, что Ansible может больше помочь вам в этом отношении.

@davidism Я .
Вот пример.

{% 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) к объекту цикла? Тогда люди могут хранить все, что захотят, в цикле.

Приснилась та же идея, но потом не удалось найти не уродливых случаев, где это было бы необходимо. И я уже мог видеть, как кто-то просит setdefault , pop и другие dict-подобные методы.

@davidism @ThiefMaster Я думал о том, чтобы просто иметь доступный объект хранения. Нравится:

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

Очевидно, что set не может устанавливать атрибуты в данный момент, но это можно легко расширить. Поскольку переназначения самому пространству имен не происходит, это вполне безопасно реализовать.

Мне кажется, это то же самое решение, что и nonlocal для Py 2. В качестве альтернативы можно автоматически настроить loop.ns , хотя это не будет доступно вне цикла.

Что мне не нравится в пространстве имен, так это то, что будет очень заманчиво сделать {% set obj.attr = 42 %} с obj чем-то, что не является 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), это выглядит немного грязным ...

Есть ли у вас способ решения этой проблемы или нам нужно дождаться previtem / nextitem в 2.9.6?
Некоторые из моих шаблонов солончаков уже не работают.

Как в разной степени продемонстрировано выше, вам может даже не понадобиться делать то, что вы делаете. В противном случае да, вам нужно подождать, если вы хотите использовать 2.9. Раньше он никогда не поддерживался, просто так получилось.

Мы не вернемся к старому поведению. Хотя это работало в простых случаях, это было некорректно и никогда не документировалось как поддерживаемое. Хотя я понимаю, что это критическое изменение, оно произошло в выпуске функции (изменение второго номера), и именно так мы всегда справлялись с этими изменениями. Закрепите версию до тех пор, пока не будет выпущено исправление, если вам нужно продолжать полагаться на старое поведение.

Блокировка, потому что все, что нужно сказать, уже было сказано. См. # 676 и # 684 для исправлений, которые в настоящее время рассматриваются.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги