Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Движок для работы с шаблонами

Оригинал: A Template Engine
Автор: Ned Batchelder
Дата публикации: July 12, 2016
Перевод: Н.Ромоданов
Дата перевода: февраль 2017 г.

Creative Commons

Перевод был сделан в соответствие с лицензией Creative Commons. С русским вариантом лицензии можно ознакомиться здесь.

Нет Батчелдер (Ned Batchelder) является разработчиком программного обеспечения и имеет многолетний опыт работы; в настоящее время он работает в edX над созданием программного обеспечения с открытым исходным кодом, предназначенного для образовтельных целей. Он майнтейнер проекта coverage.py, организатор группы Boston Python, а также лектор многих конференций PyCons. Его блоги размещены на http://nedbatchelder.com. Однажды он обедал в Белом доме.

Введение

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

Наиболее распространенными примерами подобных задач со сложной текстовой обработкой являются веб-приложения. Важный этап в любом веб-приложении — генерация кода на языке HTML, который будет пересылаться в браузер. Очень немногие страницы HTML полностью статичны: в них могут быть, по меньшей мере, некоторые динамические данные, например, такие как, имя пользователя. Как правило, на страницах много динамических данных: списки товаров, обновляемые новости с других сайтов и так далее.

В то же самое время на каждой странице HTML есть большие участки статического текста. И такие страницы большие — в них десятки тысяч байтов текста. Разработчику веб-приложений требуется решить проблему: как лучше сформировать большую строку, в которой содержатся одновременно статические и динамические данные? К этой проблеме надо еще добавить и то, что статический текст, на самом деле, содержит разметку на языке HTML, автор которой - другой член команды, дизайнер интерфейса, которому хочется работать на разметкой знакомыми ему способами.

В качестве иллюстрации давайте представим, что мы хотим создать следующий игрушечный фрагмент кода на языке HTML:

<p>Welcome, Charlie!</p>
<p>Products:</p>
<ul>
    <li>Apple: $1.00</li>
    <li>Fig: $1.50</li>
    <li>Pomegranate: $3.25</li>
</ul>

Здесь имя пользователя точно также, как название и цена товара, будут динамическими элементами. Здесь не указано даже количество товара: в другие моменты времени может потребоваться показывать другое количество товара.

Один из способов сделать такой HTML – это использовать в коде строковые константы и, когда создается страница, объединять их вместе. Динамические данные будут вставлены с помощью некоторой операции подстановки строк. Некоторые из наших динамических данных повторяются, например, элементы списка товаров. Это означает, что у нас будут повторяющиеся куски кода на языке HTML, поэтому их нужно обрабатывать отдельно и, затем, объединять с остальной частью страницы.

Таким образом, создание нашей игрушечной страницы может выглядеть следующим образом:

# Основная часть HTML всей страницы.
PAGE_HTML = """
<p>Welcome, {name}!</p>
<p>Products:</p>
<ul>
{products}
</ul>
"""

# Фрагмент кода HTML каждого отображаемого товара.
PRODUCT_HTML = "<li>{prodname}: {price}</li>\n"

def make_page(username, products):
    product_html = ""
    for prodname, price in products:
        product_html += PRODUCT_HTML.format(
            prodname=prodname, price=format_price(price))
    html = PAGE_HTML.format(name=username, products=product_html)
    return html

Это работает, но у нас полный беспорядок. В коде HTML есть несколько строковых констант, вставленных в код нашего приложения. Логику страницы трудно понять, поскольку статический текст разорван на отдельные куски. В коде языка Python теряется конкретная информация, относящаяся к форматированию данных, При модификации страницы нашему дизайнеру интерфейса для внесения изменений в код HTML потребуется возможность редактирования кода на языке Python. Представьте себе, как будет выглядеть код в случае, если бы страница была в десять (или в сто) раз более сложной; такой код быстро станет неработоспособным.

Шаблоны

Лучший способ создания страниц HTML- использовать шаблоны (templates). Страница HTML рассматривается как шаблон, а это означает, что этот файл будет, по большей части, статической страницей HTML с динамическими элементами, добавляемые в нее с помощью специальных обозначений. Наша игрушечная страница, приведенная выше, может, если ее использовать как шаблон, выглядеть следующим образом:

<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
    <li>{{ product.name }}:
        {{ product.price|format_price }}</li>
{% endfor %}
</ul>

Здесь акцент делается на текст на языке HTML с логикой, добавленной в HTML. Такой документо-ориентированный подход сильно отличается от нашего кода, ориентированного на логику, показанного выше. Раньше наша программа была в основном кодом на языке Python со вставками языка HTML, которые были добавлены логику программы. Теперь наша программа является, по большей части, статической разметкой языка HTML.

Самое главное то, что в шаблонах применяется статический стиль в отличие от того, как используется большинство языков программирования. Например, если использовать язык Python, то большая часть исходного файла будет представлять собой исполняемый код, и если вам нужен статический текст, вы вставляете его в строковый литерал:

def hello():
    print("Hello, world!")

hello()

Когда интерпретатор языка Python читает этот исходный файл, он интерпретирует текст def hello(): как инструкцию, которую нужно выполнить. Двойные кавычки в print("Hello, world!") указывают на то, что идущий далее текст является литералом, который заканчивается тогда, когда встречаются закрывающие двойные кавычки. Здесь все работает как и в большинстве языков программирования: в основном, динамически с некоторыми статическими элементами, вставленными в инструкции. Статические части вставляются с помощью двойных кавычек.

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

<p>Welcome, {{user_name}}!</p>

Здесь текст будет без изменений перемещаться в результирующую страницу на языке HTML до тех пор, пока не встретится обозначение '{{', указывающее на переключение в динамический режим, в котором в выходные данные будет подставлено значение переменной user_name.

Примерами мини-языков, используемых для создания текста из строковых литералов и данных, которые следует вставить, являются в языке Python функции форматирования строка, например, "foo = {foo}!".format(foo=17). В шаблонах этот принцип расширен и позволяет вставлять условные выражения и циклы, но различие не принципиальное.

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

Чтобы в наших программах использовать шаблоны на языке HTML, нам нужен специальный движок, обрабатывающий шаблоны (template engine): функция, которая берет статический шаблон, описывающий структуру и статическое содержание страницы, а также динамический контекст, который предоставляет собой динамические данные для добавления их в шаблон. Движок объединяет шаблон и контекст для того, чтобы создать строку на чистом языке HTML. Работа движка заключается в интерпретации шаблона и замены динамических частей реальными данными.

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

Вспомогательный синтаксис

Движки для работы с шаблонами отличаются синтаксисом, который они поддерживают. Наш синтаксис шаблона базируется на популярном веб-фреймворке Django. Поскольку мы реализуем наш движок на языке Python, в нашем синтаксисе будут использованы некоторые концепции этого языка. Мы уже вначале этой главы в нашем игрушечном примере видели некоторые фрагменты такого синтаксиса, но здесь мы кратко перечислим синтаксические конструкции, которые мы будем реализовывать.

Контекстные данные вставляется с помощью двойных фигурных скобок:

<p>Welcome, {{user_name}}!</p>

Данные передаются в шаблон в тот момент, когда выполняется рендеринг шаблона. Подробнее об этом позже.

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

dict["key"]
obj.attr
obj.method()

В нашем синтаксисе шаблонов все эти операции заменяются выражением с использованием точки:

dict.key
obj.attr
obj.method

С помощью операции точка можно получить доступ либо к атрибутам объектов, либо к значениям из словаря и, если для получения результирующего значения нужно сделать вызов, то этот вызов осуществляется автоматически. В этом отличие от кода на языке Python, в котором вам нужно для этих операций пользоваться другим синтаксисом. Синтаксис операций, используемый в шаблоне, более простой:

<p>The price is: {{product.price}}, with a {{product.discount}}% discount.</p>

Чтобы изменить значения, вы можете пользоваться функциями, которые называются фильтрами (filters). Фильтры вызываются с помощью символа «конвейер»:

<p>Short name: {{story.subject|slugify|lower}}</p>

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

{% if user.is_logged_in %}
    <p>Welcome, {{ user.name }}!</p>
{% endif %}

Использование циклов позволит нам добавлять в наши страницы наборы данных:

<p>Products:</p>
<ul>
{% for product in product_list %}
    <li>{{ product.name }}: {{ product.price|format_price }}</li>
{% endfor %}
</ul>

Как и в других языках программирования, для создания сложных логических структур условные выражения и циклы могут быть вложены друг в друга.

И, наконец, поскольку нам может потребоваться документировать наши шаблоны, между двумя символами «решетка» вставляются комментарии:

{# This is the best template ever! #}

Реализация подхода

Если говорить в общем, то функционирование движка, работающего с шаблонами, будет состоять из двух основных этапов: синтаксического анализа (parsing), а затем рендеринга (rendering) шаблона.

Рендеринг шаблона включает в себя следующие действия:

  • управление динамическим контекстоом, который является источником данных;
  • выполнение логических операций;
  • реализация доступа с применением точки и выполнение фильтров.

Ключевым является вопрос о том, как переходить от этапа синтаксического анализа к этапу рендеринга,. Что именно будет создано на этапе синтаксического анализа и каким образом следует выполнять рендеринг? Есть два основных варианта; мы будем называть их интерпретацией и компиляцией, терминами, используемыми при реализации других языков.

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

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

В нашей реализации движка используется компиляция: мы компилируем шаблон в код на языке Python. После того, как код на языке Python будет запущен, он выдаст результат.

Движок для работы с шаблонами, описанный здесь, был изначально написан как часть пакета coverage.py, предназначенного для создания отчетов на языке HTML. В нем используется только несколько шаблонов, но они используются снова и снова и с помощью одного шаблона создается большое количество файлов. В целом, если шаблоны компилируются в код на языке Python, то программа работает быстрее даже в случае, если процесс компиляции несколько сложнее, его необходимо выполнить только один раз, а выполнение скомпилированного кода происходит много раз, и это будет выполняться во много раз быстрее, чем интерпретация некоторой структуры данных.

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

Наш компилятор шаблов представляет собой небольшой пример общей технологии, называемой генерацией кода. Генерация кода лежит в основе многих мощных и гибких инструментов, в том числе и в основе компиляторов языков программирования. Генерация кода может оказаться сложной, но полезной технологией, которая должна присутствовать в вашем инструментарии.

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

Компиляция в код языка Python

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

<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
    <li>{{ product.name }}:
        {{ product.price|format_price }}</li>
{% endfor %}
</ul>

Наш движок будет компилировать этот шаблон в код на языке Python. Результирующий код на языке Python выглядит необычно, поскольку мы применили некоторые сокращения, которые создают чуть-чуть более быстрый код. Это язык Python (немного отформатированный для удобства чтения):

def render_function(context, do_dots):
    c_user_name = context['user_name']
    c_product_list = context['product_list']
    c_format_price = context['format_price']

    result = []
    append_result = result.append
    extend_result = result.extend
    to_str = str

    extend_result([
        '<p>Welcome, ',
        to_str(c_user_name),
        '!</p>\n<p>Products:</p>\n<ul>\n'
    ])
    for c_product in c_product_list:
        extend_result([
            '\n    <li>',
            to_str(do_dots(c_product, 'name')),
            ':\n        ',
            to_str(c_format_price(do_dots(c_product, 'price'))),
            '</li>\n'
        ])
    append_result('\n</ul>\n')
    return ''.join(result)

Каждый шаблон преобразуется в функцию render_function, в которой используется словарь данных, называемый контекстом. Тело функции начинается с выборки данных из контекста в локальные имена, поскольку к ним доступ будет более быстрым в случае, если они будут использоваться повторно. Все контекстные данные переносятся в локальные переменные с префиксом c_, так что мы можем пользоваться другими локальными именами, не опасаясь коллизий.

Результатом работы с шаблоном будет строка. Самый быстрый способ построить строку из частей — это создать список строк и в конце объединить их вместе. В объекте result будет находиться список строк. Поскольку мы собираемся добавлять в этот список строки, мы для его методов append и extend будем пользоваться локальными именами result_append и result_extend. Последнее локальное имя, которое мы создадим, будет to_str, используемое вместо уже имеющегося имени str.

Такие переименования необычны. Давайте рассмотрим их более подробно. В языке Python вызов метода для объекта, например, result.append("hello"), выполняется за два шага. Во-первых, атрибут append извлекается из результирующего объекта: result.append. Затем к извлеченному значению выполняется обращение как к функции, передавая ей аргумент "hello". Несмотря на то, что мы привыкли считать, что эти шаги выполняются вместе, они, на самом деле, будут выполняться по-отдельности. Если вы сохраните результат первого шага, то вы сможете выполнять второй шаг с уже сохраненным значением. Т.е. следующие два фрагмента кода на языке Python делают одно и тоже:

# Тот вариант, который мы обычно видим:
result.append("hello")

# Но следующий вариант работает точно также:
append_result = result.append
append_result("hello")

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

Это пример микро-оптимизации: необычного метода кодирования, который дает нам крошечное ускорение в работе. Микро-оптимизации могут быть менее читаемыми или более запутанными, поэтому они оправданы только для кода, о котором уже известным что он является узким местом по производительности. Разработчики расходятся во мнениях относительно того, какой объем микро-оптимизаций оправдан, и некоторые начинающие разработчики могут переусердствовать. Оптимизация здесь была добавлена только после того, как оценка времени работы показала, что они улучшат производительность, но на немного. Микро-оптимизация может быть поучительной, поскольку в ней используются некоторые экзотические аспекты языка Python, но ими не следует чрезмерно пользоваться в вашем собственном коде.

Сокращение str также является микро-оптимизацией. Имена в языке Python могут быть локальными, используемыми в функции, глобальными, используемыми в модуле, или встроенными в язык Python. Поиск локального имени выполняется быстрее, чем глобального или встроенного имени. Мы используем тот факт, что str является встроенной переменной, которая всегда доступна, но Python каждый раз, когда оно используется, должен повторно искать имя str. Если поместить его значение в локальную переменную, то это сэкономит нам еще немного времени, поскольку локальные переменные можно найти быстрее, чем встроенные.

После того, как будут определены эти сокращения, мы готовы из конкретного нашего шаблона создавать строки на языке Python. Строки будут добавляться в список результатов с помощью сокращений append_result или extend_result в зависимости от того, нужно ли нам добавить одну строку или более одной строки. Текстовый литерал в шаблоне становится простым строковым литералом.

Наличие операций append и extend увеличивает сложность кода, но помните, что нам нужно самое быстрое исполнение шаблона, а и использование extend означает создание нового списка из одного элемента, который мы можем передать в операцию extend.

Выражения, указанные в конструкции {{ ... }}? вычисляются, преобразуются в строки и добавляются к результату. Точки в выражении обрабатываются с помощью функции do_dots, так как смысл выражений с точками зависит от данных в контексте: это может быть доступ к атрибуту объекта или доступ к элементу набора или это может быть вызов функции.

Логические структуры {% if ... %} и {% for ... %} преобразуются в условные выражения и циклы языка Python. Выражение в теге {% if/for ... %} станет выражением в инструкции if или for, а содержимое вплоть до тега {% end... %} станет телом инструкции.

Создаем движок

Теперь, когда мы понимаем, что движок будет делать, давайте рассмотрим его реализацию.

Класс Templite

Сердцем движка, работающего с шаблонами, является класс Templite. (Получить его? Это шаблон, но его можно выполнить!)

В классе Templite есть некоторый интерфейс. Вы сконструировали объект Templite с текстом шаблона, затем позже вы сможете использовать метод render, имеющийся в нем, для того, чтобы с помощью шаблона отобразить определенный контекст, т. е. словарь данных:

# Создаем объект Templite.
templite = Templite('''
    <h1>Hello {{name|upper}}!</h1>
    {% for topic in topics %}
        <p>You are interested in {{topic}}.</p>
    {% endfor %}
    ''',
    {'upper': str.upper},
)

# Затем используем его для того, чтобы выполнить рендеринг некоторых данных.
text = templite.render({
    'name': "Ned",
    'topics': ['Python', 'Geometry', 'Juggling'],
})

Когда создается объект, мы проходим по тексту шаблона таким образом, чтобы можно было выполнить шаг компиляции только один раз, а потом повторно использовать скомпилированный результат, многократно вызывая метод render.

Конструктор также получает доступ к словарю значений, представляющему собой исходный контекст. Значения хранятся в объекте Templite, и они позже будут доступны при рендеренге шаблона. Это хороший прием при определении функций или констант, которые должны быть доступны повсюду точно также, как и метод upper из предыдущего примера.

Прежде чем мы обсудим реализацию класса Templite, нам нужно сначала определить вспомогательный класс CodeBuilder.

Строим код — класс CodeBuilder

Основной частью работы в нашем движке будет синтаксический анализ шаблона и создание необходимого кода на языке Python. Чтобы было проще создавать код, мы воспользуемся классом CodeBuilder, который, когда мы будем создавать код, будет возиться со всей конкретикой. Он добавляет строки кода, управляет созданием отступов в тексте и, в конце концов, возвращает нам значения из скомпилированного кода на языке Python.

Один объект CodeBuilder отвечает за всю работу с кодом. Когда наш движок шаблонов используется, весь фрагмент кода на языке Python всегда будет полным определением одной функции. Но в классе CodeBuilder не делается никаких предположений, что это будет ровно одна функция. В результате код CodeBuilder – это класс общего вида, и он не сильно связан с остальной частью кода движка.

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

В объекте CodeBuilder хранится список строк, которые будут вместе финальным кодом на языке Python. Единственное что необходимо — текущее значение отступа текста:

class CodeBuilder(object):
    """Создание исходного кода."""

    def __init__(self, indent=0):
        self.code = []
        self.indent_level = indent

Класс CodeBuilder делает небольшую работу. Метод add_line добавляет новую строку кода, которая автоматически смещается к текущему значению отступа текста и добавляется символ новой строки:

    def add_line(self, line):
        """Добавляем в код строку.

        Начальный отступ в строке и переход на новую строку будут созданы без вашего участия.

        """
        self.code.extend([" " * self.indent_level, line, "\n"])

Методы indent и dedent увеличивают или уменьшают текущее значение отступа в тексте:

def indent(self):
        """Увеличивает отступ в начале строки для последующих строк."""
        self.indent_level += self.INDENT_STEP

    def dedent(self):
        """Уменьшает отступ в начале строки для последующих строк ."""
        self.indent_level -= self.INDENT_STEP

Метод add_section управляется еще одним объектом CodeBuilder. Это позволяет нам сохранить ссылку на место в коде и позже добавлять туда текст. Список self.code является, большей частью, списком строк, но в нем также будут храниться указатели на разделы section:

    def add_section(self):
        """Добавляет секцию, часть CodeBuilder-а."""
        section = CodeBuilder(self.indent_level)
        self.code.append(section)
        return section

Метод __str__ создает одну строку со всем кодом. Здесь просто объединяются все строки из self.code. Обратите внимание, что, поскольку self.code может содержать разделы sections, он также может рекурсивно вызвать другие объекты CodeBuilder:

    def __str__(self):
        return "".join(str(c) for c in self.code)

Метод get_globals выполняет код и выдает окончательные значения. Он превращает объект в строку, выполняет ее для того, чтобы получить определения, и возвращает полученные значения:

    def get_globals(self):
        """Исполняет код и возвращает словарь глобалов."""
        # Проверка того, что вызывающий объект действительно завершил все блоки, которые были начаты.
        assert self.indent_level == 0
        # Получаем исходный код на языке Python в виде одной строки.
        python_source = str(self)
        # Исполнение исходного кода, определение глобалов и возвращение их.
        global_namespace = {}
        exec(python_source, global_namespace)
        return global_namespace

В этом последнем методе используют несколько экзотических особенностей языка Python. Функция exec выполняет строку, содержащую код на языке Python. Второй аргумент функции exec представляет собой словарь, который будет собирать вплоть до глобалов, определенных в коде. Так, например, если мы выполняем следующий код:

python_source = """\
SEVENTEEN = 17

def three():
    return 3
"""
global_namespace = {}
exec(python_source, global_namespace)

то /code>global_namespace['SEVENTEEN'] будет равно 17, а global_namespace['three'] будет функцией с именем three.

Хотя для того, чтобы создать функцию, мы пользуемся только CodeBuilder-ом, здесь нет ничего такого, чтобы ограничивало его использование именно таким образом. Просто так класс становится проще в реализации и его легче понять.

CodeBuilder позволяет нам создать фрагмент исходного кода на языке Python, и позволяет, вообще, ничего конкретно не знать о нашем движке. Мы могли бы использовать его таким образом, чтобы в языке Python были бы определены три различные функции, а затем метод get_globals мог бы возвратить словарь dict из трех значений, т. е. три функции. Когда это будет происходить, нашему движку для работы с шаблонами нужно будет выбрать только одну функцию. Но с точки зрения разработки программного обеспечения лучше держать эту деталь реализации в коде движка и убрать ее и из нашего класса CodeBuilder.

Даже сейчас, когда мы ее на самом деле используем, т. е. определяем отдельную функцию, наличие метода get_globals, возвращающего словарь, позволяет сохранять код более модульным, поскольку не нужно знать имя функции, которую мы определили. Независимо от того, какое имя функции мы определим в нашем исходном коде на языке Python, мы можем получить это имя из словаря dict, возвращаемого методом get_globals.

Теперь мы можем обратиться к реализации самого класса Templite и посмотреть, как и где используется класс CodeBuilder.

Реализация класса Templite

Большая часть нашего кода находится в классе Templite. Как мы уже ранее обсуждали, в нем есть шаг компиляции и шаг рендеринга.

Компиляция

Вся работа по компиляции шаблона в функцию языка Python происходит в конструкторе класса Templite. Сначала выполняются следующие действия с контекстами:

    def __init__(self, text, *contexts):
        """Конструктор Templite с конкретным элементом `text`.

        `contexts` are dictionaries of values to use for future renderings.
        These are good for filters and global values.

        """
        self.context = {}
        for context in contexts:
            self.context.update(context)

Обратите внимание, мы использовали *contexts в качестве параметра. Звездочка означает, что в кортеж будет упаковано и передано в качестве контекста любое количество позиционных аргументов. Такое действие называется распаковкой аргументов, и означает, что при вызове можно будет с контекстом передавать несколько различных словарей. Тогда любой из следующих вызовов будет допустимым:

t = Templite(template_text)
t = Templite(template_text, context1)
t = Templite(template_text, context1, context2)

Аргументы контекста (если таковые имеются) передаются в конструктор в виде кортежа контекстов. Затем мы можем пройтись по всем контекстам в кортеже и обработать каждый из них по-отдельности. Мы просто создаем один объединенный словарь под названием self.context, в котором будет храниться содержимое всех исходных контекстов. Если среди контекстов встречаются повторяющиеся имена, то будет использоваться последнее из них.

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

        self.all_vars = set()
        self.loop_vars = set()

Позже мы увидим, как они нам помогут построить пролог нашей функции. Во-первых, для того, чтобы начать строить нашу скомпилированный функцию, мы воспользуемся классом CodeBuilder, о котором мы писали ранее,:

        code = CodeBuilder()

        code.add_line("def render_function(context, do_dots):")
        code.indent()
        vars_code = code.add_section()
        code.add_line("result = []")
        code.add_line("append_result = result.append")
        code.add_line("extend_result = result.extend")
        code.add_line("to_str = str")

Здесь мы строим наш объект CodeBuilder, и начинаем в него записывать строки. Наша функция на языке Python будет называться render_function и у нее будет два аргумента: context, являющийся словарем с данными, которые должны использоваться, и do_dots, представляющий собой функцию, с помощью которой реализуется доступ с использованием символа точки.

Контекст здесь представляет собой комбинацию контекста данных, передаваемых в конструктор Templite, и контекста данных, переданных в функцию рендеринга. Это полный набор данных, доступных в шаблоне, созданном нами в конструкторе Templite.

Обратите внимание на то, что объект CodeBuilder очень простой: он "не знает" об определениях функций, - всего лишь строки кода. В результате объект CodeBuilder будет простым как в реализации, так и при использовании. Здесь можно просто читать сгенерированный нами код и слишком не задумываться о специализированном объекте CodeBuilder.

Мы создаем раздел (section) с названием vars_code. Затем мы будем в этот раздел записывать строки, из которых извлекаются переменные. Объект vars_code позволяет нам сохранить место в функции, которые позже мы сможем заполнить, когда будем иметь необходимую нам информацию.

Затем записываются четыре строки, в которых указывается определение списка с результатами, наши версии методов append и extend для этого списка и наш вариант метода str(). Как мы уже говорили ранее, этот странный подход просто позволит немного улучшить производительность функции рендеринга.

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

Далее мы определяем внутреннюю функцию, которая поможет нам с буферизацией выходных строк:

        buffered = []
        def flush_output():
            """Принудительный перенос `забуферированного кода` в код нашей функции."""
            if len(buffered) == 1:
                code.add_line("append_result(%s)" % buffered[0])
            elif len(buffered) > 1:
                code.add_line("extend_result([%s])" % ", ".join(buffered))
            del buffered[:]

Поскольку мы создаем фрагмент данных, который следует передать в нашу откомпилированную функцию, нам нужно превратить его в вызовы функций, добавляемые к нашему результату. Нам бы было удобно объединить повторяющиеся вызовы метода append в один расширенный вызов. Это еще одна микро-оптимизация. Для того, чтобы это стало возможным, мы воспользуемся буферизацией создаваемых фрагментов данных.

В списке buffered хранятся строки, которые еще нужно будет записать в исходный код нашей функции. По мере того, как продолжается компиляция шаблона, в buffered будут добавлять строки, а их перезапись в исходный код функции будет происходить в тот момент, когда наша обработка будет доходить до инструкций управления, например, инструкции if или начала и конца циклов.

Функция flush_output является замыканием (closure), т. е. использует ссылки на переменные, находящиеся вне самой функции. Здесь flush_output ссылается на buffered и code. Это упрощает наши вызовы функции: нам не нужно сообщать, что для сброса накапливаемых данных используется переменная flush_output или указывать, где эта переменная находится; в функции все это известно в неявном виде.

Если в буфере записана только одна строка, то для добавления его к результату используется метод append_result. Если в буфере сохранено более одной строки, то для переноса всех их в результат применяется метод append_result. Затем буферный список очищается с тем, чтобы в него можно было поместить другие строки.

Оставшаяся часть процедуры компиляции кода будет с помощью функции добавлять строки в buffered, и, в конце концов, вызовет flush_output для того, чтобы записать эти строки в CodeBuilder.

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

buffered.append("'hello'")

что будет означать, что в нашу скомпилированную функцию на языке Python будет вставлена следующая строка:

append_result('hello')

с помощью которой строка hello будет добавлена в результирующий текст, получаемый после рендеринга шаблона. У нас здесь присутствуют несколько уровней абстракции, работа с которыми может быть затруднительна. В компиляторе используется метод buffered.append("'hello'"), который в скомпилированной функции на языке Python создает обращение к методу append_result('hello'), которое, когда оно будет выполнено, добавит слово hello к результату рендеринга шаблона.

Вернемся к нашему классу Templite. Поскольку мы выполняем синтаксический анализ структур управления, нам хотелось бы проверять, не нарушена ли их вложенность. Список ops_stack является стеком строк:

        ops_stack = []

Когда нам встречается (например) тег {% if .. %}, мы помещаем в стек 'if'. Когда мы обнаруживаем тег {% endif %}, мы можем вытолкнуть из стека значение, находящее в его верхней части или сообщить об ошибке в случае, если это было не 'if'.

Теперь начинается настоящий синтаксический анализ. Мы с помощью регулярных выражений, или regex, разбиваем текст шаблона на ряд лексем. Регулярные выражения могут вас испугать: это очень компактная нотация, предназначенная для сложного сопоставления с образцом. Они также очень эффективны, поскольку весь алгоритм сравнения с использованием регулярных выражений был реализован на языке C, а не в коде на языке Python. Ниже показано наше регулярное выражение:

        tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)

Оно выглядит сложным; давайте его рассмотрим.

Функция re.split будет разбивать строку на части с помощью регулярного выражения. Наш образец заключен в скобки, поэтому совпадения будут использоваться для разбиения строки на отдельные части, которые будут возвращены в виде списка. Наш образец соответствует синтаксису наших тегов, но т. к. мы его заключили в скобки, строка будет делиться на теги, которые будут возвращены.

Флаг (?s) в регулярном выражении означает, что точка должна соответствовать новой строки. Далее у нас есть три группы заключенных в скобки альтернативных варианта: /code>{{.*?}} - соответствует выражению, {%.*?%} - соответствует тегу, а {#.*?#} - соответствует комментарию. Во всех вариантах мы используем обозначение .*?, которое соответствует произвольному количеству символов, причем самой именно короткой последовательности, которая соответствует образцу.

Результатом выполнения функции re.split будет список строк. Например, следующий текст шаблона:

<p>Topics for {{name}}: {% for t in topics %}{{t}}, {% endfor %}</p>

будет разбит на такие куски:

[
    '<p>Topics for ',               # literal
    '{{name}}',                     # expression
    ': ',                           # literal
    '{% for t in topics %}',        # tag
    '',                             # literal (empty)
    '{{t}}',                        # expression
    ', ',                           # literal
    '{% endfor %}',                 # tag
    '</p>'                          # literal
]

После того, как текст будет разбит на лексемы так, как это показано в нашем примере, мы можем создать цикл по лексемам и работать с каждой из них по очереди. Разделив их согласно типам, мы можем обрабатывать каждый тип тегов отдельно.

Откомпилированным кодом будет цикл по этим лексемам:

        for token in tokens:

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

            if token.startswith('{#'):
                # Коментарий: игнорируем его и двигаемся дальше.
                continue

В случае выражения вида {{...}} мы убираем две скобки — по одной спереди и сзади, убираем пробелы и передаем все выражение в _expr_code:

            elif token.startswith('{{'):
                # Выражение, которое оценивается.
                expr = self._expr_code(token[2:-2].strip())
                buffered.append("to_str(%s)" % expr)

Метод _expr_code будет компилировать выражение шаблона в выражение на языке Python. Мы рассмотрим эту функцию позже. Мы используем функцию to_str для того, чтобы преобразовать значение в строку, и добавить ее к нашему результату.

Третий случай более сложный: теги {% ... %}. Эти управляющие конструкции, которые станут управляющими конструкциями языка Python. Во-первых, мы должны сбросить в память все наши забуферированные выходные строки, затем мы извлекаем из тега список слов:

            elif token.startswith('{%'):
                # Тег действия: разбивается на слова и далее выполняется анализ.
                flush_output()
                words = token[2:-2].strip().split()

Теперь у нас есть три под-случая, которые различаются первым словом в теге: if, for или end. Если первое слово if, то обработка ошибок и генерации кода будет иметь следующий простой вид:

                if words[0] == 'if':
                    # Инструкция if: оценка выражения при определении if.
                    if len(words) != 2:
                        self._syntax_error("Don't understand if", token)
                    ops_stack.append('if')
                    code.add_line("if %s:" % self._expr_code(words[1]))
                    code.indent()

Тег if должен иметь одно выражение, поэтому в words должно быть только два элемента. Если это не так, мы для того, чтобы сгенерировать исключение "ошибка синтаксиса", используем метод-обработчик _syntax_error. Мы помещаем 'if' в ops_stack с тем, чтобы мы могли проверить тег endif. Часть выражения тега if компилируется с помощью _expr_code в выражение на языке Python и используется в качестве условного выражения в инструкции if.

Второй тип тега – для слова for, который будет откомпилирован в инструкцию for языка Python:

                elif words[0] == 'for':
                    # Цикл: итерация над результатом выражения.
                    if len(words) != 4 or words[2] != 'in':
                        self._syntax_error("Don't understand for", token)
                    ops_stack.append('for')
                    self._variable(words[1], self.loop_vars)
                    code.add_line(
                        "for c_%s in %s:" % (
                            words[1],
                            self._expr_code(words[3])
                        )
                    )
                    code.indent()

Мы выполняем проверку синтаксиса и помещаем 'for' в стек. Метод _variable проверяет синтаксис переменной, и добавляет ее к набору, с которым мы работаем. Мы во время компиляции собираем имена всех переменных. Позже нам нужно будет написать пролог нашей функции, в котором мы распакуем имена всех переменных, полученных нами из контекста. Чтобы правильно это сделать, мы должны запомнить имена всех переменных, которые нам встретились, в переменной self.all_vars, а имена всех переменных, определенных в циклах, в переменной self.loop_vars.

Мы добавляем одну строку в исходный код нашей функции — инструкцию for. Все наши переменные шаблона превращаются в переменные языка Python, перед каждой из которых добавляется префикс c_ для того, чтобы мы были уверены, что они не будут конфликтовать с другими именами, которыми мы пользуемся в нашей функции. Мы используем _expr_code для компиляции итерационного выражения из шаблона в итерационное выражение языка Python.

Последний вид тега, который мы обрабатываем, это тег завершения; либо {% endif %}, либо {% endfor %}. В обоих члучаях влияние на исходный код нашей скомпилированной функции будет одинаков: просто завершить либо инструкцию if, либо инструкцию for, которая была начата ранее:

                elif words[0].startswith('end'):
                    # Завершение некоторой конструкции. Убираем верхний элемент из стека ops.
                    if len(words) != 1:
                        self._syntax_error("Don't understand end", token)
                    end_what = words[0][3:]
                    if not ops_stack:
                        self._syntax_error("Too many ends", token)
                    start_what = ops_stack.pop()
                    if start_what != end_what:
                        self._syntax_error("Mismatched end tag", end_what)
                    code.dedent()

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

Если говорить об обработке ошибок, то в случае, если тегом будет не if, for или end, мы не будем знать, что это за тег, так что возникает синтаксическая ошибка:

                else:
                    self._syntax_error("Don't understand tag", words[0])

Мы закончили разбираться с тремя различными специальными синтаксическими структурами ({{...}}, {#...#} и {%...%}). Все, что осталось, это обработать содержимое литерала. Мы добавим в выходной буфер литеральную строку при помощи встроенной функции repr, которая создаст для этой лексемы строковый литерал на языке Python:

            else:
                # Содержимое литерала.  Если оно не пустое, то выдаем его.
                if token:
                    buffered.append(repr(token))

Если бы мы не использовали функцию repr, то нам, в конечном итоге, нужно было завершать нашу скомпилированную функцию следующими строками:

append_result(abc)      # Ошибка! abc не определена

Нам нужно значение, которое должно быть окружено кавычками следующим образом:

append_result('abc')

Функция repr добавляет кавычки в начале и в конце строки, а также добавляет обратную косую черту там, где это необходимо:

append_result('"Don\'t you like my hat?" he asked.')

Обратите внимание на то, что мы сначала с помощью if token: проверяем, является ли лексема пустой строкой, так как нет никакого смысла добавлять в выходные данные пустую строку. Поскольку наше регулярное выражение разбивает строку в соответствие с синтаксисом тега, то между соседними тегами может оказаться пустая лексема. Для того, чтобы не вставлять в нашу скомпилированную функцию бесполезную инструкцию append_result(""), эту ситуацию проще всего проверить здесь.

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

        if ops_stack:
            self._syntax_error("Unmatched action tag", ops_stack[-1])

        flush_output()

Мы создали секцию в начале функции. Ее роль заключается в распаковке переменных шаблона из контекста, запомненного в локальных переменных языка Python. Теперь, когда мы обработали весь шаблон, мы знаем имена всех переменных, так что мы можем написать строки пролога функции.

Для того, чтобы знать, какие имена нам нужно определить, нам потребуется выполнить некоторые несложные действия. Взглянем на наш пример шаблона:

<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
    <li>{{ product.name }}:
        {{ product.price|format_price }}</li>
{% endfor %}
</ul>

Здесь используются две переменные, user_name и product. В all_vars будут присутствовать обе эти переменные, поскольку обе они используются в выражениях {{...}}. Но в прологе из контекста нужно выбрать только user_name, поскольку переменная product определяется в цикле.

Все переменные, используемые в шаблоне, заносятся в all_vars, а все переменные, определяемые в шаблоне, находятся в loop_vars. Все имена, которые находятся в loop_vars, уже определены в коде, поскольку они используются в циклах. Так что нам нужно распаковывать все имена, которые есть в all_vars, и которых нет в loop_vars:

        for var_name in self.all_vars - self.loop_vars:
            vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))

Каждое имя становится строкой в прологе функции, причем распаковка переменной контекста происходит в локальную переменную с соответствующим именем.

Мы почти закончили компиляцию шаблона в функцию языка Python. Наша функция добавляла строки в переменную result, поэтому последняя строка функции будет просто объединять их вместе и возвращать полученный результат:

        code.add_line("return ''.join(result)")
        code.dedent()

Теперь, когда мы закончили писать исходный код для нашей скомпилированного функции, нам нужно взять саму функцию из нашего объекта CodeBuilder. Метод get_globals выполняет код языка Python, который мы собрали. Помните, что наш код является определением функции (которая начинается с def render_function(..):), так что при выполнении кода будет определена функция render_function, но тело функции render_function выполнено не будет.

Результатом выполнения get_globals будет словарь значений, определенных в коде. Мы берем из него значение функции render_function и сохраняем его в виде атрибута в нашем объекте Templite:

        self._render_function = code.get_globals()['render_function']

И теперь функция self._render_function является функцией языка Python, которую можно вызвать (объект вида callable – прим.пер.). В дальнейшем, на этапе рендеринга, мы ее будем использовать.

Компиляция выражений

Мы еще не увидели основную часть процесса компиляции: метод _expr_code, который компилирует выражение в шаблоне в выражение на языке Python. Наши выражения в шаблонов могут быть очень простыми и состоять из одного имени:

{{user_name}}

либо они могут представлять собой сложную последовательность доступа к атрибутам и фильтрам:

{{user.name.localized|upper|escape}}

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

    def _expr_code(self, expr):
        """Генерируем выражение языка Python для `expr`."""

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

        if "|" in expr:
            pipes = expr.split("|")
            code = self._expr_code(pipes[0])
            for func in pipes[1:]:
                self._variable(func, self.all_vars)
                code = "c_%s(%s)" % (func, code)

Каждая из оставшихся частей конвейера является именем функции. Значение передается через функцию, которая создает конечное значение. Имя каждой функции является переменной, которая добавляется к all_vars с тем, чтобы в прологе мы могли ее должным образом извлечь.

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

        elif "." in expr:
            dots = expr.split(".")
            code = self._expr_code(dots[0])
            args = ", ".join(repr(d) for d in dots[1:])
            code = "do_dots(%s, %s)" % (code, args)

Чтобы понять, как компилируются точки, вспомните, что x.y в шаблоне может в языке Python означать либо x['y'], либо x.y, в зависимости от того, с каким объектом происходит работа; если результат будет типа callable, то делается вызов. Такая неопределенность означает, что мы должны попробовать эти возможности на этапе выполнения, а не во время компиляции. Таким образом, мы компилирует конструкцию x.y.z в вызов функции do_dots(x, 'y', 'z'). Эта функция попытается использовать различные варианты доступа и вернет результат того варианта, который окажется успешным.

Функция do_dots передается в нашу скомпилированную функцию на языке Python на этапе выполнения. Совсем скоро мы увидим, как это реализовано.

Последнее предложение в функции _expr_code обрабатывает случай, когда во входном выражении не было ни конвейеров, ни точек. В этом случае, это просто имя. Мы записываем его в all_vars, и в языке Python мы получаем доступ к нему по имени с префиксом:

        else:
            self._variable(expr, self.all_vars)
            code = "c_%s" % expr
        return code

Вспомогательные функции

Во время компиляции мы воспользовались несколькими вспомогательными функциями. Метод _syntax_error просто строит понятное сообщение об ошибке и выдает исключение:

    def _syntax_error(self, msg, thing):
        """Выдаем синтаксическую ошибку с помощью `msg` и показываем из-за чего она возникла - `thing`."""
        raise TempliteSyntaxError("%s: %r" % (msg, thing))

Метод _variable помогает нам проверять имена переменных и добавлять их в наборы имен, которые мы собираем во время компиляции. Мы используем регулярное выражение для того, чтобы проверить, что имя является допустимым идентификатором языка Python, а затем добавляем имя к соответствующему набору:

    def _variable(self, name, vars_set):
        """Отслеживаем имя `name`, используемое в качестве переменной.

        Добавляем имя в `vars_set`, набор имен переменны.

        Выдаем ошибку в случае, если `name` является недопустимым именем.

        """
        if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
            self._syntax_error("Not a valid name", name)
        vars_set.add(name)

После этого откомпилированный код нужно будет выполнить!

Рендеринг

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

    def render(self, context=None):
        """Рендеринг этого шаблона с помощью его применения к контексту `context`.

        `context` является словарем значение, которые используются в этом рендеринге.

        """
        #Создаем весь контекст, который мы будем использовать.
        render_context = dict(self.context)
        if context:
            render_context.update(context)
        return self._render_function(render_context, self._do_dots)

Вспомните, что, когда мы строили объект Templite, мы начали с контекста данных. Здесь мы копируем его, и объединяем его со всеми данными, которые были переданы для рендеринга. Копирование выполняется таким образом, что последовательные вызовы функции рендеринга не будут видеть данные друг друга, а объединение будет выполнено для того, чтобы у нас был единый словарь, позволяющий выполнять поиск данных. Мы строим один единый контекст данных из контекста, который был создан при создании шаблона был, и данных, полученных в момент выполнения рендеринга.

Обратите внимание на то, что данные, передаваемые в render, могут замещать данные, которые передаются в конструктор Templite. Этого, как правило, не происходит, поскольку контекст, передаваемый в конструктор, содержит такие глобалы, как определения фильтров и константы, а контекст, передаваемый в render, содержит специфические данные этого конкретного рендеринга.

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

    def _do_dots(self, value, *dots):
        """Оценка выражений с точками на этапе выполнения."""
        for dot in dots:
            try:
                value = getattr(value, dot)
            except AttributeError:
                value = value[dot]
            if callable(value):
                value = value()
        return value

Во время компиляции выражение, используемые в шаблоне, например, x.y.z преобразуется в do_dots(x, 'y', 'z'). Эта функция в цикле перебирает имена с точками, и для каждого имени пытается использовать его в качестве атрибута, и если это не удается, то пытается использовать его в качестве ключа. Наш единый синтаксис шаблона гибкий и действует либо x.y, либо как x['y']. На каждом шаге мы также проверяем, допустим ли вызов каждого нового значения, и если допустим, то мы его выполняем. После того, как мы закончим обработку всех имен с точками, значение, которое мы получим, будет именно тем, которое нам нужно.

Здесь мы снова использовали распакованные аргументы языка Python (*dots), так что _do_dots может обрабатывать любое количество имен с точками. В результате мы получаем гибкую функцию, которая будет работать для любого выражения с точками, которое мы встретим в шаблоне.

Обратите внимание, что когда вызывается функция self._render_function, мы переходим в функцию, которая используется для оценки выражений с точками, но мы всегда переходим в одну и ту же функцию. Мы могли бы сделать так, чтобы это было просто частью кода скомпилированного шаблона, но это будут одни и те же восемь строк для каждого шаблона, и эти восемь строк являются частью определения того, как работает шаблон, а не частью конкретного шаблона. Лучше реализовать это так, как это сейчас сделано, а не как часть скомпилированного шаблона.

Тесты

В состав движка для работы с тестами включен набор тестов, с помощью которых проверяется все функции движка и его пограничные случаи. На самом деле предел в 500 строк немного превышен: размер движка шаблона составляет 252 строк, а размер тестов - 275 строк. Такая ситуация характерна для любого кода с полным набором тестов: размер кода в тестах больше, чем в самом продукте.

Что еще надо сделать

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

  • наследование шаблонов и включение одного шаблона в другой (template inheritance and inclusion);
  • пользовательские теги (сustom tags);
  • автоматический выход из обработки шаблонов (аutomatic escaping);
  • аргументы для фильтров;
  • сложная логика условий, например, else и elif;
  • циклы с более чем одной переменной цикла;
  • управление генерацией пробелов.

Тем не менее, наш простой движок для работы с шаблонами будет полезен. В действительности этот движок используется на сайте coverage.py для создания отчетов на языке HTML.

Подведем итог

У нас есть простой размером в 252 строки движок, способный работать с текстовыми шаблонами. В реальных движках подобного вида возможностей гораздо больше, но в этом коде отражен основной принцип: компиляция шаблона в функцию на языке Python, а затем выполнение этой функции с тем, чтобы получить текстовый результат.