Jinja2:在模板内部渲染模板

8

能否在一个给定的字符串模板中呈现Jinja2模板?例如,我想要这个字符串:

{{ s1 }}

渲染为
Hello world

假设以下字典作为Template.render的参数:

{ 's1': 'Hello {{ s2 }}', 's2': 'world' }

我知道可以使用include标签将s1的内容分离到另一个文件中,但我不想采用这种方式。


s2 怎么知道它的替换来自于那个字典呢?类似的问题是,如果那是 Hello {{ s1 }} 呢? - OneCricketeer
@cricket_007 我承认似乎没有办法知道这个。这也解释了为什么目前的机制无法完成此操作。 - perimasu
你可能可以循环遍历该字典的值,并使用该字典本身进行渲染,但在这个简单的示例之外,它会变得混乱。 - OneCricketeer
3个回答

8

我没有一个简单测试这些想法的环境,但正在探索类似于airflow使用jinja模板的东西。

从我找到的信息来看,最好的方法是在外部模板中显式地渲染内部模板字符串。为此,您可能需要在参数字典中传递或导入模板构造函数

这里是一些(未经测试的)代码:

from jinja2 import Template
template_string = '{{ Template(s1).render(s2=s2) }}'
outer_template = Template(template_string)
outer_template.render( 
    s1='Hello {{ s2 }}', 
    s2='world',
    Template=Template
)

这并不像您所希望的那样简洁,因此我们可以通过创建自定义过滤器来进一步完善它,以便我们可以像这样使用它:

{{ s1|inner_render({"s2":s2}) }}

这是我认为可以完成任务的自定义过滤器:

from jinja2 import Template
def inner_render(value, context):
    return Template(value).render(context)

现在假设我们想要与外部模板相同的上下文,并且 - 让我们渲染任意深度N。希望一些示例用法看起来像:
{{ s1|recursive_render }}

{{ s3|recursive_render(2) }}

通过使用contextfilter装饰器,可以轻松地从我们的自定义过滤器中获取上下文。

from jinja2 import Template
from jinja2 import contextfilter
@contextfilter
def recursive_render(context, value, N=1):
    if N == 1:
        val_to_render = value
    else:
        val_to_render = recursive_render(context, value, N-1)
    return Template(value).render(context)

现在你可以这样做:s3 = '{{ s1 }}!!!'{{ s3|recursive_render(2) }}应该渲染为Hello world!!!。我想你甚至可以更深入地检测要渲染多少级,通过计算括号数来实现。

经历了这一切,我希望明确指出这非常令人困惑

虽然我相信我在我的特定airflow使用中找到了需要2个级别的渲染,但我无法想象需要超过那个级别的情况。

如果你正在阅读这篇文章,并认为“这正是我需要的”:无论你试图做什么,可能都可以更优雅地完成。退后一步,考虑您可能有一个xy问题,并重新阅读jinja的文档以确定是否有更好的方法。


0

你可以使用从Ansible核心中窃取的低级Jinja API来实现这一点。

#!/usr/bin/env python3

# Stolen from Ansible, thus licensed under GPLv3+.

from collections.abc import Mapping
from jinja2 import Template

# https://github.com/ansible/ansible/blob/13c28664ae0817068386b893858f4f6daa702052/lib/ansible/template/vars.py#L33
class CustomVars(Mapping):
    '''
    Helper class to template all variable content before jinja2 sees it. This is
    done by hijacking the variable storage that jinja2 uses, and overriding __contains__
    and __getitem__ to look like a dict.
    '''

    def __init__(self, templar, data):
        self._data = data
        self._templar = templar

    def __contains__(self, k):
        return k in self._data

    def __iter__(self):
        keys = set()
        keys.update(self._data)
        return iter(keys)

    def __len__(self):
        keys = set()
        keys.update(self._data)
        return len(keys)

    def __getitem__(self, varname):
        variable = self._data[varname]
        return self._templar.template(variable)

# https://github.com/ansible/ansible/blob/13c28664ae0817068386b893858f4f6daa702052/lib/ansible/template/__init__.py#L661
class Templar:

    def __init__(self, data):

        self._data = data

    def template(self, variable):

        '''
        Assume string for now.
        TODO: add isinstance checks for sequence, mapping.
        '''

        t = Template(variable)
        ctx = t.new_context(CustomVars(self, self._data), shared=True) # shared=True is important, not quite sure yet, why.
        rf = t.root_render_func(ctx)

        return "".join(rf)

t_str = "{{ s1 }}"
data = { 's1': 'Hello {{ s2 }}', 's2': 'world' }

t = Templar(data)
print("template result: %s" % t.template(t_str))

template result: Hello world

0

嗯,你总是可以创建一个过滤器,比如:

@app.template_filter('t')
def trenderiza(value, obj):
  rtemplate = Environment(loader=BaseLoader()).from_string(value)
  return rtemplate.render(**obj)

所以如果

s1="Hello {{s2}}"

你可以从模板中进行过滤,如下:

 <p>{{s1|t(dict(s2='world')}}</p>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接