前回の続きです。
hello_world.pyだと色々とやりづらいので、
plugins以下にhello_worldという形でモジュール化しておきます。
$ ls ~/.bazaar/plugins
hello_world.py
$ cd ~/.bazaar/plugins
$ mkdir hello_world
$ mv hello_world.py hello_world/__init__.py
2-4.helloコマンドに引数を与える
bzr stなどにLOCATIONを指定できるように、
自分で作ったプラグインにもコマンドライン引数を設定する事が可能です。
というわけで、helloコマンドにユーザ名を指定して
hello usernameという形に出力するようにプラグインを拡張してみます。
引数を指定したい場合はtakes_argsというプロパティをクラス内に定義して、
取得したい要素を配列で定義します。そしてそこで定義した値をrun関数の引数で受け取る事が可能です。
# __init__.py
# クラス部分だけ抜粋
class cmd_hello(Command):
takes_args = ["username?"]
def run(self, username=None):
print "Hello %s" % username
末尾に?をつけると任意要素に、つけない場合は必須要素になります。
他に末尾に指定できる項目は、?, *, +, $あたりがあるようですが詳しくは追っていません。
bzlib.command.pyの_match_argformを見る感じだと、+だと最低1つは存在し、かつ以降の値をまとめるとかできる感じですね。
詳しくはTODOにして後で検証してみます。
必須項目にしていた場合に設定せずにコマンド起動すると、ちゃんとbzrが叱ってくれます。
bzr: ERROR: command ‘hello’ requires argument USERNAME
2-5.オプションの設定
bzrのコマンドには引数ではなく、各種Optionの設定を行う事が出来ます。
どのコマンドでも利用できる-vや-hと言ったオプションと、例えばremoveコマンド独自の–forceといったような2つに大別されます。
これらのオプションはtakes_optionsをtakes_argsと同じように設定する事で利用が可能になります。
但し、前者(-vや-h)はそのオプション名を文字列で指定すればよいのですが、
独自オプションを作る場合は、個別にOptionクラスやRegistryOptionを定義する必要があります。
TODO RegistryOption周りの説明
ここでは、verboseというどのコマンドでも利用可能オプションと、
このコマンド特有のhideというオプションの設定を行います。
# __init__.py
from bzrlib.commands import Command, register_command
from bzrlib.option import Option
class cmd_hello(Command):
takes_args = ["username"]
takes_options = [Option("hide", "username print asterisk"), "verbose"]
def run(self, username, hide=False, verbose=False):
print "hello %s" % ("*" * len(username) if hide else username)
これで–hideというオプションを設定した場合は出力が***になります
$ bzr hello hogepiyo
hello hogepiyo
$ bzr hello hogepiyo --hide
hello ********
2-6. プラグインのテスト
プラグインのテストはselftestというコマンドで実行する事が可能です。
但し、testtoolsに依存していますので、
存在していないと、こんな感じで怒られます。
bzr: ERROR: No module named testtools
You may need to install this Python library separately.
pipなりeasy_installなりでインストールしておきます。
selftestの仕組みはプラグインにtest_suite関数が定義されていれば自動でテストを実行してくれます。
なので、こんな感じで関数を定義しておきます。
# from hello_world.py
# 対象箇所のみ抜粋
def test_suite():
pass
from bzrlib.tests.TestUtil import TestLoader
import tests.test_hello
from unittest import TestSuite
result = TestSuite()
result.addTest(TestLoader().loadTestsFromModule(tests.test_hello))
これだけだと、テストの実体がないのでテストを読み込んで実行するようにします。
以下のように構成されているとします。
$ cd ~/.bazaar/plugins/hello_world
$ mkdir tests
$ touch tests/__init__.py tests/test_hello.py
$ tree .
.
├── __init__.py
└── tests
├── __init__.py
└── test_hello.py
test_hello.pyを以下のように記述します。
# test_hello.py
from bzrlib.tests import TestCase
class TestHelloWorld(TestCase):
def test_hello(self):
# とりあえず失敗させる
self.assertEqual(1, 0)
__init__.py側でも該当のモジュール読み込むようにします。
# この辺りはbzrtools辺りのtest_suiteの実装を参考にしています。
# __init__.py
def test_suite():
from bzrlib.tests.TestUtil import TestLoader
from unittest import TestSuite
import tests.test_hello
result = TestSuite()
result.addTest(TestLoader().loadTestsFromModule(tests.test_hello))
return result
pythonのunittest.TestSuiteにaddTestしているだけですね。
TestUtil.TestLoaderはunittest.TestLoaderを拡張しているので基本は同じはず…。
これでselftestを実行してみます。
$ bzr selftest hello_world
FAIL: bzrlib.plugins.hello_world.tests.test_hello.TestBase.test_hellost_hello
Empty attachments:
log
Traceback (most recent call last):
File "~/.bazaar/plugins/hello_world/tests/test_hello.py", line 6, in test_hello
self.assertEqual(1, 0)
AssertionError: not equal:
a = 1
b = 0
======================================================================
FAIL: bzrlib.plugins.hello_world.tests.test_hello.TestBase.test_hello
----------------------------------------------------------------------
_StringException: Empty attachments:
log
Traceback (most recent call last):
File "~/.bazaar/plugins/hello_world/tests/test_hello.py", line 6, in test_hello
self.assertEqual(1, 0)
AssertionError: not equal:
a = 1
b = 0
----------------------------------------------------------------------
Ran 1 test in 0.154s
FAILED (failures=1)
テストが成功するように修正して実行してみます。
# test_hello.py
#self.assertEqual(1, 0)
self.assertEqual(0, 0)
$ bzr selftest hello_world
bzr selftest: /usr/local/Cellar/bazaar/2.4.2/libexec/bzr
/usr/local/Cellar/bazaar/2.4.2/libexec/bzrlib
bzr-2.4.2 python-2.6.1 Darwin-10.8.0-i386-64bit
----------------------------------------------------------------------
Ran 1 test in 0.154s
ok
何も出ませんが、成功しています。
どのテストが実行されたかは、-vをつけると分かります。
running 1 tests...
bzr selftest: /usr/local/Cellar/bazaar/2.4.2/libexec/bzr
/usr/local/Cellar/bazaar/2.4.2/libexec/bzrlib
bzr-2.4.2 python-2.6.1 Darwin-10.8.0-i386-64bit
bzrlib.plugins.hello_world.tests.test_hello.TestBase.test_hello OK 2ms
----------------------------------------------------------------------
Ran 1 test in 0.156s
OK
3. 補足
3-1. プラグインロード時の問題
bzrは起動時にこれらのコマンドを読み込むため、__init__.pyにいわゆる『重い』処理を書いておくと
最初のbzrの起動がとても重くなってしまうそうです(2回目以降はキャッシュされますが…)。
そのための回避策としてチュートリアルには2つ方法が提示されています。
初期の読み込みを最小限にする
bzrの起動時に読み込まれるのが最初の起動ファイル?だけのようです。
ここで記述されている必要があるのがCommandクラスの定義とコマンドの登録のみなので、
それ以外を全て別のファイルに追い出してしまうという方法のようです。
遅延読み込みの実装を行う
lazy_import.lazy_importを利用する方法です。
この方法はコマンドが起動されるまで依存するモジュールの読み込みを遅延できるようです。
ドキュメントやビルドインコマンドでは以下のように読み込まれる事が確認できます。
from bzrlib.lazy_import import lazy_import
lazy_import(globals(), """
from bzrlib import (
branch as _mod_branch,
option,
workingtree,
)
""")
ただ、一部のプラグインは、commands.plugin_cmds.register_lazyを利用して読み込んでいるものもありました。
この辺はまだ調べていないのでTODOで。
3-2. 参考にしたサイト
Developing a plugin
以上です。大体こんな感じでプラグインが作成できます。
簡単ですね。あとはIntegrating with Bazaarあたりを参考にいじっていけると思います。
以上です。