Lambda как аргумент для фильтра jinja2?

Я хотел бы иметь собственный фильтр в jinja2 следующим образом:

{{ my_list|my_real_map_filter(lambda i: i.something.else)|some_other_filter }}

Но когда я его реализую, я получаю эту ошибку:

TemplateSyntaxError: expected token ',', got 'i'

Кажется, синтаксис jinja2 не позволяет использовать lambdas в качестве аргументов? Есть ли какое-нибудь хорошее обходное решение? На данный момент я создаю лямбда в python, а затем передаю его в шаблон как переменную, но я бы предпочел бы просто создать его в шаблоне.

Ответ 1

Нет, вы не можете передать общее выражение Python для фильтрации в шаблоне Jinja2

Путаница исходит из шаблонов jinja2, похожих на синтаксис Python во многих аспектах, но вы должны воспринимать их как код с полностью независимым синтаксисом.

Jinja2 имеет строгие правила, что можно ожидать, в какой части шаблона и вообще не допускает код python как есть, он ожидает точные типы выражений, которые весьма ограничены.

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

Ответ 2

У меня есть обходной путь, я сортирую объект dict:

registers = dict(
    CMD = dict(
        address = 0x00020,
        name = 'command register'),
    SR = dict(
        address = 0x00010,
        name = 'status register'),
)

Мне нужно было перебрать регистр dict, но отсортировать по адресу. Поэтому мне нужен способ сортировки по полю "адрес". Чтобы сделать это, я создал собственный фильтр и передал выражение лямбда как строку, затем я использую Python builtin eval() для создания реальной лямбда:

def my_dictsort(value, by='key', reverse = False):

    if by == 'key':
        sort_by = lambda x: x[0].lower() # assumes key is a str

    elif by == 'value':
        sort_by = lambda x: x[1]

    else:
        sort_by = eval(by)   # assumes lambda string, you should error check

    return sorted(value, key = sort_by, reverse = reverse)

С помощью этой функции вы можете ввести ее в среду jinja2 следующим образом:

env = jinja2.Environment(...)
env.filters['my_dictsort'] = my_dictsort
env.globals['lookup'] = lookup            # queries a database, returns dict

А затем вызовите его из шаблона:

{% for key, value in lookup('registers') | my_dict_sort("lambda x:x[1]['address']") %}
{{"""\
    static const unsigned int ADDR_{key} = 0x0{address:04X}; // {name}
""" | format(key = key, address = value['address'], name = value['name']) 
}}
{% endfor %}

Вывод:

static const unsigned int ADDR_SR = 0x00010; // status register
static const unsigned int ADDR_CMD = 0x00020; // command register

Итак, вы можете передать лямбду в виде строки, но для этого вам нужно добавить специальный фильтр.

Ответ 3

Недавно мне пришлось решать ту же проблему, мне пришлось создать list из dict в моем шаблоне Ansible, и это не включено в базовые фильтры.

Вот мой трудоем:

def generate_list_from_list(list, target, var_name="item"):
    """
    :param list: the input data
    :param target: the applied transformation on each item of the list
    :param var_name: the name of the parameter in the lambda, to be able to change it if needed
    :return: A list containing for each item the target format
    """

    # I didn't put the error handling to keep it short
    # Here I evaluate the lambda template, inserting the name of the parameter and the output format
    f = eval("lambda {}: {}".format(var_name, target)) 

    return [f(item) for item in list]

# ---- Ansible filters ----
class FilterModule(object):
    def filters(self):
        return {
            'generate_list_from_list': generate_list_from_list
        }

Затем я могу использовать его таким образом:

(my_input_list является list из string, но он будет работать с list чего угодно)

# I have to put quotes between the <target> parameter because it evaluates as a string
# The variable is not available at the time of the templating so it would fail otherwise
my_variable: "{{ my_input_list | generate_list_from_list(\"{ 'host': item, 'port': 8000 }\") }}"

Или, если я хочу переименовать лямбда-параметр:

my_variable: "{{ my_input_list | generate_list_from_list(\"{ 'host': variable, 'port': 8000 }\", var_name="variable") }}"

Это выводит:

  • при вызове непосредственно в Python:
[{'host': 'item1', 'port': 8000}, {'host': 'item2', 'port': 8000}]
  • при вызове в шаблоне (файл yaml в моем примере, но так как он возвращает список, вы можете делать все, что захотите, после преобразования списка):
my_variable:
-    host: item1
     port: 8000
-    host: item2
     port: 8000

Надеюсь, это кому-то поможет