среда, 2 декабря 2009 г.

Используем встроенные строковые методы Python'а в Django шаблонах

Вместо предисловия

Привет! Давно здесь не отписывался. Почему? Наверное главная причина, что после выхода 1.1 версии уже не так активно слежу за развитием Django. Может быть в ближайшее время меня пробъет на творчество и я выдам пару-тройку новых постов, но не обещаю ;)


Но это я отвлекся от темы поста. А она заключается вот в чем. Надо было сегодня в шаблонах Django для некоторых урлов убрать конечные слеши, т.е. просто вызвать url.rstrip('/'). Просмотрев в который раз список всторенных шаблонных фильтров и не обнаружив там нужного, я задумался: как быть? Создавать простой фильтр, типа:

from django.template import Library
from django.template.defaultfilters import stringfilter


register = Library()


@register.filter
@stringfilter
def rstrip(text, chars=None):
    return text.rstrip(chars)

совершенно не хотелось. Ибо вдруг мне в будущем захочется добавить поддержку lstrip или strip метода. Что надо будет морочиться с Ctrl+C, Ctrl+V и минимальными исправлениями отяжеляя эту темплейт библиотеку?

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

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

from django.template import Library
from django.template.defaultfilters import stringfilter


register = Library()


for name in dir(u''):
    if name.startswith('_'): continue

    filter = lambda value, *args: getattr(unicode(value), name)(*args)
    filter = stringfilter(filter)

    register.filter(name, filter)

Написав простой тест-кейс я уже было приготовился доставать шампанское и переходить к следующей задаче, но не тут-то было :( TemplateSyntaxError для {{ url|strip:"/" }} заставила притормозить коней. Почему же так произошло? Я пошел к месту ошибки и понял, что проблема в *args, точнее в том, что django.template.FilterExpression.args_check ожидает определенный набор аргументов, а не, наоборот, не определенный заранее.

Что ж, пришлось усложнять код. Для начала я добавил поддержку только трех аргументов, ибо это максимальноя кол-во используемых аргументов для любого строкового метода, кроме format. Затем переписал предыдущую безымянную функцию в:

@stringfilter
def make_filter(name):
    def filter(value, first=None, second=None, third=None):
        args = [first, second, third]
        method = getattr(force_unicode(value), name)

        while True:
            try:
                return method(*args)
            except TypeError:
                args.pop(len(args) - 1)

    return filter

и заодно переписал регистрацию этого фильтра. Запустил тест-кейс, получил Ran OK! и на свою голову решил усложнить тест-кейс, проверив работу фильтра с двумя аргументами, например, {{ "abcdef"|replace:"abc":"def" }}.

Каково же было мое удивление, когда Django сказала, нет много аргументов для фильтров - это не хорошо. И сгенерировала очередную TemplateSyntaxError. Что ж, пришлось реализовывать это в виде отдельного простого тега {% stringmethod %}. Принцип его работы простейший, как видно из кода:

@register.simple_tag
def stringmethod(name, value, first=None, second=None, third=None):
    return make_filter(name)(value, first, second, third)

В свою очередь это повлекло за собой обновление теста, и {{ "abcdef"|replace:"abc":"def" }} превратилось в {% stringmethod "replace" "abcdef" "abc" "def" %}. Монструозно, соглашусь, но что поделаешь. В итоге, получив Ran OK! я немного подправил документацию и выложил это все дело, как отдельный гист на гитхаб.

Пользуйтесь, возможно вам это пригодится!

зы. На последок упомяну еще о пару особенностях stringmethods. Во-первых, она не переписывает встроенный join фильтр, во-вторых, она плохо справляется с format.

зыы. Если вы знаете reusable apps решающие похожие проблемы - не стесняйтесь писать в комменты :)

blog comments powered by Disqus