venusian


デコレータの実行を遅延させる仕組み。デコレートされた関数のテストがしやすくなったりする。
メタ情報を付与するだけのアノテーションのようにデコレータを使うことができそう。

pyramidのview_config関数で使用されている。
http://docs.pylonsproject.org/projects/venusian/en/latest/

# 使い方

1. デコレータ関数を定義するときに、vensuian.attachを呼び出す。

	```python
	# 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
	```

2. 1の関数をデコレータとして呼び出す

	```python
	# theapp.py

	from theframework import jsonify

	@jsonify
	def logged_in(request):
    		return {'result':'Logged in'}
	```
	jsonify関数の実態（callback関数）はまだ呼ばれていない。

3. スキャンする

	```python
	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関数でデコレートされたlogged_in関数が入ることになる。
カテゴリごとにわけてscanすることなどもできる。（ドキュメント参照）

# 実装（ざっくり）
## attach
### パッケージ
venusian/__init__.py

### 実装

```python
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クラスを使っている。

```python
# カテゴリ名をキー、コールバック関数のリストを値として持つクラス。
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が関数なのかクラスなのかなどを判定
1. wrappedの`__venusian_callbacks__`メンバに値が設定されていなければ、Categoriesインスタンスを新たに生成し、設定。
1. Categoriesインスタンスに対して、引数categoryをキーにcallbackを登録する。複数登録できるようlistになっている。
1. Categoriesインスタンスやwrappedの情報を含んだAttachInfoクラスを生成して返す。


## Scanner.scan
### パッケージ
venusian/__init__.py

### 実装
```python
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に所属するメンバーを取得。
1. 各メンバーオブジェクトに対して内部関数invokeを適用。ここでcallbackが呼ばれる。

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

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

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

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

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