简体   繁体   中英

Jinja2: render template inside template

Is it possible to render a Jinja2 template inside another template given by a string? For example, I want the string

{{ s1 }}

to be rendered to

Hello world

given the following dictionary as a param for Template.render :

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

I know the similar process can be done with include tag separating the content of s1 to the another file, but here I don't want to follow that way.

I do not have an environment to easily test these ideas, but am exploring something similar within airflow's use of jinja templates.

From what I can find the best way to do this is do explicitly render the inner template string within the outer template. To do this you may need to pass or import the Template constructor in the param dictionary.

Here is some (untested) code:

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
)

This is not nearly as clean as you were hoping for, so we may be able to take things further by creating a custom filter so we can use it like this:

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

Here is a custom filter I think will do the job:

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

Now let's assume we want the same context as the outer template, and - what the heck - lets render an arbitrary number of levels deep, N . Hopefully some example usages will look like:

{{ s1|recursive_render }}

{{ s3|recursive_render(2) }}

An easy way to get the context from our custom filter is to use the contextfilter decorator

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)

Now you can do something like s3 = '{{ s1 }}!!!'and {{ s3|recursive_render(2) }} should render to Hello world!!! . I suppose you could go even deeper and detect how many levels to render by counting brackets.


Having gone through all this I would like to explicitly point out that this is very confusing .

Although I do believe I have found a need for 2 levels of rendering within my very specific airflow usage, I cannot imagine a need for more levels than that.

If you are reading this thinking "this is just what I need": Whatever you are trying to do can probably be done more eloquently. Take a step back, consider that you may have an xy problem , and re-read jinja's docs to be sure there isn't a better way.

Well, you can always create a filter like:

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

so if

s1="Hello {{s2}}"

you can filter from the template as:

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

You can use the low-level Jinja API for this, stolen from Ansible core.

#!/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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM