Рендеринг в собственном режиме дважды выполняет 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
до того, как весь шаблон будет отрисован с динамическими данными. Шаблон оценивается второй раз во время рендеринга, что означает, что постоянное значение было оценено дважды.
Во втором случае весь шаблон является постоянным и оценивается за один раз во время компиляции, что приводит к разным результатам.
В третьем случае весь шаблон является динамическим и оценивается за один раз во время рендеринга.
_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]
Это очень интересно. Я протестировал предложенное решение [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.