Jinja: 2.9 regresión al asignar una variable dentro de un bucle

Creado en 7 ene. 2017  ·  65Comentarios  ·  Fuente: 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 en IRC:

Un cambio en el alcance de jinja2 parece afectarme y no estoy seguro de la solución correcta. Específicamente, el problema es la asignación del año aquí: https://github.com/kennethlove/alex-gaynor-blog-design/blob/551172/templates/archive.html#L13 -L24

Comentario más útil

changed_from_last suena bien, al menos en cuanto a funcionalidad, el nombre en sí es un poco incómodo en mi opinión. ¿Quizás solo changed sería lo suficientemente claro?

Supongo que el primer elemento siempre se considerará "cambiado" sin importar cuál sea. Si ese comportamiento no está bien para alguien, siempre puede agregar un cheque not loop.first todos modos.

Todos 65 comentarios

Si bien el comportamiento actual es incorrecto, el comportamiento anterior definitivamente también es incorrecto. La etiqueta {% set %} nunca tuvo la intención de anular los ámbitos externos. Me sorprende que lo haya hecho en algunos casos. No estoy seguro de qué hacer aquí.

El comportamiento que asumiría que es correcto es una salida como [-1:0] [-1:1] [-1:2] [-1:3] [-1:4] -1 .

En este caso específico, lo resolví reescribiéndolo para usar groupby .

Tengo la sensación de que este es un caso de uso algo común, también cosas como {% set found = true %} en un bucle y luego verificarlo después. Seguramente es probable que las personas se rompan cuando esto deja de funcionar ...

Me sorprende que esto haya funcionado antes. ¿Esto siempre funcionó?

aparentemente sí:

>>> 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 el problema aquí es que esto no es en absoluto correcto. ¿Qué variable se supone que debe anular? ¿Qué pasa si hay un ámbito de función en el medio? por ejemplo: una macro o algo así. No tengo idea de cómo apoyar esto ahora.

hmm tal vez similar al alcance de Python suponiendo que no se usó nonlocal )? es decir, no permite anular algo definido en un ámbito externo (es decir, si hay una macro en el medio) pero ¿permite anularlo de otra forma?

Aparentemente, antes de que esto se detectara con un error de afirmación de plantilla:

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

el comportamiento parece ser diferente con / sin recursivo (2.8.1 me da un error UnboundLocalError con recursivo y '012' sin)

El error local independiente debe resolverse en el maestro.

No estoy seguro de cómo progresar aquí. Realmente me disgusta que aparentemente fuera posible hacer este tipo de cosas.

Con los últimos cambios voy a cerrar esto como "funciona según lo previsto". Si hay más consecuencias de esto, podemos investigar alternativas nuevamente.

Me enviaron a este problema (ver # 656) después de que este cambio hiciera explotar la plantilla de mi blog al actualizar de la v.2.8.1 a la v2.9.4.

Lo estaba usando para realizar un seguimiento si varios datos cambiaban entre la iteración del bucle. Pude solucionarlo porque escribí el código de plantilla original (ver https://github.com/MinchinWeb/seafoam/commit/8eb760816a06e4f0382816f82586204d1e7734fd y https://github.com/MinchinWeb/seafoam/commit/89d555dbd6a2f95e12469), Dudo que hubiera podido hacerlo de otra manera. El nuevo código es más difícil de seguir ya que las comparaciones ahora se realizan en línea. Por ejemplo, mi código anterior (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 %}

y el nuevo código, con comparaciones en línea (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 %}

Así que solo quería decir que la "función" (o "truco", si lo prefiere) se usa y ya se perdió.

Si los problemas de alcance son demasiado complejos para resolverlos con sensatez en este momento, ¿podría agregarse algo (como mínimo) al registro de cambios para que menos gente se dé cuenta?

No sabía que se abusa tanto de esto: - / Es molesto que realmente no haya forma de hacer que esto funcione de manera confiable. Sin embargo, me pregunto si podemos aislar los casos de uso comunes aquí e introducir una API más agradable.

En particular, tal vez queramos agregar algo como esto ( 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 suena bien, al menos en cuanto a funcionalidad, el nombre en sí es un poco incómodo en mi opinión. ¿Quizás solo changed sería lo suficientemente claro?

Supongo que el primer elemento siempre se considerará "cambiado" sin importar cuál sea. Si ese comportamiento no está bien para alguien, siempre puede agregar un cheque not loop.first todos modos.

Tal vez previous_context solo contenga todo el contexto anterior, en lugar de intentar decidir qué harán los usuarios con él.

solo previous o prev ? "contexto" suena bastante confuso en este contexto (sin juego de palabras)

..... y ahora ya puedo imaginarme a alguien pidiendo un next : /

O tal vez una declaración explícita para escribir fuera del alcance: set_outer o algo así.

estaba pensando esto:

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 no podemos hacer set_outer sin romper todo el sistema de alcance. Esto estropearía todo el asunto.

Sí, pensé que ese sería el caso. Estoy anticipando "qué pasa si quiero saber si el valor actual es mayor que el anterior", o algo similar. Pero también me gusta el método changed .

Como un punto de datos adicional, cayó en los pies de un caso en el que necesito para inyectar una clase especial en el HTML generado, pero sólo para el primer elemento de la lista iterada que no tenga loop.first (tanto como me encantaría) o comparar con cualquier cosa del último elemento para hacer de manera confiable lo que necesito hacer aquí, lo que me llevó a experimentar con la construcción maligna que funcionó perfectamente (y no me quedó claro si en realidad era yo quien estaba abusando de un error).

Además, ofrezco capacidades de extensión a través de complementos de terceros y no tengo forma de controlar cómo los autores estructuran sus plantillas, lo que provoca una rotura repentina para los usuarios finales después de la actualización a Jinja 2.9+. He anclado la última versión 2.8 ahora en mi programa para que siga siendo compatible con versiones anteriores (como promete mi esquema de control de versiones), hasta que pueda encontrar una manera de hacer que los autores actualicen sus plantillas.

¿Podría alguien aclarar por qué los alcances jinja2 no pueden funcionar exactamente como funciona Python? es decir, las plantillas jinja2 corresponden a módulos de Python, las macros corresponden a funciones (y tienen su propio alcance), los bucles no tienen su propio alcance y así sucesivamente. Si bien @mitsuhiko dice que jinja 2.8 y

Para los programadores de Python, el comportamiento de la primera publicación (Jinja 2.8) es obvio. Lo mismo se aplica a https://github.com/pallets/jinja/issues/660 , no entiendo por qué la biblioteca de Python debería implementar el comportamiento de JavaScript (entiendo que el manejo de Python de los argumentos predeterminados no es ideal, pero cualquier desarrollador de Python lo sabe de ella).

Alternativamente, debería haber un documento que describa cómo funciona el alcance en jinja para que no tengamos que adivinar.

Además, no tome mi comentario negativamente, estoy muy agradecido por jinja.

@roganov algunas notas sobre esto:

¿Podría alguien aclarar por qué los alcances jinja2 no pueden funcionar exactamente como funciona Python?

Porque, en mi opinión, el alcance de Python es realmente malo, especialmente en las plantillas, ya que puede anular fácilmente información importante por accidente.

Si bien @mitsuhiko dice que jinja 2.8 y

Tenga en cuenta que esta fue la excepción debido a un error y solo funcionó en algunas situaciones limitadas y puramente por accidente. Jinja siempre tuvo las mismas reglas de alcance documentadas y este comportamiento nunca fue documentado. En todo momento se dijo que las variables no se propagan a ámbitos externos. Puede ver esto fácilmente porque después del final del ciclo, la variable nunca se estableció.

Alternativamente, debería haber un documento que describa cómo funciona el alcance en jinja para que no tengamos que adivinar.

Mejoré los documentos hace unos días para esto.

¿Y si hubiera una forma de pasar explícitamente una variable a un bucle? Quizás una sintaxis como esta (tomando prestado de los 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 %}

¿O hay alguna forma de hacer algo similar a scoped como se aplica actualmente a las macros?


Otro caso de uso sería mantener algún tipo de contador entre bucles. Por ejemplo, ¿cuántas filas en total? ¿Cuántas filas cumplen con algunos criterios? ¿El total de alguna propiedad para las filas seleccionadas (para imprimir una fila total)?

La sintaxis de filtro no sería posible aquí, ya que los filtros ya son posibles en el iterable.

Una posible sintaxis para esto que no se vería mal podría ser esta:

{% for article in dates with last_day %}

Especialmente porque with ya hace cosas de alcance en Jinja cuando se usa, por ejemplo, como {% with %} . OTOH, aquí sería todo lo contrario ya que con los bloques se abre un nuevo alcance, no se hacen accesibles las vars del alcance externo.

Sin embargo, no estoy seguro de si esa es una característica que Jinja necesita o no.

Ninguna sintaxis cambiará esto porque todavía subvertiría las reglas de alcance. No puede hacer eso ni con la compilación actual ni con el sistema de seguimiento de identificación. Incluso si funciona para ese caso simple, ¿qué sucede si uno tiene un bucle for que es recursive ? ¿Qué pasa si alguien define una macro en un bucle for? ¿Qué sucede si aparece un bloque de llamadas en un bucle for?

Creo que tendría más sentido introducir un objeto global que actúe como un espacio de nombres de almacenamiento y luego se pueda modificar con. P.ej:

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

Sin embargo, esto requeriría que la etiqueta set cambie fundamentalmente.

@mitsuhiko y ese tipo de patrón ya está en uso con la etiqueta do : http://stackoverflow.com/a/4880398/400617

FWIW, la solución con do es extremadamente terrible. Al igual que la solución alternativa que la gente usa en Python 2 cuando necesitan nonlocal ...

Estoy completamente de acuerdo, solo señalo que está ahí fuera. Y como señaló Alex, muchos de estos problemas se pueden reescribir, ya sea con filtros o poniendo algo de lógica en Python.

Otro truco (feo) es usar una lista, agregar y hacer pop: http://stackoverflow.com/a/32700975/4276230

Hola, he estado usando este código desde hace aproximadamente 2 años, ahora se rompe y, a partir de la nueva documentación y esta discusión, no creo que haya otra forma de hacer lo que estoy tratando de hacer aquí. Si no es así, por favor, corríjanme.

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

Aunque como unique no es un filtro incorporado, siempre puede agregar otro filtro que lo haga todo, ya que ya está agregando el filtro unique .

Hola,
Entiendo que hay muchos ejemplos "feos" debido a esto, pero ¿podría aconsejarme cómo incrementar una variable en un bucle for en la plantilla jinja de una manera elegante / correcta? Porque mi código también se ha roto.

¿Por qué necesitas hacerlo? Además, incluya su 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 -%}

¿Podría aconsejarme cómo incrementar una variable en un bucle for en la plantilla jinja de manera elegante / correcta?

Se suponía que nunca debías hacer (o ser capaz de hacer) esto en absoluto. Entonces la respuesta es no hagas eso. Sin embargo, estamos interesados ​​en por qué los autores de plantillas parecen tener la necesidad de hacer eso.

Jinja ya enumera el bucle. {{ loop.index0 }}

@davidismo solo necesito aquellos que cumplen una condición

Escriba un filtro que produzca devnum, sensor tuplas exactamente como las necesita. O calcularlo en Python y pasarlo a la plantilla. O use el siguiente ejemplo. Esto se aplica a todos los demás ejemplos que he visto.

@Molodax puedes hacer esto:

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

¿no te refieres a |count ?

Lo siento, sí, cuenta.

@mitsuhiko , gracias por tu apoyo, desafortunadamente, no funciona.
Quizás, jinja2 (filtros) se ha implementado con algunas limitaciones en un proyecto que usa ( Home Assistant ). Pero funcionó con el código proporcionado por mí antes. Pena.

Hola amigos. Estás pidiendo un código de ejemplo que funcione en 2.8, así que aquí está el mío:

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

No sé cómo haría esto sin una variable de "recuento" global a la que pueda hacer referencia en 2 ciclos separados. ¿Tiene alguna sugerencia que permita que esto funcione tanto en 2.8 como en 2.9?

@davidism Tu solución es excelente, pero estoy tratando de lograrlo.
Crea 2 listas de la siguiente manera

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

y luego fusionarlos en list3, que debería verse como seguir y finalmente aplicar único (filtro ansible) en list3
last1-age1
last2-age2
last3-age3
last4-age4
last5-age5
last6-age6

Por lo que reuní, el mapa no funciona con múltiples atributos # 554
Estoy usando jinja2 a través de ansible, por lo que agregar algo en Python de antemano no es una buena idea para mí.

@ pujan14 @aabdnn @Molodax ¿Este patrón proviene de la documentación oficial de Ansible, o es algo que se le ocurrió o encontró en otro lugar? De cualquier manera, podría ser más fácil informar esto a Ansible, ya que comprenden cómo se debe usar su producto y posiblemente podrían encontrar soluciones más relevantes.

@davidism la plantilla que presenté anteriormente no provino de ninguna documentación de Ansible. Ansible no documenta Jinja2 específicamente. Yo mismo creé esta plantilla leyendo la documentación de Jinja2 y funcionó, así que la puse en producción. Supuse que Jinja2 hizo que las variables fueran globales.

Si no está documentado oficialmente, entonces estoy más inclinado a cerrar esto como estaba originalmente. Reiteraré que Ansible puede ayudarlo más en este sentido.

@davidism Jugué con bucles en el nuevo jinja 2.9 .
Aquí hay un ejemplo.

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

Puede que esta no sea la mejor manera de hacerlo, pero según tengo entendido, no estoy rompiendo ninguna regla de alcance.

Tuve este problema en 2.8 y superior

Aquí va un caso de prueba:

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

Utilice loop.index lugar.

Creo que proporcionar acceso al valor del ciclo anterior a través de un atributo del objeto loop es la única buena solución para esto. Acabo de descubrir este fragmento en nuestro proyecto que no se pudo resolver simplemente verificando si el último objeto es diferente al actual y groupby tampoco funciona allí, ya que es más que un simple acceso a un elemento / atributo trivial para obtener la clave :

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

Sí, eso suena como una idea.

¿Qué hay de agregar los métodos set(key, value) y get(key) al objeto de bucle? Entonces las personas pueden almacenar lo que quieran a lo largo del circuito.

Tenía la misma idea, pero luego no pude encontrar ningún caso no feo en el que esto fuera necesario. Y ya pude ver a alguien pidiendo setdefault , pop y otros métodos similares a dict.

@davidism @ThiefMaster Estaba pensando en tener un objeto de almacenamiento disponible. Como esto:

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

Obviamente, set no puede establecer atributos en este momento, pero esto podría extenderse fácilmente. Dado que no ocurre ninguna reasignación al espacio de nombres en sí, esto es bastante seguro de implementar.

Me parece bien, es la misma solución que nonlocal para Py 2. Alternativamente, configure automáticamente loop.ns , aunque eso no estaría disponible fuera del ciclo.

Lo que no me gusta del espacio de nombres es que se sentirá muy tentador hacer {% set obj.attr = 42 %} con obj algo que no es un namespace() , algo que creo que no debería funcionar .

De lo contrario, parece una idea interesante, aunque creo que previtem / nextitem / changed() cubre casos "simples" sin el "ruido" de tener que definir un nuevo objeto en la plantilla.

@ThiefMaster no funcionaría. La forma en que vería que esto funciona es que las asignaciones de atributos pasan por una devolución de llamada en el entorno que solo permitiría modificaciones en los objetos del espacio de nombres.

ok, al menos no hay riesgo de que las plantillas causen efectos secundarios inesperados en los objetos que se les pasan.

todavía un poco cauteloso con las cosas que la gente podría hacer ...

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

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

En realidad, creo que un nuevo bloque como {% namespace ns %} sería mejor definir uno que un invocable: una variable llamada namespace no suena como algo muy poco probable que se pase a una plantilla, y while probablemente simplemente evitaría que use la función de espacio de nombres en esa plantilla (al igual que sombrear un incorporado en Python) se siente un poco sucio ...

¿Tiene una solución para este problema o tenemos que esperar a previtem / nextitem en 2.9.6?
Algunas de mis plantillas de pila de sal están rotas ahora.

Como se ha demostrado anteriormente en diversos grados, es posible que ni siquiera necesite hacer lo que está haciendo. De lo contrario, sí, debe esperar si desea utilizar 2.9. Nunca antes fue compatible, simplemente funcionó.

No volveremos al antiguo comportamiento. Si bien funcionó en casos simples, no fue correcto y nunca se documentó como compatible. Si bien entiendo que es un cambio importante, ocurrió en una versión de función (segundo cambio de número), que es la forma en que siempre manejamos estos cambios. Fije la versión hasta que se publique una solución si necesita seguir confiando en el comportamiento anterior.

Bloqueando esto porque se ha dicho todo lo que hay que decir. Consulte los n. ° 676 y n. ° 684 para ver las correcciones que se están considerando actualmente.

¿Fue útil esta página
0 / 5 - 0 calificaciones