Jinja: NativeEnv 评估常量两次

创建于 2020-04-09  ·  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
  • Jinja 版本:2.11.1

建议的解决方案

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

所有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可以继续审查它会很有帮助。

我确实记得在处理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 中提交一个问题。

此页面是否有帮助?
0 / 5 - 0 等级