CLIしたい(typer

$ pip3 install typer[all]
 1# パッケージ/cli.py
 2import typer
 3
 4app = typer.Typer()
 5
 6@app.command()
 7def hello(name: str):
 8    print(f"Hello {name}")
 9
10
11@app.command()
12def goodbye(name: str, formal: bool = False):
13    if formal:
14        print(f"Goodbye Ms. {name}. Have a good day.")
15    else:
16        print(f"Bye {name}!")
17
18if __name__ == "__main__":
19    app()

Typerを使うと、サブコマンド付きのCLIをすぐに作ることができます。 上記のサンプルは、公式ドキュメントのAn example with two subcommands - Typerにあるコードです。 とりあえずこのサンプルコードをcli.pyのようなファイルにコピペして動かしてみるだけで、使い方がわかると思います。

これまで、CLIを作るときの引数/オプション解析は、定番のPython標準argparseパッケージを使っていましたが、サブコマンドを作るのはちょっと大変な印象でした。 (やってみようと思って調べたことはありますが実際に作ったことはない・・・) Typerは、引数とオプション、コマンドの説明も、いつもの関数を作る作業の延長ででき、非常に簡単だと感じました。

コマンドしたい(typer.Argument / typer.Option

 1import typer
 2from typing_extensions import Annotated
 3
 4def vth(
 5    """
 6    コマンドの説明
 7    """
 8    ch Annotated[int, typer.Argument(help="チャンネル番号")],
 9    vth: Annotated[int, typer.Argument(help="スレッショルド値")],
10    max_retry: Annotated[int, typer.Option(help="リトライ数")] = 3,
11    load_from: Annotated[str, typer.Option(help="設定ファイル名")] = "daq.toml"
12    ):
13    pass
14
15if __name__ == "__main__":
16    typer.run(vth)

コマンドとして実行したい関数名をtyper.runに渡します。 関数のdocstringがコマンドの説明になります。

typer.Argumentで引数を設定できます。 typer.Optionでオプション引数を設定できます。

typing_extensions.Annotatedで引数の説明を追加できます。 追加方法などは、ドキュメントを参照してください。

ヒント

typer.Argument / typer.Optionと デフォルト値のあり / なしを考えると 以下の表のような引数名のパターンが考えられます。

デフォルト値なし

デフォルト値あり

typer.Argument

CLI arguments

optional CLI arguments

typer.Option

required CLI options

CLI options

通常は、 CLI arguments(必須の位置引数)、 CLI options(オプション引数) のみのコマンドを設計するとよいと思います。

ただし、 optional CLI arguments(位置引数なのにオプション)、 required CLI options(オプション引数なのに必須) も、名前がちょっとおかしい気がしますが、定義できるようになっています。

サブコマンドしたい(@app.command

 1import typer
 2
 3app = typer.Typer()
 4
 5@app.command()
 6def vth(
 7    """
 8    コマンドの説明
 9    """
10    ch Annotated[int, typer.Argument(help="チャンネル番号")],
11    vth: Annotated[int, typer.argument(help="スレッショルド値")],
12    max_retry: Annotated[int, typer.argument(help="リトライ数")] = 3,
13    load_from: Annotated[str, typer.argument(help="設定ファイル名")] = "daq.toml"
14    ):
15    pass
16
17if __name__ == "__main__":
18    app()

@app.commandデコレーターでサブコマンドを定義できます。

注釈

appの部分は任意のオブジェクト名を使用できます。 上記のサンプルではapp = typer.Typer()を作成しているため、デコレーターは@app.commandになります。

出力に色をつけたい(from rich import print

1import typer
2from rich import print
3
4...省略...

richパッケージのprintを使うと、出力を色付けできます。 色付けの詳細や、その他の表示形式はドキュメントを参照してください。

中断/終了したい(typer.Exit

1@app.command()
2def コマンド名(引数):
3    if 引数がおかしい条件:
4        logger.error(f"値が正しくないです : {引数}")
5        typer.Exit()

Typerを使うと、引数のバリデーションを柔軟に書くことができます。 引数の値が正しくない場合に終了する場合、typer.Exitが使えます。 わざわざimport sysしてsys.exitする必要がないので便利です。

PoetryでCLIしたい

1# pyproject.toml
2
3[tool.poetry.scripts]
4CLI = "パッケージ.cli:app"

Poetryを使って自作CLIを作成する場合は、pyproject.toml[tool.poetry.scripts]セクションに記述します。 詳しくは公式ドキュメントのBuilding a package - Typerを参照してください。