Jinja: NativeEnv дважды оценивает константы

Созданный на 9 апр. 2020  ·  9Комментарии  ·  Источник: pallets/jinja

Рендеринг в собственном режиме дважды выполняет literal_eval для постоянных значений, что приводит к неожиданному поведению.

Ожидаемое поведение

```>>> из jinja2.nativetypes импортировать NativeEnvironment

NativeEnvironment().from_string(r'''0.000{{ a }}''').render({"a":7})
0,0007
NativeEnvironment().from_string(r'''0.000{{ 7 }}''').render({"a":7})
0,0007

Templates should behave the same way with constants as they do with dynamic variables.

Nothing should be eval'd before the entire template is finished.

## Actual Behavior
```>>> from jinja2.nativetypes import NativeEnvironment
>>> NativeEnvironment().from_string(r'''0.000{{ a }}''').render({"a":7})
0.07
>>> NativeEnvironment().from_string(r'''0.000{{ 7 }}''').render({"a":7})
0.0007
>>> NativeEnvironment().from_string(r'''{{b}}{{ a }}''').render({"a":7,"b":"0.000"})
0.0007

В первом случае константа 0.000 оценивается во время компиляции шаблона и усекается до 0.0 до того, как весь шаблон будет отрисован с динамическими данными. Шаблон оценивается второй раз во время рендеринга, что означает, что постоянное значение было оценено дважды.

Во втором случае весь шаблон является постоянным и оценивается за один раз во время компиляции, что приводит к разным результатам.

В третьем случае весь шаблон является динамическим и оценивается за один раз во время рендеринга.

Ваша среда

  • Версия Python: 3.7.6
  • Версия Джинджа: 2.11.1

Предлагаемое решение

_output_const_repr() не должен выполнять никаких оценок, только render() должен выполнять одну оценку в самом конце. Мне кажется, что это единственный разумный способ предотвратить странные проблемы с двойной оценкой, подобные этой.

С этим изменением обходной путь preserve_quotes в native_concat больше не понадобится. Похоже, это была предыдущая попытка исправить этот класс проблем.

Исправление этого таким образом также решит ту же проблему в базовом API root_render_func :

>>> t = NativeEnvironment().from_string(r'''{{ a }}''')
>>> list(t.root_render_func(t.new_context({"a":"7"}))) 
['7']
>>> t = NativeEnvironment().from_string(r'''{{"7"}}''')
>>> list(t.root_render_func(t.new_context({"a":"7"}))) 
[7]

Все 9 Комментарий

Это очень интересно. Я протестировал предложенное решение [0] (и, кроме того, удалил preserve_quotes [1]), и оно работает, и текущий набор тестов проходит с изменениями. Это имеет смысл для меня, но я не могу сказать, может ли это нарушить какой-либо допустимый вариант использования.

[0]

diff --git a/src/jinja2/nativetypes.py b/src/jinja2/nativetypes.py
index e0ad94d..4c89998 100644
--- a/src/jinja2/nativetypes.py
+++ b/src/jinja2/nativetypes.py
@@ -61,7 +61,7 @@ class NativeCodeGenerator(CodeGenerator):
         return value

     def _output_const_repr(self, group):
-        return repr(native_concat(group))
+        return repr("".join([str(v) for v in group]))

     def _output_child_to_const(self, node, frame, finalize):
         const = node.as_const(frame.eval_ctx)
diff --git a/tests/test_nativetypes.py b/tests/test_nativetypes.py
index 947168c..9da1b29 100644
--- a/tests/test_nativetypes.py
+++ b/tests/test_nativetypes.py
@@ -136,3 +136,11 @@ def test_concat_strings_with_quotes(env):
 def test_spontaneous_env():
     t = NativeTemplate("{{ true }}")
     assert isinstance(t.environment, NativeEnvironment)
+
+
+def test_1186(env):
+    from math import isclose
+    t = env.from_string("0.000{{ a }}")
+    result = t.render({"a":7})
+    assert isinstance(result, float)
+    assert isclose(result, 0.0007)

[1]

diff --git a/src/jinja2/nativetypes.py b/src/jinja2/nativetypes.py
index 4c89998..bba4f0a 100644
--- a/src/jinja2/nativetypes.py
+++ b/src/jinja2/nativetypes.py
@@ -1,4 +1,3 @@
-import types
 from ast import literal_eval
 from itertools import chain
 from itertools import islice
@@ -10,17 +9,14 @@ from .environment import Environment
 from .environment import Template


-def native_concat(nodes, preserve_quotes=True):
+def native_concat(nodes):
     """Return a native Python type from the list of compiled nodes. If
     the result is a single node, its value is returned. Otherwise, the
     nodes are concatenated as strings. If the result can be parsed with
     :func:`ast.literal_eval`, the parsed value is returned. Otherwise,
     the string is returned.

-    :param nodes: Iterable of nodes to concatenate.
-    :param preserve_quotes: Whether to re-wrap literal strings with
-        quotes, to preserve quotes around expressions for later parsing.
-        Should be ``False`` in :meth:`NativeEnvironment.render`.
+    :param nodes: Generator of nodes to concatenate.
     """
     head = list(islice(nodes, 2))

@@ -30,30 +26,17 @@ def native_concat(nodes, preserve_quotes=True):
     if len(head) == 1:
         raw = head[0]
     else:
-        if isinstance(nodes, types.GeneratorType):
-            nodes = chain(head, nodes)
-        raw = "".join([str(v) for v in nodes])
+        raw = "".join([str(v) for v in chain(head, nodes)])

     try:
-        literal = literal_eval(raw)
+        return literal_eval(raw)
     except (ValueError, SyntaxError, MemoryError):
         return raw

-    # If literal_eval returned a string, re-wrap with the original
-    # quote character to avoid dropping quotes between expression nodes.
-    # Without this, "'{{ a }}', '{{ b }}'" results in "a, b", but should
-    # be ('a', 'b').
-    if preserve_quotes and isinstance(literal, str):
-        quote = raw[0]
-        return f"{quote}{literal}{quote}"
-
-    return literal
-

 class NativeCodeGenerator(CodeGenerator):
     """A code generator which renders Python types by not adding
-    ``str()`` around output nodes, and using :func:`native_concat`
-    to convert complex strings back to Python types if possible.
+    ``str()`` around output nodes.
     """

     <strong i="11">@staticmethod</strong>
@@ -101,9 +84,7 @@ class NativeTemplate(Template):
         """
         vars = dict(*args, **kwargs)
         try:
-            return native_concat(
-                self.root_render_func(self.new_context(vars)), preserve_quotes=False
-            )
+            return native_concat(self.root_render_func(self.new_context(vars)))
         except Exception:
             return self.environment.handle_exception()

С удовольствием рассмотрю пиар. Эта функция появилась в Ansible, поэтому было бы полезно, если бы @jctanner и @mkrizek продолжили ее рассмотрение.

Я помню, как смотрел, почему промежуточные шаги делали literal_eval при работе над проблемой preserve_quotes , но не могу вспомнить, к какому выводу я пришел. В то время я думаю, что оставил его на месте, потому что подумал, что это может повлиять на то, как нативные типы проходят через рендеринг.

Кстати, изначально я пытался реализовать что-то похожее на ansible. То, к чему я стремился, было «вернуть собственный тип тогда и только тогда, когда в шаблоне было ровно одно выражение». (Я думал, что буду использовать собственный root_render_function , если есть один узел, верните его, в противном случае сделайте обычный шаблон concat .)

Я создал PR #1190 с патчем @mkrizek .

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

Только что выпустил 2.11.2 с этим.

@Qhesz @davidism Спасибо!

Это изменение ломает по крайней мере некоторые допустимые скрипты:

У меня есть скрипт со следующим циклом:

loop: "{{ [ 'dsa', 'rsa', 'ecdsa', 'ed25519' ] | product([ '', '.pub' ]) | list }}"

Раньше работало, а теперь вылетает с ошибкой:

fatal: FAILED! => {"msg": "Invalid data passed to 'loop', it requires a list, got this instead: [('dsa', ''), ('dsa', '.pub'), ('rsa', ''), ('rsa', '.pub'), ('ecdsa', ''), ('ecdsa', '.pub'), ('ed25519', ''), ('ed25519', '.pub')]. Hint: If you passed a list/dict of just one element, try adding wantlist=True to your lookup invocation or use q/query instead of lookup."}

Я думаю, это связано с тем, что механизм шаблонов теперь возвращает строку вместо списка методам concat.

@Jean-Daniel Спасибо, что сообщили нам! Я не могу воспроизвести проблему с основной веткой Jinja2 и веткой Ansible devel. Бьюсь об заклад, это комбинация версий Ansible и Jinja2, которая вызывает вашу проблему. Не могли бы вы зарегистрировать проблему в ansible/ansible, чтобы мы могли взять ее и разобраться там? Спасибо!

Вы должны убедиться, что jinja2_native = true включено в конфигурации ansible (что не является значением по умолчанию).

Тем не менее, я попытаюсь написать минимальную пьесу и отправить задачу в ansible.

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