virtualenvwrapper を拡張する

開発環境をカスタマイズするために自作で解決してきた長い経験から、共通タスクを自動化して、何度も繰り返す苛々するような作業を取り除く機能がどれほどの価値をもつか分かりました。大工は治具を組み立て、ソフトウェア開発者はシェルスクリプトを書きます。virtualenvwrapper は逆になりますが、求める方法で動作するようにツールを修正する職人を励ます伝統を受け継いでいます。

繰り返し行う手動の操作を取り除いたり開発ワークフローを効率化するために提供されるフックを使用してください。例えば、最後に編集されたセッションからファイルを再読み込みするために IDE にプロジェクトファイルを読む込ませる、時間追跡記録を管理する、もしくはアプリケーションサーバの開発バージョンを起動・停止するために pre_activatepost_activate フックを設定してください。 initialize は virtualenvwrapper に対するフックや完全に新しいコマンドを追加するためのフックです。そして pre_mkvirtualenvpost_mkvirtualenv といったフックはそれぞれの新しい開発環境へ基本的な必需品をインストールする、ソースコードリポジトリの初期化、その他の新たなプロジェクトの設定を行うといった機会を与えます。

virtualenvwrapper がそういったことを実行できるようにあなたのコードをアタッチする方法が2つあります。エンドユーザはシェルスクリプトか、個人的なカスタマイズを施したプログラムを使用できます(ユーザカスタマイズ を参照)。さらに拡張機能は、システムと開発者間で共通の振る舞いを共有できるようにする Distribute エントリポイント を使用して Python で実装することもできます。

拡張機能を定義する

Note

virtualenvwrapper はユーザのカスタムスクリプトを作成して実行することをプラグインで実現します(user_scripts)。次のサンプルはそういったプラグインの実装を紹介します。

コードの構成

virtualenvwrapper の Python パッケージは 名前空間パッケージ です。複数のライブラリが一緒に配布されていなかったり同じディレクトリ内にインストールされていなかったとしても、そのパッケージ内へインストールできます。拡張機能は次のようにソースツリーを設定することで virtualenvwrapper の名前空間を(オプションで)使用することが出来ます。

  • virtualenvwrapper/
    • __init__.py
    • user_scripts.py

そして __init__.py に次のコードを含めます。

"""virtualenvwrapper module
"""

__import__('pkg_resources').declare_namespace(__name__)

Note

拡張機能はどんなパッケージからも読み込まれるので virtualenvwrapper の名前空間を使用する必要はありません。

拡張 API

パッケージを作成した後の次のステップとして、拡張コードを保持するモジュールを作成します。例えば virtualenvwrapper/user_scripts.py です。そのモジュールは実際の拡張機能のエントリポイントを含みます。サポートするコードが含められるか、標準の Python コードの構成テクニックを利用してインポートされます。

API は全ての拡張ポイントで同じです。それぞれは1つの引数、つまりコマンドライン上でフックローダへ渡される文字列のリストを受け取る Python 関数を使用します。

def function_name(args):
    # args is a list of strings passed to the hook loader

引数リストのコンテンツは次の拡張ポイント毎に定義されます(拡張ポイント を参照)。

拡張機能の起動

ダイレクトアクション

プラグインは2つの方法でそれぞれのフックをアタッチできます。デフォルトは直接的に何らかの処理を実行する関数を持ちます。例えば、ユーザスクリプトプラグインの initialize() 関数は virtualenvwrapper.sh が読み込まれるときにデフォルトユーザスクリプトを作成します。

def initialize(args):
    for filename, comment in GLOBAL_HOOKS:
        make_hook(os.path.join('$WORKON_HOME', filename), comment)
    return

ユーザ環境を変更する

拡張機能がユーザ環境のアップデートを必要とするケースがあります(例えば、カレントワークディレクトリを変更したり、環境変数を設定する等)。ユーザ環境に対する変更はユーザのカレントシェル内で行われなければならず、独立したプロセスで実行できません。ユーザのシェルプロセスで実行するコードを持つために、拡張機能は実行されるシェル構文のテキストを返すフック関数を定義できます。これらの source フックは同じ名前を持つ通常のフックの後で実行されます。そして、そのフック内部で処理を行ってはいけません。

ユーザスクリプトプラグインの initialize_source() フックは、グローバルな初期化スクリプトを調べてカレントのシェルプロセスでそのスクリプトを実行させます。

def initialize_source(args):
    return """
#
# Run user-provided scripts
#
[ -f "$WORKON_HOME/initialize" ] && source "$WORKON_HOME/initialize"
"""

Warning

拡張機能はユーザのワークシェルを変更しているので、予期しない既存変数の上書きにより環境が汚染されないように注意しなければなりません。できるだけ一時的な変数を作成せずに、必要なところで一意な名前を使用してください。接頭辞として拡張名が付く変数は名前空間を管理するのに良い方法です。例えば、 temp_file ではなく user_scripts_temp_file を使用してください。一時的な変数は必要なくなったときに unset で解放してください。

Warning

virtualenvwrapper は構文が少し違う複数のシェル(bash, sh, zsh, ksh)で動作します。ソースフックを定義するときにアカウント内にこの移植性を考慮してください。最も簡単な構文のみを使用することで普通は問題ありませんが、求める結果を得るためには唯一の方法しかないケースにおいて、違う構文を生成する SHELL 環境変数を調べる可能性があります。

エントリポイントを登録する

プラグインで定義された関数は virtualenvwrapper のフックローダが見つけられるために エントリポイント として登録する必要があります。 Distribute エントリポイントは関数を実装するパッケージでその関数に対するエントリポイントの名前をマッピングすることにより、そのパッケージの setup.py で設定されます。

この virtualenvwrapper の setup.py の一部は initialize()initialize_source() エントリポイントの設定方法を説明します。

# Bootstrap installation of Distribute
import distribute_setup
distribute_setup.use_setuptools()

from setuptools import setup

setup(
    name = 'virtualenvwrapper',
    version = '2.0',

    description = 'Enhancements to virtualenv',

    # ... details omitted ...

    namespace_packages = [ 'virtualenvwrapper' ],

    entry_points = {
        'virtualenvwrapper.initialize': [
            'user_scripts = virtualenvwrapper.user_scripts:initialize',
            ],
        'virtualenvwrapper.initialize_source': [
            'user_scripts = virtualenvwrapper.user_scripts:initialize_source',
            ],

        # ... details omitted ...
        },
    )

setup() への entry_points 引数はエントリポイントの指定子を表示するエントリポイント グループ名 をマッピングするディクショナリです。違うグループ名はそれぞれの拡張ポイントのために virtualenvwrapper により定義されます(拡張ポイント を参照)。

エントリポイント指定子は name = package.module:function という構文の文字列です。慣例からエントリポイントの 名前 はプラグインの名前を付けますが、必須だというわけではありません (その名前を使わなくても構いません) 。

フックローダ

拡張機能は virtualenvwrapper.hook_loader で実装されたコマンドラインアプリケーションを通して実行されます。 virtualenvwrapper.sh がプライマリの呼び出しであり、ユーザはそのアプリケーションを直接的に実行する必要はないので、分割されたスクリプトはインストールされません。その代わり、そのアプリケーションを実行するにはインタープリタに -m オプションを指定してください。

$ python -m virtualenvwrapper.hook_loader -h
Usage: virtualenvwrapper.hook_loader [options] <hook> [<arguments>]

Manage hooks for virtualenvwrapper

Options:
  -h, --help            show this help message and exit
  -s, --source          Print the shell commands to be run in the current
                        shell
  -l, --list            Print a list of the plugins available for the given
                        hook
  -v, --verbose         Show more information on the console
  -q, --quiet           Show less information on the console
  -n NAMES, --name=NAMES
                        Only run the hook from the named plugin

initialize フックのためにその拡張機能を実行するには次のようにします。

$ python -m virtualenvwrapper.hook_loader -v initialize

initialize フックのためにシェルコマンドを読み込むには次のようにします。

$ python -m virtualenvwrapper.hook_loader --source initialize

実際は、フックローダが直接フックを実行するよりも両方のモードでフックを実行するシェル関数 virtualenvwrapper_run_hook を使用する方がもっと便利です。

$ virtualenvwrapper_run_hook initialize

シェル関数に与えられた全ての引数はフックローダへ直接渡されます。

ロギング

フックローダはログメッセージを $WORKON_HOME/hook.log に書き込むように設定します。またログメッセージは冗長フラグにより標準エラーにも出力されます。デフォルトでは、ログメッセージは info かそれ以上のレベルが標準エラーへ出力され、 debug かそれ以上がログファイルへ書き込まれます。この方法でロギングを使用することでユーザに拡張機能の冗長性を制御する便利な仕組みを提供します。

拡張機能からロギングを使用するには、単純にロガーをインスタンス化して、ログメッセージと共にその info(), debug() やその他のメソッドを呼び出してください。

import logging
log = logging.getLogger(__name__)

def pre_mkvirtualenv(args):
    log.debug('pre_mkvirtualenv %s', str(args))
    # ...

拡張ポイント

ネイティブプラグインの拡張ポイントの名前は複数のパートを持つ命名規則 virtualenvwrapper.(pre|post)_<event>[_source] に従います。 <event> は拡張機能が引き起こす virtualenvwrapper またはユーザによるアクションです。 (pre|post) はその拡張機能の呼び出しがイベントの前か後かのどちらかを指します。接尾辞 _source は直接アクションを受け取らずにシェルスクリプトのコードを返す拡張機能に追加されます(ユーザ環境を変更する を参照)。

get_env_details

virtualenvwrapper.get_env_details フックは workon が引数無しで実行されるときに実行されます。そして、仮想環境のリストを表示します。仮想環境の名前が表示された後で、そのフックは環境毎に一度実行されて、その環境に関する追加情報を表示します。

initialize

virtualenvwrapper.initialize フックは virtualenvwrapper.sh が環境に読み込まれる毎に実行されます。initialize フックは設定ファイルのテンプレートをインストールしたり、適切なプラグイン操作のためにシステムを整備するために使用されます。

pre_mkvirtualenv

virtualenvwrapper.pre_mkvirtualenv フックは仮想環境が作成された後で実行されますが、新しい環境がアクティブ化される前に実行されます。そのフックが実行されるときのためにカレントワークディレクトリは $WORKON_HOME で、1つの引数として新しい環境の名前が渡されます。

post_mkvirtualenv

virtualenvwrapper.post_mkvirtualenv フックは新しい仮想仮想が作成されて、アクティブ化された後で実行されます。 $VIRTUAL_ENV は新しい環境を指すようにセットされます。

pre_activate

virtualenvwrapper.pre_activate フックは仮想環境が有効になる前に実行されます。環境の名前は1番目の引数として渡されます。

post_activate

virtualenvwrapper.post_activate フックは仮想環境が有効になった後で実行されます。 $VIRTUAL_ENV はカレント環境を指すようにセットされます。

pre_deactivate

virtualenvwrapper.pre_deactivate フックは仮想環境が無効になる前に実行されます。 $VIRTUAL_ENV はカレント環境を指すようにセットされます。

post_deactivate

virtualenvwrapper.post_deactivate フックは仮想環境が無効になった後で実行されます。非アクティブ化される環境の名前は1番目の引数として渡されます。

pre_rmvirtualenv

virtualenvwrapper.pre_rmvirtualenv フックは仮想環境が削除される前に実行されます。削除される環境の名前は1番目の引数として渡されます。

post_rmvirtualenv

virtualenvwrapper.post_rmvirtualenv フックは仮想環境が削除された後で実行されます。削除される環境の名前は1番目の引数として渡されます。

新しい拡張ポイントを追加する

さらに新しい操作を定義するプラグインは新しい拡張ポイントも定義できます。フックローダが拡張機能を見つけるために行う設定は必要ありません。名前を記述して virtualenvwrapper_run_hook の呼び出しを追加することで、追加した拡張機能が実行されるようになります。

フックローダは全ての拡張ポイントの名前が virtualenvwrapper. で始まることを前提としています。そして、新しいプラグインは独自の名前空間の修飾語句をその接頭辞に追加したくなるでしょう。例えば project 拡張はプロジェクトのディレクトリ作成(前後)に関連して新たなイベントを定義します。そこで virtualenvwrapper.project.pre_mkprojectvirtualenvwrapper.project.post_mkproject が呼び出されます。それは次のように1つずつ実行されます。

virtualenvwrapper_run_hook project.pre_mkproject $project_name

virtualenvwrapper_run_hook project.post_mkproject

です。