Jinja: 2.9 régression lors de l'affectation d'une variable à l'intérieur d'une boucle

Créé le 7 janv. 2017  ·  65Commentaires  ·  Source: 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'

Rapporté à l'origine sur IRC :

Un changement dans la portée de jinja2 semble m'affecter, et je ne suis pas sûr de la bonne solution. Plus précisément, le problème est l'affectation de l'année ici : https://github.com/kennethlove/alex-gaynor-blog-design/blob/551172/templates/archive.html#L13 -L24

Commentaire le plus utile

changed_from_last sonne bien - au moins en termes de fonctionnalité, le nom lui-même est un peu maladroit IMO. Peut-être que juste changed serait assez clair ?

Je suppose que le premier élément serait toujours considéré comme « changé », peu importe ce que c'est ? Si ce comportement n'est pas correct pour quelqu'un, il peut toujours ajouter un not loop.first toute façon.

Tous les 65 commentaires

Bien que le comportement actuel soit incorrect, l'ancien comportement est également définitivement incorrect. La balise {% set %} n'a jamais été conçue pour remplacer les étendues externes. Je suis surpris que ce soit le cas dans certains cas. Je ne sais pas quoi faire ici.

Le comportement que je suppose être correct est une sortie comme [-1:0] [-1:1] [-1:2] [-1:3] [-1:4] -1 .

Dans ce cas spécifique, je l'ai résolu en le réécrivant pour utiliser groupby .

J'ai l'impression que c'est un cas d'utilisation assez courant - aussi des trucs comme {% set found = true %} dans une boucle, puis le vérifier par la suite. C'est sûrement susceptible de casser des choses pour les gens quand cela cesse de fonctionner...

Je suis choqué que cela ait fonctionné avant. Cela a-t-il toujours fonctionné ?

apparemment oui :

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

Super. Parce que le problème ici est que ce n'est absolument pas solide. Quelle variable est-il censé écraser ? Que faire s'il y a une portée de fonction entre les deux. par exemple : une macro ou quelque chose comme ça. Je n'ai aucune idée de comment soutenir cela maintenant.

hmm peut-être similaire à la portée de python en supposant qu'aucun nonlocal n'a été utilisé) ? c'est-à-dire ne pas autoriser le remplacement de quelque chose défini dans une portée externe (c'est-à-dire s'il y a une macro entre les deux) mais permet-il de le remplacer autrement?

Apparemment, avant cela, une erreur d'assertion de modèle a été détectée :

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

le comportement semble être différent avec/sans récursif (2.8.1 me donne une erreur UnboundLocalError avec récursif et '012' sans)

L'erreur locale non liée doit être résolue sur le maître.

Je ne sais pas comment progresser ici. Je n'aime vraiment pas qu'il soit apparemment possible de faire ce genre de chose.

Avec les derniers changements, je vais fermer ceci comme "fonctionne comme prévu". S'il y a d'autres retombées, nous pouvons à nouveau étudier des alternatives.

J'ai été envoyé à ce problème (voir #656) après que ce changement ait fait exploser mon modèle de blog lors de la mise à niveau de v.2.8.1 à v2.9.4.

Je l'utilisais pour suivre si diverses données changeaient entre les itérations de la boucle. J'ai pu le réparer car j'ai écrit le code du modèle d'origine (voir https://github.com/MinchinWeb/seafoam/commit/8eb760816a06e4f0382816f82586204d1e7734fd et https://github.com/MinchinWeb/seafoam/commit/89d555dbd62)a2f25647091d2694e4184, mais si je doute que j'aurais pu le faire autrement. Le nouveau code est plus difficile à suivre car les comparaisons se font désormais en ligne. Par exemple, mon ancien code (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 %}

et le nouveau code, avec des comparaisons en ligne (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 %}

Je voulais donc juste dire que la "fonctionnalité" (ou "hack", si vous préférez) est utilisée et est déjà ratée.

Si les problèmes de portée sont trop complexes pour être cernés de manière raisonnable pour le moment, pourrait-on (au minimum) ajouter quelque chose au journal des

Je ne savais pas que cela était si largement abusé :-/ Malheureusement, il n'y a vraiment aucun moyen de faire fonctionner cela de manière fiable. Cependant, je me demande si nous pouvons isoler les cas d'utilisation courants ici et introduire une API plus agréable.

En particulier, nous voulons peut-être ajouter quelque chose comme ceci ( 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 sonne bien - au moins en termes de fonctionnalité, le nom lui-même est un peu maladroit IMO. Peut-être que juste changed serait assez clair ?

Je suppose que le premier élément serait toujours considéré comme « changé », peu importe ce que c'est ? Si ce comportement n'est pas correct pour quelqu'un, il peut toujours ajouter un not loop.first toute façon.

Peut-être que previous_context ne contient que l'intégralité du contexte précédent, plutôt que d'essayer de décider ce que les utilisateurs en feront.

juste previous ou prev ? "contexte" semble plutôt déroutant dans ce contexte (sans jeu de mots)

.....et maintenant je peux déjà imaginer quelqu'un demander un next :/

Ou peut-être une déclaration explicite pour écrire en dehors de la portée : set_outer ou quelque chose.

je pensais à ça :

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, nous ne pouvons pas faire set_outer sans casser tout le système de cadrage. Cela gâcherait le tout.

Oui, j'ai pensé que ce serait le cas. J'anticipe "et si je veux savoir si la valeur actuelle est supérieure à la précédente", ou quelque chose de similaire. Mais j'aime aussi la méthode changed .

En tant que point de données supplémentaire, il m'est tombé sur les pieds pour un cas où je dois injecter une classe spéciale dans le code HTML généré, mais uniquement pour le premier élément de la liste itérée qui n'a pas également de propriétés spécifiques. Je ne peux pas modifier les données itérées. Je ne peux pas utiliser loop.first (autant que j'aimerais) ou comparer avec quoi que ce soit du dernier élément pour faire de manière fiable ce que je dois faire ici, ce qui a conduit à expérimenter la construction maléfique qui a parfaitement fonctionné (et il n'était pas clair pour moi que c'était en fait moi qui abusais d'un bug).

De plus, j'offre des capacités d'extension via des plugins tiers et je n'ai aucun moyen de contrôler la façon dont les auteurs structurent leurs modèles, provoquant une rupture soudaine pour les utilisateurs finaux après la mise à jour vers Jinja 2.9+. J'ai épinglé la dernière version 2.8 dans mon programme d'ailleurs pour rester rétrocompatible (comme le promet mon schéma de gestion des versions), jusqu'à ce que je puisse trouver un moyen d'amener les auteurs à mettre à jour leurs modèles.

Quelqu'un pourrait-il expliquer pourquoi les portées jinja2 ne peuvent pas fonctionner exactement comme Python fonctionne? c'est-à-dire que les modèles jinja2 correspondent aux modules python, les macros correspondent aux fonctions (et ont leur propre portée), les boucles for n'ont pas leur propre portée et ainsi de suite. Alors que @mitsuhiko dit que jinja 2.8 et les

Pour les programmeurs python, le comportement du premier post (Jinja 2.8) est évident. La même chose s'applique à https://github.com/pallets/jinja/issues/660 , je ne comprends pas pourquoi la bibliothèque python devrait implémenter le comportement javascript (je comprends que la gestion par python des arguments par défaut n'est pas idéale, mais tout développeur python est au courant de celui-ci).

Alternativement, il devrait y avoir un document qui décrit comment la portée dans jinja fonctionne afin que nous n'ayons pas à deviner.

Aussi s'il vous plaît ne prenez pas mon commentaire négativement, je suis très reconnaissant pour jinja.

@roganov quelques notes à ce sujet :

Quelqu'un pourrait-il expliquer pourquoi les étendues jinja2 ne peuvent pas fonctionner exactement comme python fonctionne

Parce que la portée de Python est à mon avis vraiment mauvaise, en particulier dans les modèles, car vous pouvez facilement écraser accidentellement des informations importantes.

Alors que @mitsuhiko dit que jinja 2.8 et les

Notez qu'il s'agissait d'une exception due à un bogue et que cela n'a fonctionné que dans certaines situations limitées et purement par accident. Jinja avait toujours les mêmes règles de cadrage documentées et ce comportement n'a jamais été documenté. À tout moment, il a été dit que les variables ne se propagent pas aux étendues externes. Vous pouvez facilement le voir car après la fin de la boucle, la variable n'a jamais été définie.

Alternativement, il devrait y avoir un document qui décrit comment la portée dans jinja fonctionne afin que nous n'ayons pas à deviner.

J'ai déjà amélioré la doc il y a quelques jours pour ça

Et s'il y avait un moyen de passer explicitement une variable dans une boucle ? Peut-être une syntaxe comme celle-ci (empruntant à des filtres de texte) :

{%- 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-t-il un moyen de faire quelque chose de similaire à scoped tel qu'il s'applique actuellement aux macros ?


Un autre cas d'utilisation serait de maintenir une sorte de compteur entre les boucles. Par exemple, combien de lignes au total ? combien de lignes répondent à certains critères ? Le total de certaines propriétés pour les lignes sélectionnées (pour imprimer une ligne totale) ?

La syntaxe de filtre ne serait pas possible ici car les filtres sont déjà possibles sur l'itérable.

Une syntaxe possible pour cela qui n'aurait pas l'air si mauvaise pourrait être celle-ci :

{% for article in dates with last_day %}

Surtout que with déjà des choses de portée dans Jinja lorsqu'il est utilisé par exemple comme {% with %} . OTOH, ici ce serait l'inverse puisqu'avec des blocs ouvrir une nouvelle portée, ne pas rendre les vars de portée extérieure accessibles.

Je ne sais pas si c'est une fonctionnalité dont Jinja a besoin ou non.

Aucune syntaxe ne changera cela car elle subvertirait toujours les règles de portée. Vous ne pouvez pas le faire avec la compilation actuelle ou le système de suivi des identifiants. Même si cela fonctionne pour ce cas simple, que se passe-t-il si l'on a une boucle for qui est recursive . Que faire si quelqu'un définit une macro dans une boucle for. Que faire si un bloc d'appel apparaît dans une boucle for.

Je pense qu'il serait plus logique d'introduire un objet global qui agit comme un espace de noms de stockage et peut ensuite être modifié avec . Par exemple:

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

Cependant, cela nécessiterait que la balise set change fondamentalement.

@mitsuhiko et ce type de modèle est déjà utilisé avec la balise do : http://stackoverflow.com/a/4880398/400617

FWIW, la solution avec do est extrêmement horrible. Tout comme la solution de contournement que les gens utilisent dans Python 2 lorsqu'ils ont besoin de nonlocal ...

Je suis tout à fait d'accord, je signale simplement qu'il existe. Et comme Alex l'a souligné, beaucoup de ces problèmes peuvent être réécrits, soit avec des filtres, soit en mettant une partie de la logique en Python.

Un autre hack (laide) consiste à utiliser une liste, à ajouter et à afficher : http://stackoverflow.com/a/32700975/4276230

Salut, j'utilise ce code depuis environ 2 ans, maintenant il casse et à partir de la nouvelle documentation et de cette discussion, je ne pense pas qu'il y ait d'autre moyen de faire ce que j'essaie de faire ici. Si ce n'est pas le cas, corrigez-moi.

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

Bien que unique ne soit pas un filtre intégré, vous pouvez toujours ajouter un autre filtre qui fait tout, puisque vous ajoutez déjà le filtre unique .

Salut,
Je comprends qu'il y a beaucoup d'exemples "moches" à cause de cela, mais pourriez-vous s'il vous plaît indiquer comment incrémenter une variable sur une boucle for dans le modèle jinja de manière élégante / correcte? Parce que mon code a été cassé aussi.

Pourquoi avez-vous besoin de le faire? Veuillez également inclure votre code.

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

pourriez-vous s'il vous plaît conseiller comment incrémenter une variable sur une boucle for dans le modèle jinja de manière élégante / correcte?

Vous n'étiez jamais censé faire (ou être capable de faire) cela du tout. Donc la réponse est de ne pas faire ça. Nous sommes cependant intéressés par la raison pour laquelle les auteurs de modèles semblent avoir besoin de le faire.

Jinja énumère déjà la boucle. {{ loop.index0 }}

@davidism je n'ai besoin que de ceux qui remplissent une condition

Écrivez un filtre qui produit des tuples devnum, sensor exactement comme vous en avez besoin. Ou calculez-le en Python et transmettez-le au modèle. Ou utilisez l'exemple ci-dessous. Cela vaut pour tous les autres exemples que j'ai vu.

@Molodax vous pouvez faire ceci :

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

tu ne voulais pas dire |count ?

Désolé oui, compte.

@mitsuhiko , merci pour votre soutien, malheureusement, cela ne fonctionne pas.
Peut-être que jinja2 (filtres) a été implémenté avec quelques limitations dans un projet qui utilise ( Home assistant ). Mais cela fonctionnait avec le code que j'avais fourni auparavant. Pitié.

Salut les gens. Vous demandez un exemple de code qui fonctionne sous 2.8, alors voici le mien :

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

Je ne sais pas comment je ferais sans une variable globale "count" que je peux référencer dans 2 boucles distinctes. Avez-vous des suggestions qui permettront à cela de fonctionner à la fois sous 2.8 et 2.9 ?

@davidism Votre solution est excellente mais j'essaie d'y parvenir.
Créez 2 listes comme suit

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

puis fusionnez-les dans list3, ce qui devrait ressembler à ce qui suit et enfin à appliquer unique (filtre ansible) sur list3
dernier1-âge1
last2-age2
dernier3-âge3
dernier4-âge4
dernier5-âge5
dernier6-âge6

D'après ce que j'ai compris, la carte ne fonctionne pas avec plusieurs attributs #554
J'utilise jinja2 via ansible et donc ajouter quelque chose en python au préalable n'est pas une bonne idée pour moi.

@pujan14 @aabdnn @Molodax est-ce que ce modèle provient de la documentation officielle d'Ansible, ou est-ce quelque chose que vous avez trouvé ou trouvé ailleurs? Quoi qu'il en soit, il pourrait être plus facile de signaler cela à Ansible, car ils comprennent comment leur produit doit être utilisé et pourraient éventuellement proposer des solutions plus pertinentes.

@davidism le modèle que j'ai présenté ci-dessus ne provient d'aucune documentation Ansible. Ansible ne documente pas spécifiquement Jinja2. Je viens de créer ce modèle moi-même en lisant la documentation Jinja2, et cela a fonctionné, je l'ai donc mis en production. J'ai supposé que Jinja2 rendait les variables globales.

Si ce n'est pas officiellement documenté, je suis plus enclin à le fermer comme il l'était à l'origine. Je répète qu'Ansible pourra peut-être vous aider davantage à cet égard.

@davidism J'ai joué avec des boucles dans le nouveau jinja 2.9 .
Voici un exemple.

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

Ce n'est peut-être pas la meilleure façon de le faire, mais ici, d'après ma compréhension, je n'enfreins aucune règle de portée.

Avait ce problème en 2.8 et supérieur

Voici un cas de test :

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

Utilisez plutôt loop.index .

Je pense que fournir l'accès à la valeur de boucle précédente via un attribut de l'objet loop est la seule bonne solution pour cela. Je viens de découvrir cet extrait dans notre projet qui n'a pas pu être résolu en vérifiant simplement si le dernier objet est différent de l'actuel et groupby ne fonctionne pas non plus là-bas car c'est plus qu'un simple accès à un élément/attribut pour obtenir la clé :

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

Ouais ça sonne comme une idée.

Qu'en est-il de l'ajout des méthodes set(key, value) et get(key) à l'objet boucle ? Ensuite, les gens peuvent stocker ce qu'ils veulent dans la boucle.

J'ai eu la même idée, mais je n'ai pas pu trouver de cas non laids où cela serait nécessaire. Et je pouvais déjà voir quelqu'un demander setdefault , pop et d'autres méthodes de type dict.

@davidism @ThiefMaster je pensais juste avoir un objet de stockage disponible. Comme ça:

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

De toute évidence, set ne peut pas définir d'attributs pour le moment, mais cela pourrait facilement être étendu. Étant donné qu'aucune réaffectation à l'espace de noms lui-même ne se produit, la mise en œuvre est assez sûre.

Cela me semble bien, c'est la même solution que nonlocal pour Py 2. Sinon, configurez automatiquement loop.ns , bien que cela ne soit pas disponible en dehors de la boucle.

Ce que je n'aime pas avec l'espace de noms, c'est qu'il sera très tentant de faire {% set obj.attr = 42 %} avec obj étant quelque chose qui n'est pas un namespace() - quelque chose que je pense ne devrait pas fonctionner .

Sinon, cela semble être une idée intéressante, même si je pense que previtem / nextitem / changed() couvrent bien les cas "simples" sans le "bruit" d'avoir à définir un nouvel objet dans le modèle.

@ThiefMaster cela ne fonctionnerait pas. La façon dont je verrais cela fonctionner est que les attributions d'attributs passent par un rappel sur l'environnement qui ne permettrait que des modifications aux objets de l'espace de noms.

ok, donc au moins aucun risque que les modèles provoquent des effets secondaires inattendus sur les objets qui leur sont transmis.

encore un peu méfiant des choses que les gens pourraient faire...

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

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

En fait, je pense qu'un nouveau bloc comme {% namespace ns %} serait mieux pour en définir un qu'un appelable - une variable nommée namespace ne semble pas être quelque chose de très improbable à passer à un modèle, et bien que cela vous empêcherait probablement simplement d'utiliser la fonctionnalité d'espace de noms dans ce modèle (tout comme l'ombrage d'une fonction intégrée en Python), cela semble un peu sale ...

Avez-vous une solution à ce problème ou devons-nous attendre previtem/nexitem dans 2.9.6 ?
Certains de mes modèles de saltstack sont cassés maintenant.

Comme cela a été démontré à des degrés divers ci-dessus, vous n'aurez peut-être même pas besoin de faire ce que vous faites. Sinon, oui, vous devez attendre si vous souhaitez utiliser la version 2.9. Cela n'avait jamais été pris en charge auparavant, cela fonctionnait simplement.

Nous ne reviendrons pas à l'ancien comportement. Bien que cela ait fonctionné dans des cas simples, ce n'était pas correct et n'a jamais été documenté comme pris en charge. Bien que je comprenne qu'il s'agit d'un changement décisif, il s'est produit dans une version de fonctionnalité (deuxième changement de numéro), c'est ainsi que nous avons toujours géré ces changements. Épinglez la version jusqu'à ce qu'un correctif soit publié si vous devez continuer à vous fier à l'ancien comportement.

Verrouiller ceci parce que tout ce qui doit être dit a été dit. Voir #676 et #684 pour les correctifs actuellement envisagés.

Cette page vous a été utile?
0 / 5 - 0 notes