こんにちは、いっちゃんです。10ANTZでは、サーバーサイドのソフトウェア開発に携わっています。
本記事では、2024年3月18日に開催された社内勉強会の復習をします。10ANTZのエンジニアの間では不定期で勉強会が開催されており、業務と直接関係あるかどうかに関わらず幅広いテーマが取り上げられています。今回は、OSS(オープンソースソフトウェア)「cyamli」の紹介がテーマでした。
以下では、まずcyamliについて勉強会で紹介された内容の概要を示し、その上で実際にcyamliを利用したデモンストレーションを示します。デモンストレーションでは、Python3でコマンドライン引数を持つ簡単なコンソールアプリを作成してみます。
目次
cyamliの概要
cyamli( https://github.com/Jumpaku/cyamli )は、コンソールアプリケーション用のCLIを扱うためのライブラリを生成するツールです。ここで、コンソールアプリケーションとは、CLIを備えたアプリケーションプログラムであり、CLIとは、コマンドラインのインターフェースです。cyamliで扱うことができるCLIとは具体的には、サブコマンド、オプション引数、位置引数で構成されるコマンドライン引数です。
cyamliは、YAMLファイルで定義されたCLIのスキーマに基づいて、CLIを扱うための関数や型を含むライブラリを出力します。
cyamliには、以下のような特徴があります。
- 型付けされたコマンドライン引数を定義・解釈することができる。
- YAMLファイルをスキーマの情報源として、ライブラリを自動生成できる。
- Go、Pyhton3など複数のプログラミング言語がサポートされている。
Python3コンソールアプリの作成
ここでは、勉強会で紹介があったcyamliを実際に利用して、デモンストレーション用のコンソールアプリケーションの開発を行ってみます。
コンソールアプリケーションの概要
機能
- テーブル一覧取得
- テーブル情報取得
開発言語
Python 3.10
CLIの定義
以下はデモンストレーション用のコンソールアプリケーションのCLIを定義するYAMLファイルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
name: demo-app description: demo app to fetch table information from a specified database options: -help: description: show help of this command short: -h type: boolean -version: description: show version short: -v type: boolean subcommands: list: description: list tables options: -help: description: show help of list command short: -h type: boolean -config: description: path to config file short: -c fetch: description: show information of tables options: -help: description: show help of fetch command short: -h type: boolean -config: description: path to config file short: -c -verbose: description: shows detailed log short: -v type: boolean arguments: - name: tables variadic: true description: names of tables to be described |
ここには、テーブル情報取得のためのサブコマンドfetch、テーブル一覧取得のためのサブコマンドlistが扱うことのできるコマンドライン引数の定義が含まれています。例えば、サブコマンドlistはオプション引数-help、-configを持ち、サブコマンドfetchはオプション引数-help、-config、-verboseを持ちます。また、サブコマンドfetchは位置引数tablesを持ちます。
ソースコードの生成
ここでは、以下のコマンドでcyamliを実行することにより、cli.yamlからcli_gen.pyを生成します。
1 2 |
docker run -it -v $(pwd):/workspace -w /workspace ghcr.io/jumpaku/cyamli:v1.1.7 \ cyamli generate python3 -schema-path=cli.yaml -out-path=cli_gen.py |
cyamliではDockerイメージも提供されており、本記事ではこれを用いてcyamliを実行します。ここでは、ホストマシンとDockerコンテナの間でファイルを共有するために、ホストマシンのカレントディレクトリをDockerコンテナの/workspaceにマウントしています。ホストマシンのカレントディレクトリには、上のCLIの定義が記述されたcli.yamlを配置しておきます。cyamliコマンドに対しては、サブコマンドgenerate python3を指定し、オプション引数-schema-path=cli.yaml、-out-path=cli_gen.pyを渡しています。オプション引数-schema-path=cli.yamlにより、cli.yamlがcyamliに入力されます。生成されたライブラリは、オプション引数-out-path=cli_gen.pyによりcli_gen.pyとして出力されます。
出力されたcli_gen.pyには以下のようなクラスと関数が含まれます。
1 2 3 4 5 6 7 8 9 10 |
def run(cli: CLI, args: list[str]): class CLI: class CLI_Input: class CLI_List: class CLI_List_Input: class CLI_Fetch: class CLI_Fetch_Input: def get_doc(subcommand: list[str]) -> str: |
関数runは、コマンドライン引数を解釈し、コマンドライン引数で指定されたサブコマンドの処理を呼び出します。
クラスCLIは、ルートコマンドを表しており、その子のサブコマンドをフィールドとして持ちます。CLI_Inputは、オプション引数と位置引数といったルートコマンドに対する入力を表します。クラスCLI_<サブコマンド>は、サブコマンドを表すクラスで、CLI_<サブコマンド>_Inputはサブコマンドに対する入力を表します。
関数get_docは指定されたサブコマンドに対して、自動生成されたヘルプメッセージを返します。
エントリーポイント
生成されたソースコードcli_gen.pyに含まれるクラスと関数を利用して、コンソールアプリケーションのエントリーポイントとして以下のmain.pyを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import cli_gen import sys def root_func(subcommand: list[str], args: cli_gen.CLI_Input, err: Exception | None): if err is not None: raise err print(args) print(cli_gen.get_doc(subcommand)) def list_func(subcommand: list[str], args: cli_gen.CLI_List_Input, err: Exception | None): if err is not None: raise err print(args) print(cli_gen.get_doc(subcommand)) def fetch_func(subcommand: list[str], args: cli_gen.CLI_Fetch_Input, err: Exception | None): if err is not None: raise err print(args) print(cli_gen.get_doc(subcommand)) cli = cli_gen.CLI() cli.FUNC = root_func cli.list.FUNC = list_func cli.fetch.FUNC = fetch_func cli_gen.run(cli, sys.argv) |
ここではまず、クラスCLIのFUNCフィールドに関数root_funcを割り当てることにより、ルートコマンドに対応する処理を設定します。同様に、クラスCLI_<サブコマンド>のFUNCフィールドに関数を割り当てることにより、サブコマンドに対応する処理を設定することができます。
ここで割り当てる関数は、以下のようなシグネチャを持ちます。
- 第一引数:サブコマンドを文字列のリスト(ルートコマンドの場合は空のリスト)として受け取る。
- 第二引数:オプション引数、位置引数のような入力をCLI_InputまたはCLI_<サブコマンド>_Inputのオブジェクトとして受け取る。
- 第三引数:コマンドライン引数の解釈に失敗した場合の例外をException型の引数として受け取る。
- 返り値:無し。
割り当てた関数root_func、list_func、fetch_funcは、入力を表すオブジェクトと関数get_docで自動生成されたヘルプメッセージを標準出力に表示しています。これらの関数は、入力を表すオブジェクトがサブコマンド毎の専用の型となっているため、型チェックやコード補完といったエディタの機能を活用することができます。以下の画像はVisual Studio Codeでfetch_funcの実装を記述している場面のスクリーンショットです。エディタ上で、引数argsの型に基づく補完候補が表示されていることが分かります。
実行例
pythonのDockerコンテナにカレントディレクトリをマウントして、作成したデモンストレーション用アプリケーションを実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
docker run -it -v $(pwd):/workspace -w /workspace python:3.10 \ python main.py \ -help -version # CLI_Input(opt_help=True, opt_version=True) # demo-app # # demo-app # # Description: # demo app to fetch table information from a specified database # Syntax: # $ demo-app [<option>]... # ... |
ここでは、ルートコマンドに渡したオプション引数が確認できます。また、自動生成されたヘルプメッセージも確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
docker run -it -v $(pwd):/workspace -w /workspace python:3.10 \ python main.py \ list -c=config.json # CLI_List_Input(opt_config='config.json', opt_help=False) # demo-app # # demo-app list # # Description: # list tables # # Syntax: # $ demo-app list [<option>]... # ... |
ここでは、listサブコマンドに渡したオプション引数が確認できます。オプション引数の短縮形やオプション引数への値の設定もできることが確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
docker run -it -v $(pwd):/workspace -w /workspace python:3.10 \ python main.py \ fetch -c=config.json Users Items # CLI_Fetch_Input(opt_config='config.json', opt_help=False, opt_verbose=False, arg_tables=('Users', 'Items')) # demo-app # # demo-app fetch # # Description: # show information of tables # # Syntax: # $ demo-app fetch [<option>|<argument>]... [-- [<argument>]...] # ... |
ここでは、fetchサブコマンドに渡したオプション引数と位置引数が確認できます。位置引数は可変長とすることができることも確認できます。
まとめ
本記事では、社内勉強会で紹介されたOSS「cyamli」の概要を示した上で、これを利用してPython3コンソールアプリケーションを作成してみました。cyamliは、YAMLファイルに基づいて、CLIを扱うための適切に型付けされたライブラリを生成するため、静的解析に基づくエディタ等のコーディング支援機能を活用することができます。cyamliを用いてコンソールアプリケーションを開発すれば、型チェックにより安全性を確保すると同時に、コーディング支援機能を最大限活用することで効率的に開発できると思います。