perimasu
perimasu

Reputation: 1095

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.

Upvotes: 9

Views: 14136

Answers (3)

ciis0
ciis0

Reputation: 503

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

Upvotes: 0

pvilas
pvilas

Reputation: 1377

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>

Upvotes: 0

7yl4r
7yl4r

Reputation: 5348

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.

Upvotes: 8

Related Questions