#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