はじめに
マルチコマンド・パイプライニング! nikkieです。
先日Clickのドキュメントの「Multi Command Chaining」を取り上げました。
今回はそのよくあるユースケースの「Multi Command Pipelines」について、ドキュメントから理解したことをまとめます。
目次
前提
前回「Multi Command Chaining」
- Clickでは1つのコマンドに複数のサブコマンドをまとめられる(1つのやり方がGroup)
- Groupを作るときに
@click.group(chain=True)
と関数をデコレートすると、コマンド利用者はグループにまとめたコマンドを連鎖させて呼び出せる - 例:
setuppy sdist bdist_wheel
setuppy sdist
をまず実行しsetuppy bdist_wheel
を次に実行
Clickで実装したコマンドをpyproject.tomlを使って環境にインストールする方法も取り上げました。
動作環境
前回と共通です
- Python 3.11.4
- Click 8.1.3
- ドキュメントも8.1.xを参照します。 https://click.palletsprojects.com/en/8.1.x/
- pip 23.1.2
- setuptools 68.0.0
Multi Command Pipelines
https://click.palletsprojects.com/en/8.1.x/commands/#multi-command-pipelines
ドキュメントの例
テキストファイルを受け取り、各行を変換するコマンドを考えます。
- 空白文字の削除(strip)
- 小文字に統一(lowercase)
- 大文字に統一(uppercase)
ドキュメントからの工夫として、最終成果物を(標準出力も含めて)出力できるようにしました。
実行例
input.txtとして、空白文字や大文字小文字が入り混じったファイルを用意しました。
2行目・3行目は右側にも空白文字が入っています。
kokoro Aki Fuka KOKORO
空白文字を除いてstdoutに出力
% pipeline -i input.txt -o - strip kokoro Aki Fuka KOKORO
大文字にしてstdoutに出力
% pipeline -i input.txt -o - uppercase KOKORO AKI FUKA KOKORO
空白文字を除いてから小文字にしてstdoutに出力
% pipeline -i input.txt -o - strip lowercase kokoro aki fuka kokoro
-o
にファイルパスを渡せば保存もされます。
実装のポイント
まず@click.group(chain=True)
とデコレートしているので、サブコマンドを連鎖させて呼び出せます。
一番のポイントは、@cli.result_callback()
でデコレートしたprocess_pipeline
関数!
- 3つの引数のうち
- inputとoutputはGroup
cli
の引数です - processors引数は、サブコマンドが返したprocessor(関数オブジェクト)のリストを指します
- inputとoutputはGroup
- 例えばサブコマンドに
strip uppercase
と渡した場合- サブコマンド2つが順番に実行されます
- サブコマンドすべての実行が終わったら
result_callback
(process_pipeline
関数)が実行されます - サブコマンドが返した関数(processor)が順に入力ファイルの各行に適用され、結果が出力ファイルに保存される、という実装になっています!
- サブコマンドは処理を表す関数を返し、それらをまとめて
result_callback
で適用するというのがポイント!
細かい点としては
@click.group
のinvoke_without_command=True
pipeline -i input.txt -o -
とサブコマンド無しで読んだときの挙動です1invoke_without_command=False
がデフォルトの挙動で、コマンドラインから--help
が渡されたときのようにヘルプメッセージを表示しますinvoke_without_command=True
ではヘルプメッセージは表示せず、サブコマンドなしでグループのcallbackを呼び出します- 今回の実装では
processors
が空のリストを指した状態で実行されます - 途中で落ちたりせずに、input.txtの各行が何も変換されずに出力されるという動きになります(この動きで問題ないから
invoke_without_command=True
としたわけですね)
- 今回の実装では
- 詳しくは「Group Invocation Without Command」参照
% pipeline -i input.txt -o - kokoro Aki Fuka KOKORO
宿題:さらなる例(画像変換コマンド)
For a more complex example that also improves upon handling of the pipelines have a look at the imagepipe multi command chaining demo in the Click repository.
以下のようなコマンドを提供するようで、ざっと見た感じ、これを会得すればClickの力を解き放てそうに思われます。
(独自のデコレータを作っているのですが、なんで必要なのかがぱっとは分かっていません=宿題)
$ imagepipe open -i example01.jpg resize -w 128 display $ imagepipe open -i example02.jpg blur save
寄り道「Multi Command Chainingで同じことやるなら?」(中間ファイルを残したい)
パイプラインの例は非常に刺激的だったのですが、「中間ファイルを残せるようにするにはどうやればいいんだろう」と少し手を動かしてみました2。
前回のエントリにある「引数の扱いはどうなるのか」という素振り項目も兼ねています。
中間ファイルを残す提案実装
strip -> lowercase(=空白文字が削除され、小文字に揃う)
% python practice_pipeline.py \ strip input.txt strip_output.txt \ lowercase strip_output.txt output.txt % cat strip_output.txt kokoro Aki Fuka KOKORO % cat output.txt kokoro aki fuka kokoro
uppercase -> strip(=空白文字が削除され、大文字に揃う)
% python practice_pipeline.py \ uppercase input.txt upper_output.txt \ strip upper_output.txt output.txt % cat upper_output.txt KOKORO AKI FUKA KOKORO % cat output.txt KOKORO AKI FUKA KOKORO
提案実装のポイント
- 中間ファイルを残したく、各サブコマンドでファイルの読み書きをするように変更
- 位置引数(argument。必須の引数)に変更
- 引数のtypeを
click.Path
に変更- 例えば strip -> lower と連鎖させた時、typeが
click.File
だとlowerの入力ファイルを即時開こうとし、そのファイルはまだないので見つからないというエラーで落ちる3 - 即時開くのではなく、stripが作った後にlowerは開いてほしい
- 例えば strip -> lower と連鎖させた時、typeが
- click.Pathのドキュメントより
- コマンド実行時にはできていないファイルがあるので、ファイルの存在確認はしない(
exists=False
。デフォルト値)- サブコマンドを呼び出す順番が決まっていないのでinputのファイルもexistsのチェックはできない
path_type
引数を使ってpathlib.Path
に変換した- pathlib.Path.openを使っている
- コマンド実行時にはできていないファイルがあるので、ファイルの存在確認はしない(
終わりに
Clickのドキュメントの「Multi Command Pipelines」について理解したことをまとめました。
- 「Multi Command Pipelines」は「Multi Command Chaining」のよくあるユースケース
@click.group(chain=True)
@cli.result_callback()
で、連鎖したサブコマンドがすべて実行された後のコールバックを登録する- 個々のサブコマンドは関数を返す
result_callback
で関数を1つずつ反復して適用するよう実装
宿題としたimagepipe
の例は非常に肝な感触がするので、自力で書けるように読み解いていきたいと思います。
-
ファイルパスの代わりに
-
と標準出力も指定できているのが、type=click.File
の効果です。ref: https://click.palletsprojects.com/en/8.1.x/arguments/#file-arguments↩ - パイプラインを1コマンドずつ実行するというやり方もあります(コマンドの回数は増えますが、複雑さをこれが一番持ち込まないかもしれません)↩
- 「the files were opened immediately.」ref: https://click.palletsprojects.com/en/8.1.x/arguments/#file-path-arguments↩