2013年2月4日月曜日

Bottleのプラグインの動作方法:基本


プラグインの動作方法:基本

プラグイン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

bottle.install(stopwatch)
このプラグインはリクエストごとに実行時間を測定し、応答に適切なX-EXECタイムヘッダーを追加します。あなたが見ることができるように、プラグインがラッパーを返し、ラッパーは、再帰的に元のコールバックを呼び出します。これは、デコレータは、通常の動作方法です。
最後の行は、デフォルトのアプリケーションにプラグインをインストールするには、ボトルに指示します。これは、プラグインは自動的にそのアプリケーションのすべてのルートに適用されるようになります。言い換えれば、ストップウォッチ()は、各ルートのコールバックに対して一度呼び出され、戻り値は、元のコールバックの代わりとして使用されています。
プラグインは、ルートが初めて要求されるとすぐに、つまりオンデマンドで適用されます。このマルチスレッド環境で正しく動作させるには、プラグインはスレッド·セーフでなければなりません。これはほとんどの時間の問題ではありませんが、それに留意してください。
すべてのプラグインがルートに適用されると、ラップされたコールバックは、キャッシュされ、後続の要求は、直接キャッシュされたバージョンによって処理されます。これは、プラグインは通常、特定のルートに一度だけ適用されることを意味します。そのキャッシュは、しかし、たびにインストールされているプラ​​グインの変更のリストがクリアされます。あなたのプラグインは、複数回同じルートを飾ることができるはずです。
デコレータのAPIは非常にかかわらず、制限されています。あなたがデコレートされたルートまたは、関連付けられたアプリケーションオブジェクトについて何を知っていて、効率的にすべてのルート間で共有されるデータを格納する方法がありませんありません。しかし、恐怖はありません!プラグインは、単にデコレータ関数に限定されるものではない。ボトルは、それが呼び出し可能であるか、または拡張APIを実装している限り、プラグインとして何かを受け入れます。このAPIは、以下に説明すると、処理全体の制御の多くを提供しています。

プラグインAPI

プラグインは、実際のクラス(あなたがボトルからそれをインポートすることはできません)が、プラグインが実装すると予想されているインタフェースではありません。ボトルは、プラグインとして任意の型の任意のオブジェクトを受け入れる限り、それは以下のAPIに準拠している。
class Plugin(object)
プラグインは呼び出し可能であるかは、apply()を実装する必要があります。
apply()適用されるが定義されている場合、それは常に直接プラグインを呼び出すことが優先されます。 他のすべてのメソッドや属性はオプションです。
name
両方Bottle.uninstall()とBottle.routeのスキップ·パラメータは、()プラグインまたはプラグインタイプを参照するために名前の文字列を受け入れます。これは、name属性を持っているプラ​​グインに対してのみ機能します。
api
プラグインAPIは、まだ進化しています。この整数型の属性は、どちらのバージョンを使用するボトルを指示します。最初のバージョンにそれが欠落している場合は、ボトルをデフォルトとします。現在のバージョンは2です。詳細については、プラグインAPIの変更を参照してください。
setup(self, app)
とすぐにプラグインがアプリケーションにインストールされていると呼ばれる(Bottle.install()を参照)。唯一のパラメータは、関連付けられたアプリケーションオブジェクトです。
call(self, callback)
限り定義されていません)(適用されるように、プラグイン自体は、デコレータとして使用され、各ルートのコールバックに直接適用されます。唯一のパラメータは、飾るためのコールバックです。このメソッドによって返される任意の元のコールに置き換えられます。与えられたコールバック関数をラップまたは交換する必要はありません場合は、単に変更されていないコールバックパラメータを返します。
apply(self, callback, route)¶
定義されている場合、このメソッドはルートのコールバックを飾るためにcallを通じて()に有利に使用されています。追加ルートパラメータは、ルートのインスタンスであり、そのルートのメタ情報とコンテキストの多くを提供しています。詳細については、ルートコンテキストを参照してください。
close(self)¶
プラグインがアンインストールされるか、またはアプリケーションが閉じられる直前に呼び出されます。
(Bottle.uninstall()またはBottle.close()を参照してください)​​
Plugin.setup()とPlugin.close()の両方がBottle.route()デコレータを経由して、だけのアプリケーションにインストールされているプラ​​グインのルートに直接適用されているプラ​​グインを呼び出されません。

プラグインAPIの変更

プラグインAPIはまだ発展途上とルートコンテキストの辞書を持つ特定の問題に対処するためにボトル0.10で変更されます。 0.9プラグインとの下位互換性を確保するために、我々は、どのAPIが使用するように瓶を指示するオプションのPlugin.api属性を追加しました。 APIの違いはここに要約されています。
ボトル0.9 API 1(Plugin.api存在しない)
0.9のドキュメントで説明するようにオリジナルのプラグインAPI。
ボトル0.10 API 2(Plugin.apiは2に等しい)
Plugin.apply()メソッドのcontextパラメータは現在、代わりにコンテキスト辞書のルートのインスタンスです。

ルートコンテキスト

Plugin.applyに渡されたルート·インスタンスは、()関連付けられたルートに関する詳細な情報を提供しています。最も重要な属性は以下にまとめています:

属性の説明

アプリは、アプリケーションオブジェクトは、このルートは次のようにインストールされています。
AttributeDescription
appThe application object this route is installed to.
ruleThe rule string (e.g. /wiki/:page).
methodThe HTTP method as a string (e.g. GET).
callbackThe original callback with no plugins applied. Useful for introspection.
nameThe name of the route (if specified) or None.
pluginsA list of route-specific plugins. These are applied in addition to application-wide plugins. (see Bottle.route()).
skiplistA list of plugins to not apply to this route (again, see Bottle.route()).
configAdditional keyword arguments passed to the Bottle.route() decorator are stored in this dictionary. Used for route-specific configuration and meta-data.
プラグインについては、Route.configは、おそらく最も重要な属性です。この辞書は、ルートに対してローカルであることを心に留めておくが、すべてのプラグイン間で共有されます。それは常にあなたのプラグインが設定ディクショナリ内の別の名前空間に格納し、コンフィギュレーションの多くを必要とする場合、一意の接頭辞を追加したりすることをお勧めします。これは、プラグイン間での名前の衝突を回避するのに役立ちます。

Routeオブジェクトを変更する

いくつかのルートの属性は可変ですが、変更が他のプラグイン上で望ましくない影響があるかもしれません。それは猿パッチを適用する可能性が最も高い代わりに、有用なエラーメッセージを提供し、ユーザーが問題を解決させるの壊れたルートは悪い考えです。
まれに、しかし、それはこのルールを破ることが正当かもしれません。あなたはルートインスタンスに変更を加えた後、例外としてRouteResetが発生します。これは、キャッシュから現在のルートを削除し、すべてのプラグインが再適用されるようになります。ルータは、しかし、更新されません。ルールまたはメソッドの値の変更は、ルータに影響を与えませんが、プラグインだけで。ただし、これは将来変更されるかもしれません。

ランタイムの最適化

すべてのプラグインがルートに適用されると、ラップされたルートのコールバックは、後続の要求をスピードアップするためにキャッシュされます。プラグインの動作は設定に依存し、実行時にその設定を変更することができるようにしたい場合は、要求ごとに設定を読む必要があります。十分に簡単です。
パフォーマンス上の理由から、しかし、それは、現在のニーズに基づいて、さまざまなラッパーを選択して、クロージャで動作する、または実行時にプラグインを有効または無効にする価値があるかもしれません。の例として、組み込みHooksPluginてみましょう。ないフックがインストールされていない場合、プラグインが影響を受けるすべてのルートから自身を削除し、virtaullyないオーバーヘッドがありません。できるだけ早くあなたが最初のフックをインストールすると、プラグイン自体を活性化し、再び有効になります。
これを実現するには、コールバックのキャッシュを制御する必要があります。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
このプラグインは、実際に役に立つとボトルにバンドルされているバージョンと非常に似ています。コー​​ド未満の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のキーワード引数を要求することにより、ハンドルを作成するためのプラグインに指示します。 2つ目のルートは、データベースを必要としないので、プラグインによって無視されます。第三のルートは 'db'とキーワード引数を期待していますが、明示的にsqliteのプラグインをスキップします。この方法では引数はプラグインによって無効にしても同じ名前のURL引数の値が含​​まれていません。

0 件のコメント:

コメントを投稿