The Dabsong Conshirtoe

技術系の話を主にします。

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

流れ

  1. sys._getframeしてwrappedが関数なのかクラスなのかなどを判定
  2. wrappedの__venusian_callbacks__メンバに値が設定されていなければ、Categoriesインスタンスを新たに生成し、設定。
  3. Categoriesインスタンスに対して、引数categoryをキーにcallbackを登録する。複数登録できるようlistになっている。
  4. 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で指定可能。

流れ

  1. packageに対してinspect.getmembersし、packageに所属するメンバーを取得。
  2. 各メンバーオブジェクトに対して内部関数invokeを適用。ここでcallbackが呼ばれる。

    python def invoke(mod_name, name, ob): """ mod_name: scan中のモジュール名 name: getmembersで得られたオブジェクト名 ob: nameに対応するオブジェクト """

    1. ignoreに指定されたオブジェクトでないかチェック。指定されたオブジェクトならreturn。
    2. __venusian_callbacks__メンバ(attachで登録されたCategoryインスタンス)があり、なおかつそのCategoryインスタンスが持つattached_toメンバがobと等しければ次に進む。
    3. callback呼び出しの対象となるcategoryを決める。scan関数のcategoriesパラメータが指定されていなければ、登録されているcategoryをすべて対象に含める。
    4. 各categoryに紐付いたcallback関数を取得し、実行する。
  3. packageが__path__を持つか検証。持っていなければreturn
  4. packageに対してwalk_package関数を呼び出し、そのパッケージに属するモジュールのモジュールローダーのイテレータを取得する。 ※この関数はpkgutil.walk_packagesとほぼ同じだが、ignoreを指定させたいので実装している。
  5. walk_packageの結果新しい未importのモジュールが見つかればimportし、そのモジュールに対してinspect.getmembers, invokeを行う。

感想

デコレートされたオブジェクトのメタ情報を結局はその関数自身にもたせている。(__venusian_callbacks__メンバ)

この辺は動的にメンバーを追加できる言語ならではか。言語として関数にメタ情報を含ませる仕組みがあるといいと思うのだけど。

[追記] python3のアノテーションの仕組みと共存できるようなやり方ができればいいかも。 func.annotationsに関数自身のメタ情報を持てるようにする、とか。 python3のアノテーションは引数と戻り値に関するメタ情報は持てるが、それ以外の情報(例えばjavaの@Deprecatedとか)は持てない。