Jinja2: отрисовка шаблона внутри шаблона

#python #jinja2

#python #jinja2

Вопрос:

Возможно ли визуализировать шаблон Jinja2 внутри другого шаблона, заданного строкой? Например, я хочу строку

 {{ s1 }}
  

для визуализации в

 Hello world
  

учитывая следующий словарь в качестве параметра для Template.render :

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

Я знаю, что аналогичный процесс можно выполнить с помощью include тега, разделяющего содержимое s1 другого файла, но здесь я не хочу следовать этому пути.

Комментарии:

1. Как s2 узнать, что его замена происходит из этого словаря? Аналогичный вопрос — что, если бы это было Hello {{ s1 }}

2. @cricket_007 Я признаю, что, похоже, нет способа узнать это. И это также объясняет, почему это невозможно сделать с помощью текущего механизма..

3. Возможно, вы сможете перебирать значения этого словаря, отображая их с помощью самого словаря, но за пределами этого простого примера это приведет к беспорядку

Ответ №1:

У меня нет среды, позволяющей легко протестировать эти идеи, но я изучаю нечто подобное в рамках использования шаблонов jinja в airflow.

Из того, что я могу найти, лучший способ сделать это — явно визуализировать внутреннюю строку шаблона во внешнем шаблоне. Для этого вам может потребоваться передать или импортировать конструктор шаблона в словаре параметров.

Вот некоторый (непроверенный) код:

 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!!! . Я полагаю, вы могли бы пойти еще глубже и определить, сколько уровней нужно отобразить, подсчитав скобки.


Пройдя через все это, я хотел бы явно указать, что это очень запутанно.

Хотя я считаю, что обнаружил необходимость в 2 уровнях рендеринга в рамках моего очень специфического использования воздушного потока, я не могу представить потребность в большем количестве уровней, чем это.

Если вы читаете это, думая «это именно то, что мне нужно»: все, что вы пытаетесь сделать, вероятно, можно сделать более красноречиво. Сделайте шаг назад, подумайте, что у вас может быть проблема с xy, и перечитайте документы jinja, чтобы убедиться, что лучшего способа нет.

Ответ №2:

Ну, вы всегда можете создать фильтр, подобный:

 @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>
  

Ответ №3:

Для этого вы можете использовать низкоуровневый API Jinja, украденный из 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