在本机模式下渲染对常量值执行两次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()
应该在最后执行一个评估。 在我看来,这似乎是防止像这样的奇怪的双重评估问题的唯一理智的方法。
此更改将不再需要native_concat
中的preserve_quotes
解决方法。 这似乎是解决此类问题的先前尝试。
以这种方式修复它也将解决底层root_render_func
api 中的相同问题:
>>> 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可以继续审查它会很有帮助。
我确实记得在处理preserve_quotes
问题时查看了为什么中间步骤正在执行literal_eval
,但不记得我得出了什么结论。 当时我想我把它留在原地,因为我认为它可能会对原生类型如何通过渲染产生影响。
作为旁注,我最初试图实现类似于 ansible 的东西。 我想要做的确切的事情是“当且仅当模板只有一个表达式时才返回本机类型”。 (我想我会使用原生的root_render_function
,如果有一个节点返回它,否则做一个普通的模板concat
。)
我使用@mkrizek的补丁创建了 PR #1190。
据我所知,这不会对任何事情产生负面影响。 在中间组上运行native_concat
将返回本机类型,但这会在后续组中连接为字符串,或者它将成为最终节点并通过native_concat
无论如何。
刚刚发布了 2.11.2。
@Qhesz @davidism谢谢!
此更改至少破坏了一些有效的 ansible 脚本:
我有一个带有以下循环的脚本:
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 开发分支重现该问题。 我敢打赌是 Ansible 和 Jinja2 版本的组合导致了您的问题。 你介意在 ansible/ansible 中提交一个问题,以便我们可以在那里解决它吗? 谢谢!
您必须确保在 ansible 配置中启用了jinja2_native = true
(这不是默认设置)。
尽管如此,我将尝试编写一个最小的剧本并在 ansible 中提交一个问题。