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

1# 簡単設定
2name: str  # 位置引数(required)
3
4# 詳細設定
5name: Annotated[int, typer.Argument(help="名前")]     # 位置引数(required)
6name: Annotated[int, typer.Argument(help="名前")] = 0 # 位置引数(optional)

コマンドの引数に型ヒントを指定します。 引数のヘルプなど詳細設定したい場合はtyping_extensions.Annotatedを利用します。

デフォルトでは関数のdocstringがコマンドの説明になりますが、 type.Argumenthelpで引数ごとにヘルプを追加できます。

簡単設定では required な位置引数のみ設定できます。 詳細設定では optional な位置引数も設定できます。

オプション引数したい(typer.Option

1# 簡単設定
2name: int = 0  # オプション引数(optional)
3
4# 詳細設定
5name: Annotated[int, typer.Option(help="名前")] = 0 # オプション引数(optional)
6name: Annotated[int, typer.Option(help="名前")]     # オプション引数(required)

デフォルト値を与えると、オプション引数になります。 引数のヘルプなど詳細設定したい場合はtyping_extensions.Annotatedを利用します。

デフォルトでは関数のdocstringがコマンドの説明になりますが、 type.Optionhelpでオプションごとにヘルプを追加できます。

簡単設定では optional なオプション引数のみ設定できます。 詳細設定では required なオプション引数も設定できます。

CLI Arguments / CLI Options

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は、 次のようなコマンドの使い方になります。

// optional な位置引数
$ cmd [NAME]

// required なオプション引数
$ cmd --name 名前

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

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

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

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

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

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

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

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

1import typer
2
3@app.command()
4def コマンド名(is_debug: bool = False):
5    if is_debug:
6        logger.error(f"DEBUG mode : {is_debug}")
7        typer.Exit()

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

PoetryでCLIしたい

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

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