今回は Google が公開した python-fire というパッケージを試してみた。
python-fire では、クラスやモジュールを渡すことで、定義されている関数やメソッドを元に CLI を自動で生成してくれる。
ただし、一つ注意すべきなのは、できあがる CLI はそこまで親切な作りではない、という点だ。
実際にユーザに提供するような CLI を実装するときは、従来通り Click のようなフレームワークを使うことになるだろう。
では python-fire はどういったときに活躍するかというと、これは開発時のテストだと思う。
実装した内容をトライアンドエラーするための CLI という用途であれば python-fire は非常に強力なパッケージだと感じた。
今回使った環境は次の通り。
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.12.3
BuildVersion: 16D32
$ python --version
Python 3.6.0
インストール
インストールは Python のパッケージツールの pip を使ってできる。
$ pip install fire
もし pip がインストールされていないときは、あらかじめ入れておこう。
$ curl https://bootstrap.pypa.io/get-pip.py | sudo python
あと、これは完全に蛇足だけどシステムの Python 実行環境にそのまま入れるのはおすすめしない。
色んなパッケージを試すときは、システムからは独立した Python 仮想環境を作って入れた方が良い。
以下は、例えば virtualenv を使ったやり方。
$ sudo pip install virtualenv
$ mkdir -p venv
$ virtualenv venv
これで、最低限のパッケージの入った Python 仮想環境ができる。
(venv) $ pip list --format=columns
Package Version
---------- -------
appdirs 1.4.3
packaging 16.8
pip 9.0.1
pyparsing 2.2.0
setuptools 34.3.1
six 1.10.0
wheel 0.29.0
python-fire をインストールすると、次のようなパッケージが入る。
意外と依存パッケージが多い。
(venv) $ pip install fire
(venv) $ pip list --format=columns
Package Version
---------------- -------
appdirs 1.4.2
appnope 0.1.0
decorator 4.0.11
fire 0.1.0
ipython 5.3.0
ipython-genutils 0.1.0
packaging 16.8
pexpect 4.2.1
pickleshare 0.7.4
pip 9.0.1
prompt-toolkit 1.0.13
ptyprocess 0.5.1
Pygments 2.2.0
pyparsing 2.2.0
setuptools 34.3.1
simplegeneric 0.8.1
six 1.10.0
traitlets 4.3.2
wcwidth 0.1.7
wheel 0.29.0
基本的な使い方
まずは python-fire の基本的な使い方から見ていく。
とはいっても、その使い方は至ってシンプル。
例えば CLI を自動生成したいクラスがあるなら、それを fire.Fire() コマンドに渡すだけ。
次のコマンドを実行すると helloworld.py
というファイルでサンプルコードが保存される。
$ cat << 'EOF' > helloworld.py
class MyClass(object):
def greet(self):
print('Hello, World!')
if __name__ == '__main__':
import fire
fire.Fire(MyClass)
EOF
上記のサンプルコードでは MyClass というクラスを python-fire に渡している。
このクラスには greet()
というメソッドがあって、文字列をプリントする内容になっている。
上記を何の引数も渡さずに実行してみよう。
$ python helloworld.py
Type: MyClass
String form: <__main__.MyClass object at 0x10bb64f28>
File: ~/Documents/temporary/helloworld.py
Usage: helloworld.py
helloworld.py greet
すると Usage が出力されることがわかる。
上記の出力に沿ってオプションを渡してみよう。
具体的には、MyClass
の持つメソッド名である greet
を指定する。
$ python helloworld.py greet
Hello, World!
これだけで MyClass#greet()
の処理が呼び出された。
関数・メソッドに引数を渡す
先ほどの例ではメソッドの呼び出しを試した。
ただ、そのメソッドには引数がなかった。
次は引数のあるメソッドを呼び出すときについて見てみる。
以下のコマンドを実行すると、引数のあるメソッドを定義したサンプルコードができる。
$ cat << 'EOF' > calculate.py
class MyClass(object):
def add(self, a, b=1):
return a + b
if __name__ == '__main__':
import fire
fire.Fire(MyClass)
EOF
上記では MyClass#add()
メソッドが引数として a
と b
を受け取る。
そのうち b
に関してはデフォルト引数が指定されている。
上記で用意したサンプルコードを先ほどと同じようにメソッド名だけ入れて実行してみよう。
この場合、必要な引数 a
が入力されていないためトレースが表示される。
$ python calculate.py add
Fire trace:
1. Initial component
2. Instantiated class "MyClass" (calculate.py:5)
3. Accessed property "add" (calculate.py:7)
4. ('The function received no value for the required argument:', 'a')
Type: method
String form: <bound method MyClass.add of <__main__.MyClass object at 0x109fba518>>
File: ~/Documents/temporary/calculate.py
Line: 7
Usage: calculate.py add A [B]
calculate.py add --a A [--b B]
Usage を見ると、メソッド名である add
に続いて引数を渡せば良いことがわかる。
上記にもとづいてメソッド名に続いて引数を渡してみよう。
それぞれ引数の a
と b
に対応する。
$ python calculate.py add 1 2
3
引数の b
はデフォルト引数が指定されているので省略できる。
$ python calculate.py add 1
2
引数に渡す内容の指定を順不定にしたいときは --
を使って引数名を指定してやる。
$ python calculate.py add --a=1 --b=2
3
コンストラクタに引数を渡す
先ほどまでの例では CLI を自動生成したいクラスのコンストラクタに引数がなかった。
現実には、何も引数を取らないコンストラクタの方が珍しいと思う。
そこで、次はコンストラクタに引数を取る場合を試してみる。
次のコマンドを実行するとコンストラクタに引数を取るサンプルコードができる。
$ cat << 'EOF' > constructor.py
class MyClass(object):
def __init__(self, msg):
"""コンストラクタに引数 msg を取る"""
self.msg = msg
def greet(self):
print(self.msg)
if __name__ == '__main__':
import fire
fire.Fire(MyClass)
EOF
先ほどの例と同じように引数を何も指定せず実行してみよう。
するとコンストラクタの引数が指定されていないという内容でトレースが表示される。
$ python constructor.py
Fire trace:
1. Initial component
2. ('The function received no value for the required argument:', 'msg')
Type: type
String form: <class '__main__.MyClass'>
File: ~/Documents/temporary/constructor.py
Line: 5
Usage: constructor.py MSG
constructor.py --msg MSG
どうやらコンストラクタの引数をまずは指定する必要があるらしい。
上記の表示に沿って、コンストラクタの --msg
引数を指定してみよう。
$ python constructor.py --msg="Hello World"
Type: MyClass
String form: <__main__.MyClass object at 0x102b303c8>
File: ~/Documents/temporary/constructor.py
Init docstring: コンストラクタに引数 msg を取る
Usage: constructor.py --msg='Hello World'
constructor.py --msg='Hello World' greet
constructor.py --msg='Hello World' msg
すると、今度は実行できるメソッド名が表示されるようになった。
メソッド greet()
の下にある msg
はインスタンスのメンバとして引数を保存しているため表示されているんだろう。
上記の表示に沿ってコンストラクタの引数を入力しながらメソッドを指定する。
$ python constructor.py --msg='Hello World' greet
Hello World
これでコンストラクタに引数を取るクラスであっても実行できるようになった。
関数から自動生成する
これまでの例ではクラスに対して CLI を自動生成する例だった。
次は関数に対して試してみることにしよう。
次のコマンドを実行すると関数を使うパターンのサンプルコードができる。
$ cat << 'EOF' > function.py
def greet(msg='Hello, World!'):
print(msg)
if __name__ == '__main__':
import fire
fire.Fire(greet)
EOF
とはいえ、やっていることはこれまでと変わらない。
渡すものがクラスの代わりに関数になっただけ。
今度も、特に何も指定せずまずは実行してみよう。
$ python function.py
Hello, World!
今回に関しては、これだけで事足りてしまった。
ちなみに、上記の場合はデフォルト引数が指定されていたので動いた。
もちろん、次のようにして引数を指定することもできる。
$ python function.py --msg="Hajimemashite Sekai"
Hajimemashite Sekai
ちなみに、ここで面白い挙動に気づいた。
引数を指定するときに内容にカンマが含まれていると、型が自動的にタプルになるようだ。
$ python function.py --msg="Hajimemashite, Sekai"
('Hajimemashite', 'Sekai')
型の自動判別
先ほどの例を元に、どういった内容を引数に指定すると、どの型と判別するのか調べてみた。
次のサンプルコードでは受け取った引数の型を出力する。
$ cat << 'EOF' > argtype.py
def print_type(obj):
print(type(obj))
if __name__ == '__main__':
import fire
fire.Fire(print_type)
EOF
色んな内容を渡してみた結果が次の通り。
いきなりタプルになったのは驚いたけど、文字列を指定したいなら ''
で囲むのがセオリーなようだ。
$ python argtype.py --obj="foo"
<class 'str'>
$ python argtype.py --obj="1"
<class 'int'>
$ python argtype.py --obj="1.0"
<class 'float'>
$ python argtype.py --obj="'1'"
<class 'str'>
$ python argtype.py --obj="1,2"
<class 'tuple'>
$ python argtype.py --obj="'1,2'"
<class 'str'>
$ python argtype.py --obj="[1, 2]"
<class 'list'>
$ python argtype.py --obj="{1, 2}"
<class 'set'>
$ python argtype.py --obj="{1: 2}"
<class 'dict'>
モジュールから自動生成する
次は、おそらく一番使う場面が多そうなやり方。
モジュールをそのまま指定して CLI を自動生成してしまうやり方。
次のコマンドを実行するとモジュールをそのまま指定するサンプルコードができる。
この場合は、特に何も指定せず fire.Fire()
をインスタンス化すれば良い。
$ cat << 'EOF' > module.py
class MyClass(object):
def greet(self):
print('Hello, World!')
def greet(msg='Hello, World!'):
print(msg)
def add(a, b):
return a + b
if __name__ == '__main__':
import fire
fire.Fire()
EOF
モジュールには MyClass
クラスや greet()
および add()
関数が定義されている。
次も、特に何も指定せずまずは実行してみよう。
ただ、これだけだと何が指定できるのかよく分からない出力になる。
$ python module.py
MyClass: <class '__main__.MyClass'>
greet: <function greet at 0x10e53ee18>
add: <function add at 0x10e67d378>
fire: <module 'fire' from '/Users/amedama/.virtualenvs/fire/lib/python3.6/site-packages/fire/__init__.py'>
そこで --help
オプションを指定してみよう。
間にある --
は python-fire 自体に渡す引数と、自動生成した CLI に渡す引数を区別するために使う。
$ python module.py -- --help
Type: dict
String form: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_e <...> ule 'fire' from '/Users/amedama/.virtualenvs/fire/lib/python3.6/site-packages/fire/__init__.py'>}
Length: 13
Usage: module.py
module.py MyClass
module.py greet
module.py add
module.py fire
すると、実行できる内容が出力されるようになった。
関数に関しては、これまでと特に変わらないやり方で実行できる。
$ python module.py greet
Hello, World!
$ python module.py add 1 2
3
インスタンスメソッドについては、クラス名とメソッド名を続けて入力すれば良い。
$ python module.py MyClass greet
Hello, World!
インタラクティブシェルに入る
最初に依存パッケージの中に ipython があったことに気づいていたかもしれない。
ipython は高機能な Python の REPL で python-fire の引数に --interactive
を指定すると、それが起動するようになっている。
起動した ipython では特にインポートなどをすることなくモジュールの中で定義されているクラスなどを使うことができる。
$ python helloworld.py -- --interactive
Fire is starting a Python REPL with the following objects:
Modules: fire
Objects: MyClass, component, helloworld.py, result, trace
Python 3.6.0 (default, Feb 25 2017, 20:17:10)
Type "copyright", "credits" or "license" for more information.
IPython 5.3.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]: MyClass
Out[1]: __main__.MyClass
In [2]: MyClass().greet()
Hello, World!
In [3]: exit()
通常通り ipython を指定しただけでは、こうはならない。
インポートしていないオブジェクトを使おうとするとエラーになる。
$ ipython
Python 3.6.0 (default, Feb 25 2017, 20:17:10)
Type "copyright", "credits" or "license" for more information.
IPython 5.3.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]: MyClass
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-1-d1eb688265f8> in <module>()
----> 1 MyClass
NameError: name 'MyClass' is not defined
本来であれば次のようにしてインポートしてやる必要がある。
python-fire 経由で起動すると、この手間をはぶくことができるようだ。
In [2]: from helloworld import MyClass
In [3]: MyClass
Out[3]: helloworld.MyClass
呼び出しをトレースする
これまでの例でもエラーになったときに何度か表示されていたけど、呼び出し内容をトレースすることもできる。
これには python-fire の引数として --trace
オプションを指定する。
$ python helloworld.py greet -- --trace
Fire trace:
1. Initial component
2. Instantiated class "MyClass" (helloworld.py:5)
3. Accessed property "greet" (helloworld.py:7)
上記のように各ステップでどういったことが実行されているのかが表示される。
補完スクリプトを出力する
python-fire では bash 用の補完スクリプトを出力することもできる。
それには、次のようにして --completion
を python-fire の引数に渡す。
$ python helloworld.py -- --completion
_complete-helloworldpy()
{
local start cur opts
COMPREPLY=()
start="${COMP_WORDS[@]:0:COMP_CWORD}"
cur="${COMP_WORDS[COMP_CWORD]}"
opts=""
if [[ "$start" == "helloworld.py" ]] ; then
opts="greet"
fi
if [[ "$start" == "helloworld.py greet" ]] ; then
opts="--self"
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _complete-helloworldpy helloworld.py
ここで出力される内容を bash の設定ファイルに入れて読み込んでやるとシェルで補完が効くようになる。
$ python helloworld.py -- --completion >> ~/.bashrc
$ source ~/.bashrc
ただし、出力される補完スクリプトはパスが通っている場所で、かつスクリプトに実行権限がついていることを前提になっている。
そのため、上記のように python コマンド経由で実行する限りでは補完が効かない。
もし使うとしたら、こんな感じにしないとダメそう。
$ chmod +x helloworld.py
$ sudo mv helloworld.py /usr/local/bin/
ちなみに bashcompinit
を有効にして試してみたけど zsh では動かなかった。
詳細を出力する
これまで見てきたように --help
オプションを指定すると詳しい使い方が出力された。
次のように --verbose
オプションを指定すると、特殊メソッドやアトリビュートを含むさらに詳しい内容が出力される。
$ python helloworld.py -- --verbose
Type: MyClass
String form: <__main__.MyClass object at 0x1055cc4a8>
File: ~/Documents/temporary/helloworld.py
Usage: helloworld.py
helloworld.py __class__
helloworld.py __delattr__
helloworld.py __dict__
helloworld.py __dir__
helloworld.py __doc__
helloworld.py __eq__
helloworld.py __format__
helloworld.py __ge__
helloworld.py __getattribute__
helloworld.py __gt__
helloworld.py __hash__
helloworld.py __init__
helloworld.py __init_subclass__
helloworld.py __le__
helloworld.py __lt__
helloworld.py __module__
helloworld.py __ne__
helloworld.py __new__
helloworld.py __reduce__
helloworld.py __reduce_ex__
helloworld.py __repr__
helloworld.py __setattr__
helloworld.py __sizeof__
helloworld.py __str__
helloworld.py __subclasshook__
helloworld.py __weakref__
helloworld.py greet
まとめ
今回は Google の作った CLI を自動生成するパッケージである python-fire を試してみた。
生成される CLI は割りと雑だけど、開発者が使う分には全く問題ないレベルだと思う。
これまでは挙動を軽く試すのにもスクリプトを毎回書き換えたり ipython
を起動したりと面倒が多かった。
一つ一つの作業にかかる時間は短くとも、長い目で見れば多くの時間を浪費していることだろう。
そんなとき python-fire を使えば、スクリプトに数行を追加するだけでその手間をはぶくことができるのは大きいと感じた。
いじょう。