Spec-Zone.ru › Bottle 0.12

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

Это руководство объясняет 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() принимают строку имени для ссылки на плагин или тип плагина. Это работает только для плагинов, у которых есть атрибут name.

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.12/plugindev.html

Spec-Zone.ru

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