nikkie-ftnextの日記

イベントレポートや読書メモを発信

Clickで複数コマンドの連鎖をパイプラインとして実装できる! ドキュメント「Multi Command Pipelines」の例も完全に理解しました

はじめに

マルチコマンド・パイプライニング! nikkieです。

先日Clickのドキュメントの「Multi Command Chaining」を取り上げました。
今回はそのよくあるユースケースの「Multi Command Pipelines」について、ドキュメントから理解したことをまとめます。

目次

前提

前回「Multi Command Chaining」

  • Clickでは1つのコマンドに複数のサブコマンドをまとめられる(1つのやり方がGroup)
  • Groupを作るときに@click.group(chain=True)と関数をデコレートすると、コマンド利用者はグループにまとめたコマンドを連鎖させて呼び出せる
  • 例:setuppy sdist bdist_wheel
    1. setuppy sdistをまず実行し
    2. setuppy bdist_wheelを次に実行

Clickで実装したコマンドをpyproject.tomlを使って環境にインストールする方法も取り上げました。

動作環境

前回と共通です

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(関数オブジェクト)のリストを指します
  • 例えばサブコマンドにstrip uppercaseと渡した場合
    • サブコマンド2つが順番に実行されます
    • サブコマンドすべての実行が終わったらresult_callbackprocess_pipeline関数)が実行されます
    • サブコマンドが返した関数(processor)が順に入力ファイルの各行に適用され、結果が出力ファイルに保存される、という実装になっています!
    • サブコマンドは処理を表す関数を返し、それらをまとめてresult_callbackで適用するというのがポイント!

細かい点としては

  • @click.groupinvoke_without_command=True
    • pipeline -i input.txt -o -とサブコマンド無しで読んだときの挙動です1
    • invoke_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は開いてほしい
  • click.Pathのドキュメントより
    • コマンド実行時にはできていないファイルがあるので、ファイルの存在確認はしない(exists=False。デフォルト値)
      • サブコマンドを呼び出す順番が決まっていないのでinputのファイルもexistsのチェックはできない
    • path_type引数を使ってpathlib.Pathに変換した

終わりに

Clickのドキュメントの「Multi Command Pipelines」について理解したことをまとめました。

  • 「Multi Command Pipelines」は「Multi Command Chaining」のよくあるユースケース
    • @click.group(chain=True)
  • @cli.result_callback()で、連鎖したサブコマンドがすべて実行された後のコールバックを登録する
    • 個々のサブコマンドは関数を返す
    • result_callbackで関数を1つずつ反復して適用するよう実装

宿題としたimagepipeの例は非常に肝な感触がするので、自力で書けるように読み解いていきたいと思います。


  1. ファイルパスの代わりに-と標準出力も指定できているのが、type=click.Fileの効果です。ref: https://click.palletsprojects.com/en/8.1.x/arguments/#file-arguments
  2. パイプラインを1コマンドずつ実行するというやり方もあります(コマンドの回数は増えますが、複雑さをこれが一番持ち込まないかもしれません)
  3. the files were opened immediately.」ref: https://click.palletsprojects.com/en/8.1.x/arguments/#file-path-arguments