はじめに
オプション解析処理を行うモジュールを調べているうちに python-fire を知りました。
既存コードをほとんど修正することなくCLIアプリケーションにできるので、
知っておいても無駄にはならないと思い、自習のためとして資料にしました。
この資料は公式ドキュメントの翻訳程度のものですが、
これから使ってみようという方々の助けになれば幸いです。
Python-Fire について
Python-Fire はPythonオブジェクトからコマンドラインインターフェイス(CLI)を自動的に生成するためのライブラリで、次のような特徴があります。
- Python-Fireは、PythonでCLIアプリケーションを簡単に作成できる。
- 既存コードもほとんど修正することなくCLIアプリケーションにできる。
- Python-Fireは、Pythonコードを開発/デバッグで便利なツールとなる。
- Python-Fireは、既存のコードを探索するときに便利なツールとなる。
- Python-Fireは、会話形インタフェースREPLを簡単に提供することができる。
- モジュールと変数などのオブジェクトを読み込まれていてすぐに使かえる。
Python-Fire のインストール
Python-Fire は拡張モジュールなのでインストールする必要があります。
$ pip install fire
使用方法
今、次のような関数が定義されたファイルがあるとします。
def hello(name="World"):
return "Hello %s!" % name
このhello()
をCLIアプリケーションとしたい場合は、単に fire.Fire()
を呼び出すだけで、既存コードを修正する必要はありません。
from hello import hello
import fire
fire.Fire(hello)
ヘルプメッセージを表示するためには、--help
もしくは -h
をオプションで与えます。
$ python hello_cli.py --help
fire は組み込み関数 help()
のようにヘルプメッセージを表示してくれます。
NAME
hello_cli.py
SYNOPSIS
hello_cli.py <flags>
FLAGS
--name=NAME
$ python hello_cli.py
Hello World!
$ python hello_cli.py --name Jack
Hello Jack!
複数のオプションが与えられると最後に指定した値が使用されます。
$ python hello_cli.py --name Jack --name Eddie
Hello Eddie!
また、Python 実行時に -m fire
を与えると、
hello_cli.py
のような呼び出すためにスクリプトを作成しなくても、既存コードをそのままCLIアプリケーションとして動作させることができます。
$ python -m fire hello
ここで与えている hello は fire が読み込むモジュール名です。つまり、このディレクトリにある hello.py が読み込まれます。
次のようにヘルプメッセージが表示され、関数hello()
がサブコマンドとして実行受け付けることがわかります。
NAME
hello
SYNOPSIS
hello COMMAND
COMMANDS
COMMAND is one of the following:
hello
$ python -m fire hello hello --help
NAME
hello hello
SYNOPSIS
hello hello <flags>
FLAGS
--name=NAME
$ python -m fire hello hello
Hello World!
$ python -m fire hello hello Jack
Hello Jack!
$ python -m fire hello hello --name David
Hello David!
この使い方では、指示するモジュールはPYTHONPATH
から検索します。
また、docstrings をヘルプメッセージとして表示されます。
$ python -m fire json --help
NAME
json - JSON (JavaScript Object Notation) <http://json.org> is a subset of JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data interchange format.
SYNOPSIS
json GROUP | COMMAND
DESCRIPTION
:mod:`json` exposes an API familiar to users of the standard library
:mod:`marshal` and :mod:`pickle` modules. It is derived from a
version of the externally maintained simplejson library.
Encoding basic Python object hierarchies::
>>> import json
>>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
'["foo", {"bar": ["baz", null, 1.0, 2]}]'
>>> print(json.dumps("\"foo\bar"))
"\"foo\bar"
>>> print(json.dumps('\u1234'))
"\u1234"
>>> print(json.dumps('\\'))
"\\"
複数の関数があるときはどうするのか?
次のように複数の関数があっても、fire は対応できます。
def add(x, y):
return x + y
def multiply(x, y):
return x * y
def squre(n):
return(n**2)
def cube(n):
return(n**3)
これまでと同様に fire.Fire()
を実行するだけです。
from mymath import *
import fire
fire.Fire()
ヘルプメッセージを表示してみましょう。
$ python mymath_cli.py --help
ファイルにある関数を読み込んで複数のサブコマンドとして受け付けることがわかります。
NAME
mymath_cli.py
SYNOPSIS
mymath_cli.py GROUP | COMMAND
GROUPS
GROUP is one of the following:
fire
The Python Fire module.
COMMANDS
COMMAND is one of the following:
add
multiply
squre
cube
特定の関数だけにCLIを追加したい
デフォルトではfireが読み取れるすべての関数をサブコマンドとして扱います。
1つの関数だけを対象にしたいのであれば、hello_cli.py
と同様にfire.Fire()
に対象の関数を引数で与えるだけです。
from mymath import squre, cube
import fire
fire.Fire(cube)
fire.Fire()
を呼び出すときに対象関数を辞書で指定することもできます。
from mymath import squre, cube
import fire
fire.Fire({
'Squire': squre,
'Cube': cube,
})
ヘルプメッセージを表示してみましょう。
$ python mymath_cli2.py --help
NAME
mymath_cli2.py
SYNOPSIS
mymath_cli2.py COMMAND
COMMANDS
COMMAND is one of the following:
Squire
Cube
辞書で与えたキーの文字列がサブコマンド名となります。
クラスを呼び出す
fire.Fire()
はクラスもCLIアプリケーションにすることができます。
今、次のようなクラスがあるとします。
class Calculator(object):
"""A simple calculator class."""
def double(self, number):
return 2 * number
fire.Fire()
の引数にCLIにするクラスを与えます。
from calculator import Calculator
import fire
fire.Fire(Calculator)
このクラスの double()
メソッドにはデフォルト値が定義されていません。
そのため。コマンド引数を与えずに実行すると、--help
オプションが与えられたときと同様にヘルプメッセージを表示します。
$ python calculator_cli.py
NAME
calculator_cli.py - A simple calculator class.
SYNOPSIS
calculator_cli.py COMMAND
DESCRIPTION
A simple calculator class.
COMMANDS
COMMAND is one of the following:
double
Calculatror
クラスのdouble
メソッドの関数名がサブコマンド名となります。
サブコマンドのヘルプを表示してみましょう。
$ python calculator_cli.py double --help
NAME
calculator_cli.py double
SYNOPSIS
calculator_cli.py double NUMBER
POSITIONAL ARGUMENTS
NUMBER
NOTES
You can also use flags syntax for POSITIONAL ARGUMENTS
$ python calculator_cli.py double 20
40
$ python calculator_cli.py double --number 20
40
関数よりもクラスを使用する方が柔軟性が高まります。
次の例を見てみましょう。
class BrokenCalculator(object):
def __init__(self, offset=1):
self._offset = offset
def add(self, x, y):
return x + y + self._offset
def multiply(self, x, y):
return x * y + self._offset
if __name__ == '__main__':
import fire
fire.Fire(BrokenCalculator)
このBrokenCalculator
はインスタンス生成時にoffset
引数を取ることができますが、これをオプションとしてコマンドラインから与えることができます。
$ python example.py add 10 20
31
$ python example.py multiply 10 20
201
$ python example.py add 10 20 --offset=0
30
$ python example.py multiply 10 20 --offset=0
200
サブコマンドのグループ化
サブコマンドをグループ化することもできます。
class IngestionStage(object):
def run(self):
return 'Ingesting! Nom nom nom...'
class DigestionStage(object):
def run(self, volume=1):
return ' '.join(['Burp!'] * volume)
def status(self):
return 'Satiated.'
class Pipeline(object):
def __init__(self):
self.ingestion = IngestionStage()
self.digestion = DigestionStage()
def run(self):
value = list()
result = self.ingestion.run()
value.append(result)
result = self.digestion.run()
value.append(result)
return value
if __name__ == '__main__':
import fire
fire.Fire(Pipeline)
コマンドラインで引数を何も与えずに実行するとサブコマンドが選ばれていないため、
ヘルプメッセージが表示されます。
$ python group.py
NAME
group.py
SYNOPSIS
group.py GROUP | COMMAND
GROUPS
GROUP is one of the following:
digestion
ingestion
COMMANDS
COMMAND is one of the following:
run
サブコマンドのヘルプを見てみましょう。
$ python group.py digestion --help
NAME
group.py digestion
SYNOPSIS
group.py digestion COMMAND
COMMANDS
COMMAND is one of the following:
run
status
$ python group.py ingestion --help
NAME
group.py ingestion
SYNOPSIS
group.py ingestion COMMAND
COMMANDS
COMMAND is one of the following:
run
実行してみます。
$ python group.py run
Ingesting! Nom nom nom...
Burp!
$ python group.py ingestion run
Ingesting! Nom nom nom...
$ python group.py digestion run
Burp!
% python group.py digestion status
Satiated.
公式ドキュメントについて補足:
公式ドキュメントでは
Pipeline
はIngestionStage
とDigestionStage
クラスのrun()
メソッドを呼び出すだけと表記されています。
しかし、これではその結果を表示することができません。
fire は戻り値を表示するため、値をreturn するか、
明示的にprint()
する必要があります。
プロパティーにアクセスする
これまで見てきたサンプルプログラムでは、何かの関数を実行しています。
次のコードは、単にプロパティにアクセスするだけの例です。
airports はアメリカの空港コードを保持するモジュールです。
from airports import airports
class Airport(object):
def __init__(self, code):
self.code = code
self.name = dict(airports).get(self.code)
self.city = self.name.split(',')[0] if self.name else None
if __name__ == '__main__':
import fire
fire.Fire(Airport)
$ python access_cli.py --help
NAME
access_cli.py
SYNOPSIS
access_cli.py --code=CODE
ARGUMENTS
CODE
$ python access_cli.py --code=JSK
NAME
access_cli.py --code=JFK
SYNOPSIS
access_cli.py --code=JFK VALUE
VALUES
VALUE is one of the following:
city
code
name
ヘルプメッセージにあるように、プロパティーを与えるとその値が帰ってくるようになります。
$ python access_cli.py --code=JFK city
New York-New Jersey-Long Island
$ python access_cli.py --code=JFK name
New York-New Jersey-Long Island, NY-NJ-PA - John F. Kennedy International (JFK)
オブジェクトのメソッドを呼び出せる
fire.Fire()
は呼び出した結果に対して、引数で与えられたオブジェクトで実行できるメソッドをすべて実行することができます。
$ python access_cli.py --code=SFO name upper
SAN FRANCISCO-OAKLAND-FREEMONT, CA - SAN FRANCISCO INTERNATIONAL (SFO)
これは airports モジュールの airports
は文字列をリストで保持しているだけですが、str
型では upper
メソッドが使えるからです。
どのようなメソッドがあるかは--help
をオプションを与えると詳細を知ることができますが、簡単には help
としても推察することができます。
% python access_cli.py --code=SFO name help
ERROR: Could not consume arg: help
Usage: access_cli.py --code=SFO name <command>
available commands: capitalize | casefold | center | count | encode |
endswith | expandtabs | find | format | format_map |
index | isalnum | isalpha | isascii | isdecimal |
isdigit | isidentifier | islower | isnumeric |
isprintable | isspace | istitle | isupper | join |
ljust | lower | lstrip | maketrans | partition |
replace | rfind | rindex | rjust | rpartition |
rsplit | rstrip | split | splitlines | startswith |
strip | swapcase | title | translate | upper | zfill
For detailed information on this command, run:
access_cli.py --code=SFO name --help
このため、関数を数珠つなぎで呼ばれるように設定したい場合は、メソッドが自分自身を返すクラスを用意するだけです。
class BinaryCanvas(object):
"""A canvas with which to make binary art, one bit at a time."""
def __init__(self, size=10):
self.pixels = [[0] * size for _ in range(size)]
self._size = size
self._row = 0 # The row of the cursor.
self._col = 0 # The column of the cursor.
def __str__(self):
return '\n'.join(' '.join(str(pixel) for pixel in row) for row in self.pixels)
def show(self):
print(self)
return self
def move(self, row, col):
self._row = row % self._size
self._col = col % self._size
return self
def on(self):
return self.set(1)
def off(self):
return self.set(0)
def set(self, value):
self.pixels[self._row][self._col] = value
return self
if __name__ == '__main__':
import fire
fire.Fire(BinaryCanvas)
% python canvas_cli.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 1 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 1 0 0 0
0 0 0 0 1 1 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
この例 BinaryCanvas
の例では、0/1 の文字で構成されるキャンバスが表示されています。
これは、__str__
メソッドの定義に従っています。
__str__
メソッドが最終コンポーネントに存在する場合、オブジェクトはシリアル化されて表示されます。__str__
メソッドがない場合は、代わりにオブジェクトのヘルプ画面が表示されます。
変数にアクセスする
プロパティーにアクセスするだけでその値を返すようなCLIアプリケーションが記述できるわけなのですので、次のように変数にアクセスしても同様にことができます。
english = 'Hello World'
spanish = 'Hola Mundo'
import fire
fire.Fire()
ヘルプメッセージを見てみましょう。
$ python greeting.py --help
NAME
greeting.py
SYNOPSIS
greeting.py GROUP | VALUE
GROUPS
GROUP is one of the following:
fire
The Python Fire module.
VALUES
VALUE is one of the following:
english
spanish
変数名を引数として受け付けることがわかります。
$ python greeting.py english
Hello World
$ python greeting.py spanish
Hola Mundo
可変引数をとる関数
次のような可変引数をとるような関数の場合でも、fire はうまく処理してくれます。
def order_by_length(*items):
"""Orders items by length, breaking ties alphabetically."""
sorted_items = sorted(items, key=lambda item: (len(str(item)), str(item)))
return ' '.join(sorted_items)
if __name__ == '__main__':
import fire
fire.Fire(order_by_length)
$ python nargs_cli.py --help
NAME
nargs_cli.py - Orders items by length, breaking ties alphabetically.
SYNOPSIS
nargs_cli.py [ITEMS]...
DESCRIPTION
Orders items by length, breaking ties alphabetically.
POSITIONAL ARGUMENTS
ITEMS
$ python nargs_cli.py Beer Wine Sake
Beer Sake Wine
fire に与えたオブジェクトが持つメソッドはすべて呼び出せることは説明しましたが、この場合はコマンド引数とサブコマンド(メソッド)の区別をつけられません。こうしたときは、マイナス記号1つ(-
) でコマンドライン引数とサブコマンドを区切ります。
$ python nargs_cli.py Beer Wine Sake - upper
BEER SAKE WINE
区切り文字を変えたい場合、fire に --separator
オプションで与えます。
$ python nargs_cli.py Beer Wine Sake @ upper -- --separator=@
BEER SAKE WINE
この--separator
のように、fire 自身が解釈するオプションがいくつかあります。コマンドラインでCLIアプリケーションのオプションや引数と区別するために、マイナス記号2つ(--
) で区切ると、それ以後は fire が解釈するオプションとなります。
パラメタの型
typer はコマンドラインに与えられた型を推定することができます。
import fire
fire.Fire(lambda obj: type(obj).__name__)
$ python check_type.py 10
int
$ python check_type.py 10.0
float
$ python check_type.py hello
str
$ python check_type.py '(1,2)'
tuple
$ python check_type.py [1,2]
list
$ python check_type.py True
bool
$ python check_type.py {name:David}
dict
パラメタに文字列を与えるときの注意点
コマンドラインをはじめにシェルがパースするために、
シングルクォートとダブルクォートを2重にする必要があります。
$ python check_type.py 10
int
$ python check_type.py '10'
int
$ python check_type.py "10"
int
$ python check_type.py '"10"'
str
$ python check_type.py "'10'"
str
$ python check_type.py \"10\"
str
パラメタに辞書型のデータを使用するときの注意点
パラメタに辞書を使用する場合はシェルを意識する必要があります。
$ python check_type.py '{"name": "David Bieber"}'
dict
$ python check_type.py {"name":'"David Bieber"'}
dict
$ python check_type.py {"name":"David Bieber"}
str
$ python check_type.py {"name": "David Bieber"}
<error> # 複数のパラメタだとシェルが解釈してしまう
パラメタにbool型のデータを使う
これまでの例で、パラメタにbool型を使うことができることは説明しました。
bool型では、その指示の方法には次のように複数の方法があります。
$ python check_type.py --obj=True
bool
$ python check_type.py --obj=False
bool
$ python check_type.py --obj
bool
$ python check_type.py --noobj
bool
会話形モード:REPL
スクリプトを呼び出すとき -- --interactive
を与えると、
Python のREPL が起動します。
IPython がインストールされていれば、次のようにIPython のRPELが起動します。
Fireは与えられたコンテキストで使用されているすべてのモジュールと変数を含むを、すぐに使用できるようになっているため、デバッグなどで便利です。
$ python calculator_cli.py -- --interactive
Fire is starting a Python REPL with the following objects:
Modules: fire
Objects: Calculator, calculator.py, component, result, trace
Python 3.8.6 | packaged by conda-forge | (default, Nov 27 2020, 19:17:44)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.19.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]:
プログラムをトレースする
Python プログラムがどうのように挙動しているか把握できるとデバッグが楽になります。こうしたときのために fire には --trace
オプションがあります。
前述の chain.py
をトレースしてみましょう。
$ python chain.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on -- --trace
実行した結果です。
Fire trace:
1. Initial component
2. Instantiated class "BinaryCanvas" (chain.py:1)
3. Accessed property "move" (chain.py:17)
4. Called routine "move" (chain.py:17)
5. Accessed property "on" (chain.py:22)
6. Called routine "on" (chain.py:22)
7. Accessed property "move" (chain.py:17)
8. Called routine "move" (chain.py:17)
9. Accessed property "on" (chain.py:22)
10. Called routine "on" (chain.py:22)
11. Accessed property "move" (chain.py:17)
12. Called routine "move" (chain.py:17)
13. Accessed property "on" (chain.py:22)
14. Called routine "on" (chain.py:22)
15. Accessed property "move" (chain.py:17)
16. Called routine "move" (chain.py:17)
17. Accessed property "on" (chain.py:22)
18. Called routine "on" (chain.py:22)
19. Accessed property "move" (chain.py:17)
20. Called routine "move" (chain.py:17)
21. Accessed property "on" (chain.py:22)
22. Called routine "on" (chain.py:22)
23. Accessed property "move" (chain.py:17)
24. Called routine "move" (chain.py:17)
25. Accessed property "on" (chain.py:22)
入力補完スクリプトの生成
fire に --completion
を与えると、Bashの入力補完スクリプトを生成します。
入力補完スクリプトをホームディレクトリに保存するには、次のようにします。
$ python chain.py -- --completion > $HOME/._chain.py
このあと、このファイルを読み込んでおきます。
$ source $HOME/._chain.py
ただし、対応しているシェルは Bash と Fish だけです。
所感
プログラム開発では、CLIアプリケーションのための実装は本来の目的ではないことが多く、煩雑で面倒なものです。
いずれは typer や click で実装するにしても、まずは、python-fire で簡易的にCLIを実装して、その時間を肝心のコアな部分の開発に集中できることは注目すべきことだと考えています。
既存コードをほぼそのままで、invoke を連想させるような、タスクランナーとしての利用もできるといった側面もあり興味深いです。
python-fire には、気づいたところでは次のような制限があると考えています。
- サブコマンド間でオプションを共有することは簡単にはできない。
- クラスを与えたときは、そすべてメソッドをCLIのサブコマンドとして扱う。
これを書いてから 爆速python-fire という記事があることを知ってしまった。orz.