Руководство по разработке плагинов
Это руководство объясняет 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