Jinja: 2.9 regressão ao atribuir uma variável dentro de um loop

Criado em 7 jan. 2017  ·  65Comentários  ·  Fonte: 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'

Reportado originalmente no IRC:

Uma mudança no escopo do jinja2 parece me afetar e não tenho certeza da correção correta. Especificamente, o problema é a atribuição do ano aqui: https://github.com/kennethlove/alex-gaynor-blog-design/blob/551172/templates/archive.html#L13 -L24

Comentários muito úteis

changed_from_last soa bem - pelo menos em termos de funcionalidade, o nome em si é um pouco estranho IMO. Talvez apenas changed seja claro o suficiente?

Eu acho que o primeiro elemento sempre seria considerado "alterado", não importa o que seja. Se esse comportamento não for bom para alguém, eles podem sempre adicionar um cheque not loop.first qualquer maneira.

Todos 65 comentários

Embora o comportamento atual esteja incorreto, o comportamento antigo também está definitivamente incorreto. A tag {% set %} nunca teve a intenção de substituir escopos externos. Estou surpreso que sim em alguns casos. Não tenho certeza do que fazer aqui.

O comportamento que presumo que esteja correto é uma saída como [-1:0] [-1:1] [-1:2] [-1:3] [-1:4] -1 .

Neste caso específico, resolvi reescrevendo-o para usar groupby .

Tenho a sensação de que este é um caso de uso comum - também coisas como {% set found = true %} em um loop e verificá-lo posteriormente. É certamente provável que as coisas parem de funcionar para as pessoas ...

Estou chocado que isso funcionou antes. Isso sempre funcionou?

aparentemente sim:

>>> 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] '

Excelente. Porque o problema aqui é que isso não é absolutamente correto. Qual variável deve ser substituída? E se houver um escopo de função no meio. por exemplo: uma macro ou algo parecido. Não tenho ideia de como apoiar isso agora.

hmm talvez semelhante ao escopo do python assumindo que nenhum nonlocal foi usado)? ou seja, não permite sobrescrever algo definido em um escopo externo (ou seja, se houver uma macro no meio), mas permite sobrescrevê-lo de outra forma?

Aparentemente, antes que isso fosse detectado com um erro de declaração de modelo:

>>> 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)

o comportamento parece ser diferente com / sem recursivo (2.8.1 me dá um erro UnboundLocalError com recursivo e '012' sem)

O erro local não acoplado deve ser resolvido no mestre.

Não tenho certeza de como progredir aqui. Eu realmente não gosto que seja aparentemente possível fazer esse tipo de coisa.

Com as últimas alterações, fecharei isso como "funciona conforme planejado". Se houver mais consequências disso, podemos investigar alternativas novamente.

Fui enviado para este problema (consulte # 656) depois que essa mudança explodiu meu modelo de blog na atualização de v.2.8.1 para v2.9.4.

Eu estava usando para monitorar se várias partes de dados estavam mudando entre a iteração do loop. Consegui consertá-lo porque escrevi o código do modelo original (consulte https://github.com/MinchinWeb/seafoam/commit/8eb760816a06e4f0382816f82586204d1e7734fd e https://github.com/MinchinWeb/seafoam/commit/89d55e4f0382816f82586204d1e7734fd e https://github.com/MinchinWeb/seafoam/commit/89d55e2dbd6a2f25471e49518e43e29549518e49518e29518e295471). duvido que eu seria capaz de outra forma. O novo código é mais difícil de seguir, pois as comparações agora são feitas em linha. Por exemplo, meu código antigo (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 %}

e o novo código, com comparações in-line (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 %}

Então, eu só queria dizer que o "recurso" (ou "hack", se você preferir) foi usado e já foi esquecido.

Se as questões de escopo forem muito complexas para serem descobertas de forma sensata no momento, algo poderia (no mínimo) ser adicionado ao changelog para que menos pessoas inconscientes percebam?

Eu não sabia que isso era tão amplamente abusado: - / Infelizmente, não há realmente nenhuma maneira de fazer isso funcionar de maneira confiável. No entanto, eu me pergunto se podemos isolar os casos de uso comuns aqui e apresentar uma API melhor.

Em particular, talvez queiramos adicionar algo assim ( 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 soa bem - pelo menos em termos de funcionalidade, o nome em si é um pouco estranho IMO. Talvez apenas changed seja claro o suficiente?

Eu acho que o primeiro elemento sempre seria considerado "alterado", não importa o que seja. Se esse comportamento não for bom para alguém, eles podem sempre adicionar um cheque not loop.first qualquer maneira.

Talvez previous_context apenas contenha todo o contexto anterior, ao invés de tentar decidir o que os usuários farão com ele.

apenas previous ou prev ? "contexto" soa um tanto confuso neste contexto (sem trocadilhos)

..... e agora já posso imaginar alguém pedindo um next : /

Ou talvez uma declaração explícita para escrever fora do escopo: set_outer ou algo assim.

eu estava pensando isso:

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 não podemos fazer set_outer sem quebrar todo o sistema de escopo. Isso iria estragar tudo.

Sim, imaginei que seria o caso. Estou antecipando "e se eu quiser saber se o valor atual é maior do que o anterior", ou algo semelhante. Mas também gosto do método changed .

Como um ponto de dados adicional, caí no meu pé para um caso em que preciso injetar uma classe especial no HTML gerado, mas apenas para o primeiro elemento da lista iterada que também não tem algum conjunto de propriedades específico. Não consigo modificar os dados iterados. Não posso usar loop.first (tanto quanto adoraria) ou comparar com qualquer coisa do último elemento para fazer o que preciso fazer aqui de maneira confiável, o que me levou a experimentar a construção do mal que funcionou perfeitamente (e não estava claro para mim se eu estava realmente abusando de um bug).

Além disso, ofereço recursos de extensão por meio de plug-ins de terceiros e não tenho como policiar como os autores estruturam seus modelos, causando quebra repentina para os usuários finais após a atualização para Jinja 2.9+. Eu fixei a última versão 2.8 agora em meu programa para permanecer compatível com as versões anteriores (como meu esquema de versionamento promete), até que eu possa encontrar uma maneira de fazer com que os autores atualizem seus modelos.

Alguém poderia esclarecer por que escopos jinja2 não funcionam exatamente como o python funciona? ou seja, modelos jinja2 correspondem a módulos python, macros correspondem a funções (e têm seu próprio escopo), loops for não têm seu próprio escopo e assim por diante. Embora @mitsuhiko diga que o jinja 2.8 e

Para programadores de python, o comportamento da primeira postagem (Jinja 2.8) é óbvio. O mesmo se aplica a https://github.com/pallets/jinja/issues/660 , não entendo por que a biblioteca python deve implementar o comportamento javascript (entendo que o manuseio de args padrão pelo python não é ideal, mas qualquer desenvolvedor de python está ciente disso).

Como alternativa, deve haver um documento que descreve como o escopo no jinja funciona para que não tenhamos que adivinhar.

Além disso, por favor, não tome meu comentário negativamente, estou muito grato por jinja.

@roganov algumas notas sobre isso:

Alguém poderia esclarecer por que escopos jinja2 não funcionam exatamente como o python funciona

Porque o escopo do Python é, na minha opinião, muito ruim, particularmente em modelos, porque você pode facilmente substituir informações importantes acidentalmente.

Embora @mitsuhiko diga que o jinja 2.8 e

Observe que esta foi uma exceção devido a um bug e só funcionou em algumas situações limitadas e puramente por acidente. Jinja sempre teve as mesmas regras de escopo documentadas e esse comportamento nunca foi documentado. Sempre foi dito que as variáveis ​​não se propagam para escopos externos. Você pode ver isso facilmente porque após o final do loop, a variável nunca foi definida.

Como alternativa, deve haver um documento que descreve como o escopo no jinja funciona para que não tenhamos que adivinhar.

Eu melhorei os documentos há alguns dias para isso

E se houvesse uma maneira de passar explicitamente uma variável em um loop? Talvez sintaxe como esta (emprestando de filtros de texto):

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

Ou existe uma maneira de fazer algo semelhante a scoped conforme se aplica atualmente a macros?


Outro caso de uso seria manter algum tipo de contador entre os loops. Por exemplo, quantas linhas no total? quantas linhas atendem a alguns critérios? O total de alguma propriedade para as linhas selecionadas (para imprimir uma linha total)?

A sintaxe de filtro não seria possível aqui, pois os filtros já são possíveis no iterável.

Uma possível sintaxe para isso que não pareceria tão ruim seria esta:

{% for article in dates with last_day %}

Especialmente porque with já faz o escopo no Jinja quando usado, por exemplo, como {% with %} . OTOH, aqui seria o oposto, pois com blocos abrir um novo escopo, não tornar o escopo externo vars acessível.

Não tenho certeza se esse é um recurso de que o Jinja precisa ou não.

Nenhuma sintaxe mudará isso porque ainda subverteria as regras de escopo. Você não pode fazer isso com a compilação atual ou sistema de rastreamento de id. Mesmo que funcione nesse caso simples, o que acontece se alguém tiver um loop for recursive . E se alguém definir uma macro em um loop for. E se um bloco de chamada aparecer em um loop for.

Acho que faria mais sentido introduzir um objeto global que atua como um namespace de armazenamento e pode ser modificado com. Por exemplo:

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

No entanto, isso exigiria que a tag set mudasse fundamentalmente.

@mitsuhiko e esse tipo de padrão já está em uso com a tag do : http://stackoverflow.com/a/4880398/400617

FWIW, a solução com do é extremamente terrível. Assim como a solução alternativa que as pessoas usam no Python 2 quando precisam de nonlocal ...

Eu concordo totalmente, apenas observando que está lá fora. E, como Alex apontou, muitos desses problemas podem ser reescritos, seja com filtros ou colocando parte da lógica em Python.

Outro hack (feio) é usar uma lista, anexar e pop: http://stackoverflow.com/a/32700975/4276230

Olá, tenho usado este código há cerca de 2 anos, agora ele quebra e com a nova documentação e essa discussão, acho que não há outra maneira de fazer o que estou tentando fazer aqui. Caso contrário, corrija-me.

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

Embora unique não seja um filtro embutido, você sempre pode adicionar outro filtro que faça a coisa toda, visto que você já está adicionando o filtro unique .

Oi,
Eu entendo que há muitos exemplos "feios" devido a isso, mas você poderia aconselhar como incrementar uma variável em um loop for no modelo jinja de maneira elegante / correta? Porque meu código também foi quebrado.

Por que você precisa fazer isso? Além disso, inclua seu código.

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

você poderia aconselhar como incrementar uma variável em um loop for no modelo jinja de maneira elegante / correta?

Você nunca deveria fazer (ou ser capaz de fazer) isso. Portanto, a resposta é não faça isso. No entanto, estamos interessados ​​em por que os autores de modelos parecem ter a necessidade de fazer isso.

Jinja já enumera o loop. {{ loop.index0 }}

@davidism , preciso apenas daqueles que atendem a uma condição

Escreva um filtro que produza devnum, sensor tuplas exatamente do jeito que você precisa delas. Ou calcule em Python e passe para o template. Ou use o exemplo abaixo. Isso vale para todos os outros exemplos que já vi.

@Molodax você pode fazer isso:

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

você não quis dizer |count ?

Desculpe, sim, conte.

@mitsuhiko , obrigado pelo apoio, infelizmente não funciona.
Talvez, jinja2 (filtros) tenha sido implementado com algumas limitações em um projeto que usa ( Assistente doméstico ). Mas funcionou com o código fornecido por mim antes. Pena.

Oi pessoal. Você está pedindo um código de exemplo que funcione em 2.8, então aqui está o meu:

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

Não sei como faria isso sem uma variável "contagem" global à qual posso fazer referência em 2 loops separados. Você tem alguma sugestão que permitirá que isso funcione em 2.8 e 2.9?

@davidism Sua solução é ótima, mas estou tentando conseguir isso.
Crie 2 listas como a seguir

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

e, em seguida, mesclá-los na lista3, que deve ser semelhante a seguir e, finalmente, aplicar o único (filtro ansible) na lista3
last1-age1
last2-age2
last3-age3
last4-age4
últimos 5 anos
últimos 6 anos

Pelo que recolhi, o mapa não funciona com vários atributos # 554
Estou usando o jinja2 via ansible e, portanto, adicionar fazer algo em python de antemão não é uma boa ideia para mim.

@ pujan14 @aabdnn @Molodax esse padrão veio da documentação oficial da Ansible ou é algo que você inventou ou encontrou em outro lugar? De qualquer forma, pode ser mais fácil relatar isso para a Ansible, uma vez que eles entendem como seu produto deve ser usado e podem apresentar soluções mais relevantes.

@davidism o modelo que apresentei acima não veio de nenhuma documentação do Ansible. Ansible não documenta Jinja2 especificamente. Eu acabei de criar este modelo lendo a documentação do Jinja2 e funcionou, então eu o coloquei em produção. Presumi que o Jinja2 tornava as variáveis ​​globais.

Se não estiver documentado oficialmente, estou mais inclinado a encerrar como estava originalmente. Vou reiterar que o Ansible pode ajudá-lo mais nesse sentido.

@davidism Eu brinquei com loops no novo jinja 2.9 .
Aqui está um exemplo.

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

Esta pode não ser a melhor maneira de fazer isso, mas pelo meu entendimento, não estou quebrando nenhuma regra de escopo.

Tive esse problema no 2.8 e superior

Aqui vai um caso de teste:

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()

Use loop.index vez disso.

Acho que fornecer acesso ao valor do loop anterior por meio de um atributo do objeto loop é a única boa solução para isso. Acabei de descobrir este trecho em nosso projeto que não poderia ser resolvido apenas verificando se o último objeto é diferente do atual e groupby também não funciona lá, pois é mais do que apenas um item trivial / acesso de atributo para obter a chave :

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

Sim, parece uma ideia.

Que tal adicionar os métodos set(key, value) e get(key) ao objeto de loop? Assim, as pessoas podem armazenar o que quiserem no loop.

Teve a mesma ideia, mas não conseguiu encontrar nenhum caso não feio em que isso fosse necessário. E eu já pude ver alguém pedindo setdefault , pop e outros métodos do tipo dict.

@davidism @ThiefMaster eu estava pensando em apenas ter um objeto de armazenamento disponível. Assim:

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

Obviamente, set não pode definir atributos no momento, mas isso poderia ser facilmente estendido. Uma vez que nenhuma reatribuição ao namespace em si acontece, isso é bastante seguro de implementar.

Parece bom para mim, é a mesma solução que nonlocal para Py 2. Alternativamente, configure loop.ns automaticamente, embora isso não esteja disponível fora do loop.

O que eu não gosto com o namespace é que vai parecer muito tentador fazer {% set obj.attr = 42 %} com obj sendo algo que não é namespace() - algo que eu acho que não deveria funcionar .

Caso contrário, parece uma ideia interessante, embora eu ache que previtem / nextitem / changed() cubra casos "simples" sem o "ruído" de ter que definir um novo objeto no modelo.

@ThiefMaster não funcionaria. A forma como eu veria isso funcionando é que as atribuições de atributo passam por um retorno de chamada no ambiente que só permitiria modificações em objetos de namespace.

ok, pelo menos não há risco de os modelos causarem efeitos colaterais inesperados nos objetos passados ​​para eles.

ainda um pouco desconfiado das coisas que as pessoas podem fazer ...

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

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

Na verdade, acho que um novo bloco como {% namespace ns %} seria melhor definir um do que um chamável - uma variável chamada namespace não parece algo muito improvável de ser passado para um modelo, e enquanto provavelmente, simplesmente impediria você de usar o recurso de namespace nesse modelo (assim como fazer sombra em um Python embutido), parece um pouco sujo ...

Você tem uma solução alternativa para esse problema ou temos que esperar o previtem / nextitem em 2.9.6?
Alguns dos meus modelos de saltstack estão quebrados agora.

Como foi demonstrado em vários graus acima, talvez você nem precise fazer o que está fazendo. Caso contrário, sim, você precisa esperar se quiser usar o 2.9. Nunca foi apoiado antes, simplesmente funcionou.

Não estaremos voltando ao antigo comportamento. Embora funcionasse em casos simples, não era correto e nunca foi documentado como compatível. Embora eu entenda que é uma alteração significativa, ocorreu em um lançamento de recurso (alteração do segundo número) que é como sempre gerenciamos essas alterações. Fixe a versão até que uma correção seja lançada se precisar continuar contando com o comportamento antigo.

Bloqueando isso porque tudo o que precisa ser dito foi dito. Veja # 676 e # 684 para as correções que estão sendo consideradas.

Esta página foi útil?
0 / 5 - 0 avaliações