Spec-Zone.ru › Bottle 0.11

Руководство по разработке плагинов

Это руководство объясняет API плагинов и как писать пользовательские плагины. Рекомендуется сначала прочитать Плагины, если вы этого ещё не сделали. Возможно, вам также стоит взглянуть на Список доступных плагинов для получения практических примеров.

Примечание

Это черновик. Если вы обнаружите какие-либо ошибки или считаете, что какой-либо раздел не достаточно ясно объяснён, сообщите об этом на списке рассылки или подайте отчет об ошибке.

Как работают плагины: Основы

API плагинов строится на основе концепции декораторов. Короче говоря, плагин — это декоратор, применяемый ко всем обработчикам маршрутов приложения.

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

from bottle import response, install
import time

def stopwatch(callback):
    def wrapper(*args, **kwargs):
        start = time.time()
        body = callback(*args, **kwargs)
        end = time.time()
        response.headers['X-Exec-Time'] = str(end - start)
        return body
    return wrapper

install(stopwatch)

Этот плагин измеряет время выполнения каждого запроса и добавляет соответствующий X-Exec-Time заголовок в ответ. Как видите, плагин возвращает обёртку, а обёртка рекурсивно вызывает оригинальный обработчик. Так обычно работают декораторы.

Последняя строка сообщает Bottle об установке плагина в приложении по умолчанию. Это приводит к тому, что плагин автоматически применяется ко всем маршрутам этого приложения. Другими словами, stopwatch() вызывается один раз для каждого обработчика маршрута, а возвращаемое значение используется вместо оригинального обработчика.

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

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

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

API плагинов

Plugin — не реальный класс (вы не можете импортировать его из bottle), а интерфейс, который должны реализовывать плагины. Bottle принимает любой объект любого типа в качестве плагина, если он соответствует следующему API.

class Plugin(object)

Плагины должны быть вызываемыми или реализовывать apply(). Если apply() определён, он всегда имеет приоритет по сравнению с прямым вызовом плагина. Все остальные методы и атрибуты являются необязательными.

name

И Bottle.uninstall(), и skip параметр Bottle.route() принимают строку с именем для ссылки на плагин или тип плагина. Это работает только для плагинов, имеющих атрибут имени.

api

API плагинов всё ещё развивается. Этот целочисленный атрибут сообщает Bottle, какую версию использовать. Если он отсутствует, Bottle использует первую версию. Текущая версия — 2. Подробности см. в Изменения API плагинов.

setup(self, app)

Вызывается как только плагин устанавливается в приложение (см. Bottle.install()). Единственным параметром является связанный объект приложения.

__call__(self, callback)

Пока apply() не определён, сам плагин используется как декоратор и применяется непосредственно к каждому обработчику маршрута. Единственным параметром является обработчик для декорирования. То, что возвращает этот метод, заменяет оригинальный обработчик. Если нет необходимости обёртывать или заменять заданный обработчик, просто верните неизменённый параметр обработчика.

apply(self, callback, route)

Если определён, этот метод используется вместо __call__() для декорирования обработчиков маршрутов. Дополнительный route параметр — это экземпляр Route, предоставляющий много метаинформации и контекста для этого маршрута. Подробности см. в Контексте маршрута.

close(self)

Вызывается непосредственно перед тем, как плагин будет удалён или приложение закрыто (см. Bottle.uninstall() или Bottle.close()).

И Plugin.setup(), и Plugin.close() не вызываются для плагинов, которые применяются напрямую к маршруту через декоратор Bottle.route(), но только для плагинов, установленных в приложении.

Изменения API плагинов

API плагинов всё ещё развивается и изменился в Bottle 0.10, чтобы устранить определённые проблемы с контекстным словарем маршрута. Для обеспечения обратной совместимости с плагинами 0.9 мы добавили необязательный атрибут Plugin.api, чтобы сообщить Bottle, какой API использовать. Различия в API обобщены здесь.

  • API Bottle 0.9 1 (Plugin.api отсутствует)
    • Оригинальный API плагинов, как описано в документации 0.9.
  • API Bottle 0.10 2 (Plugin.api равно 2)
    • Параметр context метода Plugin.apply() теперь является экземпляром Route, а не контекстным словарем.

Контекст маршрута

Экземпляр Route, переданный в Plugin.apply(), предоставляет подробную информацию о связанном маршруте. Наиболее важные атрибуты обобщены здесь:

Атрибут Описание
app Объект приложения, к которому установлен этот маршрут.
rule Строка правила (например, /wiki/:page).
method Метод HTTP в виде строки (например, GET).
callback Оригинальный обработчик без применённых плагинов. Полезно для интроспекции.
name Имя маршрута (если задано) или None.
plugins Список плагинов, специфичных для маршрута. Они применяются в дополнение к плагинам, глобальным для приложения. (см. Bottle.route()).
skiplist Список плагинов, которые не нужно применять к этому маршруту (ещё раз, см. Bottle.route()).
config Дополнительные ключевые аргументы, переданные декоратору Bottle.route(), хранятся в этом словаре. Используются для конфигурации и метаданных, специфичных для маршрута.

Для вашего плагина Route.config — вероятно, самый важный атрибут. Помните, что этот словарь локален для маршрута, но общий для всех плагинов. Рекомендуется добавить уникальный префикс или, если ваш плагин нуждается в много конфигурации, сохранить его в отдельном пространстве имён внутри config словаря. Это помогает избежать конфликтов имён между плагинами.

Изменение объекта Route

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

В некоторых редких случаях, однако, может быть оправдано нарушение этого правила. После внесения изменений в экземпляр Route, вызовите RouteReset в качестве исключения. Это удаляет текущий маршрут из кэша и вызывает повторное применение всех плагинов. Однако маршрутизатор не обновляется. Изменения в rule или method значениях не влияют на маршрутизатор, но только на плагины. Хотя это может измениться в будущем.

Оптимизация во время выполнения

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

Однако по соображениям производительности может быть целесообразно выбрать другой обёртку в зависимости от текущих потребностей, работать с замыканиями или включать/отключать плагин во время выполнения. Рассмотрим встроенный плагин HooksPlugin в качестве примера: если плагины не установлены, плагин удаляет себя из всех затронутых маршрутов и практически не накладывает нагрузки. Как только вы установите первый хук, плагин активируется и снова начинает действовать.

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

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

Пример плагина: SQLitePlugin

Этот плагин предоставляет обработчик подключения к базе данных sqlite3 в качестве дополнительного ключевого аргумента к вызываемым обратным вызовам, но только если обратный вызов его ожидает. Если нет, маршрут игнорируется, и не добавляется дополнительная нагрузка. Обёртка не влияет на возвращаемое значение, но корректно обрабатывает исключения, связанные с плагином. Plugin.setup() используется для проверки приложения и поиска конфликтующих плагинов.

import sqlite3
import inspect

class SQLitePlugin(object):
    ''' This plugin passes an sqlite3 database handle to route callbacks
    that accept a `db` keyword argument. If a callback does not expect
    such a parameter, no connection is made. You can override the database
    settings on a per-route basis. '''

    name = 'sqlite'
    api = 2

    def __init__(self, dbfile=':memory:', autocommit=True, dictrows=True,
                 keyword='db'):
         self.dbfile = dbfile
         self.autocommit = autocommit
         self.dictrows = dictrows
         self.keyword = keyword

    def setup(self, app):
        ''' Make sure that other installed plugins don't affect the same
            keyword argument.'''
        for other in app.plugins:
            if not isinstance(other, SQLitePlugin): continue
            if other.keyword == self.keyword:
                raise PluginError("Found another sqlite plugin with "\
                "conflicting settings (non-unique keyword).")

    def apply(self, callback, context):
        # Override global configuration with route-specific values.
        conf = context.config.get('sqlite') or {}
        dbfile = conf.get('dbfile', self.dbfile)
        autocommit = conf.get('autocommit', self.autocommit)
        dictrows = conf.get('dictrows', self.dictrows)
        keyword = conf.get('keyword', self.keyword)

        # Test if the original callback accepts a 'db' keyword.
        # Ignore it if it does not need a database handle.
        args = inspect.getargspec(context.callback)[0]
        if keyword not in args:
            return callback

        def wrapper(*args, **kwargs):
            # Connect to the database
            db = sqlite3.connect(dbfile)
            # This enables column access by name: row['column_name']
            if dictrows: db.row_factory = sqlite3.Row
            # Add the connection handle as a keyword argument.
            kwargs[keyword] = db

            try:
                rv = callback(*args, **kwargs)
                if autocommit: db.commit()
            except sqlite3.IntegrityError, e:
                db.rollback()
                raise HTTPError(500, "Database Error", e)
            finally:
                db.close()
            return rv

        # Replace the route callback with the wrapped one.
        return wrapper

Этот плагин на самом деле полезен и очень похож на версию, поставляемую с Bottle. Неплохо для менее чем 60 строк кода, не так ли? Вот пример использования:

sqlite = SQLitePlugin(dbfile='/tmp/test.db')
bottle.install(sqlite)

@route('/show/:page')
def show(page, db):
    row = db.execute('SELECT * from pages where name=?', page).fetchone()
    if row:
        return template('showpage', page=row)
    return HTTPError(404, "Page not found")

@route('/static/:fname#.*#')
def static(fname):
    return static_file(fname, root='/some/path')

@route('/admin/set/:db#[a-zA-Z]+#', skip=[sqlite])
def change_dbfile(db):
    sqlite.dbfile = '/tmp/%s.db' % db
    return "Switched DB to %s.db" % db

Первый маршрут нуждается в подключении к базе данных и сообщает плагину создать обработчик, запросив ключевой аргумент db. Второй маршрут не нуждается в базе данных и поэтому игнорируется плагином. Третий маршрут ожидает ключевой аргумент «db», но явно пропускает плагин sqlite. Таким образом, аргумент не перекрывается плагином и по-прежнему содержит значение аргумента url с таким же именем.

© 2009–2017 Marcel Hellkamp
Licensed under the MIT License.
https://bottlepy.org/docs/0.11/plugindev.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API