venusianライブラリの実装
デコレータの実行を遅延させる仕組み。デコレートされた関数のテストがしやすくなったりする。 メタ情報を付与するだけのアノテーションのようにデコレータを使うことができそう。
pyramidのview_config関数で使用されている。 http://docs.pylonsproject.org/projects/venusian/en/latest/
使い方
デコレータとなる関数の作成
vensuian.attachにより、あとでスキャンしたときに実行される関数が登録される。
# theframework.py import venusian def jsonify(wrapped): def callback(scanner, name, ob): def jsonified(request): result = wrapped(request) return json.dumps(result) scanner.registry.add(name, jsonified) venusian.attach(wrapped, callback) return wrapped
デコレータ関数を使用
# theapp.py from theframework import jsonify @jsonify def logged_in(request): return {'result':'Logged in'}
この時点では、jsonify関数の実態(callback関数)はまだ呼ばれていない。
スキャンする
import venusian import theapp class Registry(object): def __init__(self): self.registered = [] def add(self, name, ob): self.registered.append((name, ob)) registry = Registry() scanner = venusian.Scanner(registry=registry) scanner.scan(theapp)
この時点で、callback関数が呼ばれ、scanner.registryにjsonfiy関数の中で定義されたjsonified関数が登録される。 カテゴリごとにわけてscanすることなどもできる。(ドキュメント参照)
実装(ざっくり)
attach
スキャン対象としてオブジェクトを登録する関数。
パッケージ
venusian/init.py
実装
def attach(wrapped, callback, category=None, depth=1): """ Attach a callback to the wrapped object. It will be found later during a scan. This function returns an instance of the :class:`venusian.AttachInfo` class."""
wrappedの__venusian_callbacks__
メンバに、callback関数を登録する。
callback関数のホルダーにはCategoriesクラスを使っている。
# カテゴリ名をキー、コールバック関数のリストを値として持つクラス。 class Categories(dict): def __init__(self, attached_to): super(dict, self).__init__() if attached_to is None: self.attached_id = None else: self.attached_id = id(attached_to) def attached_to(self, obj): if self.attached_id: return self.attached_id == id(obj) return True
流れ
- sys._getframeしてwrappedが関数なのかクラスなのかなどを判定
- wrappedの
__venusian_callbacks__
メンバに値が設定されていなければ、Categoriesインスタンスを新たに生成し、設定。 - Categoriesインスタンスに対して、引数categoryをキーにcallbackを登録する。複数登録できるようlistになっている。
- Categoriesインスタンスやwrappedの情報を含んだAttachInfoクラスを生成して返す。
Scanner.scan
attachにて登録されたオブジェクトをスキャンし、callback関数を実行する。
パッケージ
venusian/init.py
実装
class Scanner(object): ... def scan(self, package, categories=None, onerror=None, ignore=None): """ Scan a Python package and any of its subpackages. All top-level objects will be considered; those marked with venusian callback attributes related to ``category`` will be processed. ...
attachされたオブジェクトに登録されたcallback関数を呼び出す。どれを対象とするかはcategoryやignoreで指定可能。
流れ
- packageに対してinspect.getmembersし、packageに所属するメンバーを取得。
各メンバーオブジェクトに対して内部関数invokeを適用。ここでcallbackが呼ばれる。
python def invoke(mod_name, name, ob): """ mod_name: scan中のモジュール名 name: getmembersで得られたオブジェクト名 ob: nameに対応するオブジェクト """
- packageが
__path__
を持つか検証。持っていなければreturn - packageに対して
walk_package
関数を呼び出し、そのパッケージに属するモジュールのモジュールローダーのイテレータを取得する。 ※この関数はpkgutil.walk_packages
とほぼ同じだが、ignoreを指定させたいので実装している。 - walk_packageの結果新しい未importのモジュールが見つかればimportし、そのモジュールに対してinspect.getmembers, invokeを行う。
感想
デコレートされたオブジェクトのメタ情報を結局はその関数自身にもたせている。(__venusian_callbacks__
メンバ)
この辺は動的にメンバーを追加できる言語ならではか。言語として関数にメタ情報を含ませる仕組みがあるといいと思うのだけど。
[追記] python3のアノテーションの仕組みと共存できるようなやり方ができればいいかも。 func.annotationsに関数自身のメタ情報を持てるようにする、とか。 python3のアノテーションは引数と戻り値に関するメタ情報は持てるが、それ以外の情報(例えばjavaの@Deprecatedとか)は持てない。