Jinja模板中允许使用内联代码吗?

19 投票
3 回答
32996 浏览
提问于 2025-04-17 03:01

我在我的网站上使用Jinja,觉得它很好用。

我遇到了一个简单的问题。怎么显示今天的日期呢? 有没有办法在Jinja模板中直接写一些Python代码?

import datetime
now = datetime.datetime.utcnow()
print now.strftime("%Y-%m-%d %H:%M")

这篇文章说不可以,但建议使用宏或者过滤器?

真的需要这样做吗?好吧,那在这种情况下应该怎么做呢?

3 个回答

3

你可以往全局变量里添加内容,这样在Jinja模板中就能访问这些变量了。你可以把自己需要的函数定义放进去,这样它们就能执行你想要的操作。

5

目前的回答在大多数情况下都是正确的。不过,有一些非常少见的情况,你可能会想在模板里写一些python代码。在我的例子中,我想用它来处理一些latex文件,并且我希望把生成表格值、图表等的python代码放在latex文件里面。

所以我做了一个Jinja2的扩展,增加了一个新的“py”块,这样就可以在模板里写python代码了。请注意,为了让这个功能正常工作,我做了一些不太靠谱的变通方法,所以我不太确定在哪些情况下它会出问题或者表现得不如预期。

这是一个示例模板。

Foo was given to the template
foo: {{ foo }}

Bar was not, so it is missing
bar is missing: {{ bar == missing }}

{% py %}
    # Normal python code in here
    # Excess indentation will be removed.
    # All template variables are accessible and can be modified.
    import numpy as np
    a = np.array([1, 2])
    m = np.array([[3, 4], [5, 6]])
    bar = m @ a * foo

    # It's also possible to template the python code.
    {% if change_foo %}
    foo = 'new foo value'
    {% endif %}

    print("Stdio is redirected to the output.")
{% endpy %}

Foo will have the new value if you set change_foo to True
foo: {{ foo }}

Bar will now have a value.
bar: {{ bar }}

{% py %}
    # The locals from previous blocks are accessible.
    m = m**2
{% endpy %}
m:
{{ m }}

如果我们把模板参数设置为 foo=10, change_foo=True,输出结果是:

Foo was given to the template
foo: 10

Bar was not, so it is missing
bar is missing: True

Stdio is redirected to the output.


Foo will have the new value if you set change_foo to True
foo: new foo value

Bar will now have a value.
bar: [110 170]


m:
[[ 9 16]
 [25 36]]

这个扩展包含一个主函数,用来运行这个示例。

from jinja2 import Environment, PackageLoader, nodes
from jinja2.ext import Extension
from textwrap import dedent
from io import StringIO
import sys
import re
import ctypes


def main():
    env = Environment(
        loader=PackageLoader('python_spike', 'templates'),
        extensions=[PythonExtension]
    )

    template = env.get_template('emb_py2.txt')
    print(template.render(foo=10, change_foo=True))


var_name_regex = re.compile(r"l_(\d+)_(.+)")


class PythonExtension(Extension):
    # a set of names that trigger the extension.
    tags = {'py'}

    def __init__(self, environment: Environment):
        super().__init__(environment)

    def parse(self, parser):
        lineno = next(parser.stream).lineno
        body = parser.parse_statements(['name:endpy'], drop_needle=True)
        return nodes.CallBlock(self.call_method('_exec_python',
                                                [nodes.ContextReference(), nodes.Const(lineno), nodes.Const(parser.filename)]),
                               [], [], body).set_lineno(lineno)

    def _exec_python(self, ctx, lineno, filename, caller):
        # Remove access indentation
        code = dedent(caller())

        # Compile the code.
        compiled_code = compile("\n"*(lineno-1) + code, filename, "exec")

        # Create string io to capture stdio and replace it.
        sout = StringIO()
        stdout = sys.stdout
        sys.stdout = sout

        try:
            # Execute the code with the context parents as global and context vars and locals.
            exec(compiled_code, ctx.parent, ctx.vars)
        except Exception:
            raise
        finally:
            # Restore stdout whether the code crashed or not.
            sys.stdout = stdout

        # Get a set of all names in the code.
        code_names = set(compiled_code.co_names)

        # The the frame in the jinja generated python code.
        caller_frame = sys._getframe(2)

        # Loop through all the locals.
        for local_var_name in caller_frame.f_locals:
            # Look for variables matching the template variable regex.
            match = re.match(var_name_regex, local_var_name)
            if match:
                # Get the variable name.
                var_name = match.group(2)

                # If the variable's name appears in the code and is in the locals.
                if (var_name in code_names) and (var_name in ctx.vars):
                    # Copy the value to the frame's locals.
                    caller_frame.f_locals[local_var_name] = ctx.vars[var_name]
                    # Do some ctypes vodo to make sure the frame locals are actually updated.
                    ctx.exported_vars.add(var_name)
                    ctypes.pythonapi.PyFrame_LocalsToFast(
                        ctypes.py_object(caller_frame),
                        ctypes.c_int(1))

        # Return the captured text.
        return sout.getvalue()

if __name__ == "__main__":
    main()
13

不,不能把Python代码直接写进Jinja里。不过,你可以通过扩展模板引擎的环境或者所有模板都能用的全局命名空间来增加Jinja能识别的内容。另外,你还可以添加一个过滤器,让你可以格式化日期时间对象。

Flask把Jinja2的环境存储在app.jinja_env里。你可以通过直接向这个字典添加内容,或者使用@app.context_processor装饰器来向环境中注入新的上下文。

无论你选择哪种方式,这些操作都应该在你设置应用的时候完成,也就是在处理任何请求之前。(可以查看网站的代码片段部分,里面有一些不错的 例子,教你如何设置过滤器 - 文档中也有一个很好的例子,讲的是如何添加全局变量)。

撰写回答