500 строк кода на Python для реализации механизма шаблонов

задняя часть Python продукт HTML

См. исходный текст: http://aosabook.org/en/500L/a-template-engine.html

См. код: https://github.com/aosabook/500lines/tree/master/template-engine.

введение

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

Одна из наиболее распространенных текстовых задач — в веб-приложениях. Важным шагом в любом веб-приложении является создание 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, поэтому их нужно обрабатывать отдельно и объединять с остальной частью страницы.

Например, наша демонстрационная страница выглядит так:

# The main HTML for the whole page.
PAGE_HTML = """
<p>Welcome, {name}!</p>
<p>Products:</p>
<ul>
{products}
</ul>
"""

# The HTML for each product displayed.
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. Представьте, как выглядел бы код, если бы страница была в 10 (или 100) раз сложнее. Он быстро становится непригодным для обслуживания.

шаблон

Лучшим способом создания HTML-страниц является использование шаблонов. 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 с некоторой встроенной в него логикой. Сравните этот ориентированный на документы подход с логически ориентированным кодом выше. Предыдущая программа в основном представляет собой код Python, а HTML-код встроен в логику Python. Здесь наша программа представляет собой в основном статическую HTML-разметку.

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

поддерживаемый синтаксис

Механизмы шаблонов различаются синтаксисом, который они поддерживают. Синтаксис нашего шаблона основан на Django, популярном веб-фреймворке. Теперь, когда мы реализовали наш движок на Python, в нашей грамматике появятся некоторые концепции 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>

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

<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! #}

Выполнение

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

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

  • Управление динамическим контекстом, источником данных
  • выполнять логические элементы
  • Реализовать доступ к точке и выполнение фильтра

Что передать от этапа парсинга к этапу рендеринга — это ключ.

Что может дать разбор? Есть два варианта: мы называем их интерпретацией и компиляцией.

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

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

Реализация нашего движка использует модель компиляции: мы компилируем шаблоны в код 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 следующим образом:

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)

Несколько заметок:

  • Код оптимизирован путем кэширования некоторых функций в локальные переменные (такие как append_result = result.append и т.д.)
  • Операция записи через точку преобразуется вdo_dotsфункция
  • Логический код преобразуется в код Python и циклы

Написание шаблонизатора

Класс шаблона

Объект Templite может быть создан с использованием текста шаблона, который затем можно использовать для отображения определенного контекста, словаря данных:

# Make a Templite object.
templite = Templite('''
    <h1>Hello {{name|upper}}!</h1>
    {% for topic in topics %}
        <p>You are interested in {{topic}}.</p>
    {% endfor %}
    ''',
    {'upper': str.upper},
)

# Later, use it to render some data.
text = templite.render({
    'name': "Ned",
    'topics': ['Python', 'Geometry', 'Juggling'],
})

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

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

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

CodeBuilder

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

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

class CodeBuilder(object):
    """Build source code conveniently."""

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

CodeBuilder мало что делает. add_line добавляет новую строку кода, которая автоматически устанавливает отступ текста до текущего уровня отступа и предоставляет новую строку:

def add_line(self, line):
    """Add a line of source to the code.

    Indentation and newline will be added for you, don't provide them.

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

indentиdedentУвеличьте или уменьшите уровень отступа:

INDENT_STEP = 4      # PEP8 says so!

def indent(self):
    """Increase the current indent for following lines."""
    self.indent_level += self.INDENT_STEP

def dedent(self):
    """Decrease the current indent for following lines."""
    self.indent_level -= self.INDENT_STEP

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

def add_section(self):
    """Add a section, a sub-CodeBuilder."""
    section = CodeBuilder(self.indent_level)
    self.code.append(section)
    return section

__str__Генерирует строку со всем кодом, объединяя вместе все строки в self.code. Обратите внимание: поскольку файл self.code может содержать разделы, он может рекурсивно вызывать другиеCodeBuilderОбъект:

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

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

def get_globals(self):
    """Execute the code, and return a dict of globals it defines."""
    # A check that the caller really finished all the blocks they started.
    assert self.indent_level == 0
    # Get the Python source as a single string.
    python_source = str(self)
    # Execute the source, defining globals, and return them.
    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)

ноglobal_namespace['SEVENTEEN']17,global_namespace['three']функция возвратаthree.

Хотя мы используем толькоCodeBuilderдля создания функции, но нет предела тому, что она может сделать. Это упрощает реализацию классов и упрощает их понимание.CodeBuilderПозволяет нам создать фрагмент исходного кода Python, не зная ничего о нашем механизме шаблонов.get_globalsВозвращается словарь, что делает код более модульным, поскольку ему не нужно знать имя функции, которую мы определили. Независимо от того, какое имя функции мы определяем в исходном коде Python, мы можем получить его изget_globalsИмя извлекается из возвращаемого объекта. Теперь мы можем войтиTempliteРеализация самого класса см.CodeBuilderкак и где он используется.

Реализовать класс шаблона

компилировать

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

def __init__(self, text, *contexts):
    """Construct a Templite with the given `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)

Здесь, используя переменные параметры Python, можно передать несколько контекстов, и более поздний переданный будет переопределять ранее переданный.

мы используем множествоall_varsЧтобы записать переменные, используемые в шаблоне, используйтеloop_varsЗапишите переменные, используемые в теле цикла шаблона:

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, он будет принимать два аргумента: контекст — это словарь данных, который он должен использовать, иdo_dotsэто функция, реализующая доступ к атрибуту точки.

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

затем кэшируетсяlistдва метода иstrк локальным переменным, как упоминалось выше, это может повысить производительность кода.

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

buffered = []
def flush_output():
    """Force `buffered` to the code builder."""
    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вызов функции. Надеемся повторитьappendзвонки сливаются в одинextendcall, это точка оптимизации. Чтобы сделать это возможным, мы буферизуем эти блоки.

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

flush_outputФункция - это закрытие. Это упрощает наш призыв к функции: нам не нужно говоритьflush_outputКакой буфер промотал или где его промыть; это знает все это.

Если буферизуется только одна строка, используйтеappend_resultДобавьте его к результату. Если имеется более одного буфера, то он будет использоватьсяextend_resultДобавьте их к результату.

Вернемся к нашему классу Templite. При разборе управляющих структур мы хотим проверить их синтаксическую корректность. Необходимо использовать структуру стекаops_stack:

ops_stack = []

Например, когда мы сталкиваемся с управляющими операторами\{\% if \%\}, мы толкаем стекif. когда мы встретимся\{\% endif \%\}, извлеките стек и проверьте, является ли извлеченный элементif.

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

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

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

(?s)Для однострочного режима точка должна соответствовать новой строке. Далее следуют соответствующие выражения/управляющие структуры/комментарии, все из которых не являются жадными.

Результатом разделения является список строк. Например, этот текст шаблона:

<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('{#'):
        # Comment: ignore it and move on.
        continue
    # 表达式:提取出内容交给 _expr_code 进行处理,然后生成一行代码
    elif token.startswith('{{'):
        # An expression to evaluate.
        expr = self._expr_code(token[2:-2].strip())
        buffered.append("to_str(%s)" % expr)
    # 控制语句
    elif token.startswith('{%'):
        # Action tag: split into words and parse further.
        # 先将前面生成的代码刷新到编译函数之中
        flush_output()
        words = token[2:-2].strip().split()
        if words[0] == 'if':
            # An if statement: evaluate the expression to determine if.
            # if语句只能有两个单词
            if len(words) != 2:
                self._syntax_error("Don't understand if", token)
            # if 入栈
            ops_stack.append('if')
            # 生成代码
            code.add_line("if %s:" % self._expr_code(words[1]))
            # 增加下一条语句的缩进级别
            code.indent()
        elif words[0] == 'for':
            # A loop: iterate over expression result.
            # 语法检查
            if len(words) != 4 or words[2] != 'in':
                self._syntax_error("Don't understand for", token)
            # for 入栈
            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()
        elif words[0].startswith('end'):
            # Endsomething.  Pop the ops stack.
            # 语法检查
            if len(words) != 1:
                self._syntax_error("Don't understand end", token)
            end_what = words[0][3:]
            # end 语句多了
            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()
        else:
            self._syntax_error("Don't understand tag", words[0])
    else:
        # Literal content.  If it isn't empty, output it.
        # 纯文本内容
        if token:
            buffered.append(repr(token))

Несколько замечаний:

  • использоватьreprзаключать текст в кавычки, иначе сгенерированный код будет выглядеть так:
extend_result([
  <h1>Hello , to_str(c_upper(c_name)), !</h1>
  ])
  • использоватьif token:чтобы удалить пустые строки и избежать создания ненужных пустых строк кода

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

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

flush_output()

Ранее мы прошлиvars_code = code.add_section()Создается раздел, роль которого состоит в деконструкции входящего контекста в локальные переменные функции рендеринга.

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

<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_list product.all_varsКоллекции содержат их, потому что они используются в выражениях и операторах управления.

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

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

На этом этапе наш код добавляется вresult, и, наконец, объедините их в строку, и все готово:

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

пройти черезget_globalsМы можем создать функцию рендеринга и сохранить ее в_render_functionначальство:

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

выражение

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

Наше выражение может быть таким же простым, как имя переменной:

{{user_name}}

Он также может быть очень сложным:

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

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

def _expr_code(self, expr):
    """Generate a Python expression for `expr`."""

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

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)

Переменные в нашей функции рендеринга имеют префикс c_, то же самое ниже

Второй случай - нет|, но есть.. затем с.в качестве разделителя первая часть передается_expr_codeОцените, результат такойdo_dotsпервый параметр . Остальные такие жеdo_dotsнеопределенные параметры.

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.zбудет проанализирован как вызов функцииdo_dots(x, 'y', 'z')

Последний случай ничего не содержит. Это относительно просто, возвращая переменную с префиксом напрямую:

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

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

  • обработка ошибок
def _syntax_error(self, msg, thing):
    """Raise a syntax error using `msg`, and showing `thing`."""
    raise TempliteSyntaxError("%s: %r" % (msg, thing))
  • коллекция переменных
def _variable(self, name, vars_set):
    """Track that `name` is used as a variable.

    Adds the name to `vars_set`, a set of variable names.

    Raises an syntax error if `name` is not a valid 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, и процесс рендеринга очень прост. Все, что нам нужно сделать, это получить контекст и вызвать скомпилированную функцию:

def render(self, context=None):
    """Render this template by applying it to `context`.

    `context` is a dictionary of values to use in this rendering.

    """
    # Make the complete context we'll use.
    render_context = dict(self.context)
    if context:
        render_context.update(context)
    return self._render_function(render_context, self._do_dots)

renderФункция сначала объединяет начальные входящие данные и параметры для получения окончательных данных контекста и, наконец, вызывает_render_functionчтобы получить окончательный результат. Наконец, давайте проанализируем_do_dots:

def _do_dots(self, value, *dots):
    """Evaluate dotted expressions at runtime."""
    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'). Вот пример: Во-первых, попробуйте оценить y как свойство объекта x. Если это не удается, он оценивается как ключ. Наконец, если y можно вызвать, сделайте вызов. Затем продолжить ту же операцию с полученным значением в качестве объекта.

TODO

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

  • Наследование и включение шаблонов
  • пользовательский ярлык
  • автоматический побег
  • параметр фильтра
  • Сложная логика управления, такая как else и elif
  • тело цикла с более чем одной переменной цикла
  • космический контроль