walkingmask’s development log

IT系の情報などを適当に書いていきます

MENU

argparseでconfigファイルを使えるようにする

CLI ツールを作るときに、設定ファイルを使いたい場合があると思います。

Python には argparse という素晴らしい CLI 用の引数パーサがありますが、設定ファイルの読み込みには対応していません。

configparser という設定ファイル読み込み用モジュールもありますが、argparse と連携させるのを想定されてません。

そこで、argparse に設定ファイルを読ませる方法を探しました。

これらはどちらも同じ目的を達成するアプローチですが、前者は argparse の subparser について触れていません。また、デフォルトを dict で設定するのは個人的に嫌でした。

後者は使ってみたところ、config の section が作れない、subcommand の config の設定ができない等の問題があるようでした。

以上を踏まえて、

  • argparse の command オプションで -c, --config_file を受け取る
  • config_file が指定された場合はそれで command、subcommand のデフォルト値を上書きする
  • config_file は section の設定が可能
  • config_file は subcommand 用の記述が可能
  • command, subcommand に CLI でオプションが与えられた場合は config で設定した値を上書きする

といった機能を考えました。実装したものが以下のものです。

github.com

インストールは、

git clone https://github.com/walkingmask/argconfigparse_dev.git
cd argconfigparse_dev
pip install -e argconfigparse

です。

言葉の定義

ちょっとだけこの記事での言葉を定義しておきます。

  • arg: parser.add_argument('arg') のように -- なしで設定されたもの、必須
  • config: parser.add_argument('--sub_config') のように設定されたもの、そのデフォルト値、config_file に設定されたその値
  • option: CLI引数で config を指定されたもの、デフォルト値や config_file を上書きする

実装について

ごちゃごちゃ色々置いてありますが、メインのものはこれだけです。

特に、_cli_parser がコア部分です。まずはその中の build_parser のポイントを箇条書きしていきます。

  • parser には必ず conflict_handler='resolve' をつける
    • これはデフォルト値を上書きした時に add_argument を再度実行する実装を以前行っていたため付けてましたが、今は必要ないかも
  • config ファイル用の add_argument
  • command の arg, config を設定
    • command arg1 ... --config1 config1 ... の部分がこの時点で完成
  • subcommand 用の parser 群を作成
    • dest, required の指定で必須化 (参考)
  • 全ての subcommands の add_argument を読んでデフォルト値の設定を行う
    • command ... subcommand sub_arg1 ... --sub_config1 ... の部分が完成
  • 各 subcommand に global な config を設定
    • 例えば --root_path のような全てのサブコマンドで使いたいようなもの

これでパーサは完成しました。この時点でパースすると、デフォルトの設定が得られます。

次に設定ファイルを読み込み、option で上書きする手順です。

  • 一旦パースして、subcommand 名と config_file、デフォルト値を得る
    • この時点で arg が足りない、subcommand が求めている global な設定等が足りないとエラーになります
  • config_file が指定されている場合はそれを読み込む
    • config_file は汎用性を考えて json を想定、configparser は今回使わなかった
    • config から読み込んだ値を対応する argparse の config のデフォルト値に設定します
    • この辺は argparse の private メソッドでゴリゴリやってます...
  • デフォルト値を上書きした場合は、パースし直します
    • これによって、新たに得られるarg, config の値は config_file によって設定されたものに
    • CLI--sub_config1 のように option が指定されていればその値が取得されます
  • 最後に args, subcommand_args, config を分けて返します

これが基本の動作です。ここにたどり着くまで結構悩みながら試行錯誤しました。。。

特に、デフォルト値 -> config -> option の順で上書きしていくロジックは parse を二回するなど苦しい感じですね。

その他こだわりポイント

get_subcommandsset_config 等は個人的なこだわりで作りました。コマンドを作る際、ディレクトリ構造はだいたいこんな感じになると思います。

command/
    __init__.py
    subcommand1/
        __init__.py
        ...
    ...

このディレクトリ構造からコマンドを設定していくためには、subcommand/__init__.py 等に何か作って、command/__init__.py でそれを import することになると思うのですが、そうすると、

from subcommand1 import foo as sub1_foo, ...
from subcommand2 import foo as sub2_foo, ...
from subcommand3 import foo as sub3_foo, ...

という悲惨なことに...。そこで、importlib.import_modulesetuptools.find_packages を使って、自動で subcommands を走査します。

is_subcommand = Truesubcommand/__init__.py に設定された package を subcommand とみなし、set_args, set_config, check_config を自動で import し実行します。それらが定義されていなくても問題はありません。

また、command/__init__.py にある set_args, set_config, set_global_config は定義されているだけで実行されます。

is_subcommand を設定しなくても、CLIParser.register で import した subcommand を登録しすることも可能です。

from argconfigparse import CLIParser
import subcommand3

parser = CLIParser()
parser.get_subcommands()
parser.register(subcommand3)
parser.parse()

うー。シンプル。

課題

今のところ、nargs といったオプションは想定してない (configargparse では実装されていた) ので、今後必要が出てきたら実装していきたいですね。