コマンドに全てのオプションを設定で渡すのがだるいので、設定ファイルでデフォルトの設定を指定したかった

はじめに

# オプション引数がないとなんだかわからない
myapp foo bar boo baz bee

# オプション引数を指定できると楽
myapp --foo=foo --bar=bar --boo=boo --baz bee

# でも、本当は以下の様にして使いたい
myapp bee --config=~/.config/myapp

argparseなどでコマンドを作成した時に幾つかオプションを指定可能にする。 これはsys.argvなどを直接扱うよりはずっと使いやすい。 一方で指定するオプションの数が増えてくると次第にだるくなってくる。 すべてのオプションを指定可能にする一方でオプションで与えられるパラメーターについてのデフォルト値を別途設定したい。

雑にdelegationしてみる

結局、argparseで行うparserの設定は parse_args() を使うためにある。 これでコマンドライン引数の内容が解析された結果がオブジェクトとなって返ってくる。 このオブジェクトを委譲を行うwrapperで包んであげれば良いような気がした。

以下のようなWrapperオブジェクトで包んで見る

class MixiedArgs(object):
    def __init__(self, args, defaults):
        self.args = args
        self.defaults = defaults

    def __getattr__(self, k):
        return getattr(self.args, k, None) or getattr(self.defaults, k)


args = MixedArgs(a, b)
args.name # a.nameが無かったら b.nameを探す

これをargparseを使ったものに組み込むと以下の様な形

parser = make_parser()

# name messageは設定値
class DefaultConfig:
    name = "foo"
    message = "{name}: hai"


# --nameが不足しているが、DefaultConfigに委譲される
args = parser.parse_args(["--message", "{name}: hello"])
args = (MixiedArgs(args, DefaultConfig))
print(args.name) # -> "foo"

とりあえずデフォルトの設定値的なものとして利用する事はできそう

config(--profile)を読み込む

設定ファイルの在処をオプションで指定できるようにする。設定ファイルのフォーマットはなんでも良いがとりあえずjsonで書くことにした。 別途以下のようなアクションを作っておく。

class ObjectLikeDict(dict):
    __getattr__ = dict.get


class LoadJSONConfigAction(argparse.Action):
   """parser.add_argument("--profile", action=LoadJSONConfigAction) などとして利用"""
    def __call__(self, parser, namespace, val, option_string=None):
        import json

        if val.startswith("file://"):
            with open(val.lstrip("file://")) as rf:
                data = json.load(rf, object_pairs_hook=ObjectLikeDict)
        else:
            data = json.loads(val, object_pairs_hook=ObjectLikeDict)
        setattr(namespace, self.dest, data)

これを使うと以下のように書ける

parser = make_parser()

# 直接jsonで渡された場合
args = parser.parse_args(["--message", "{name}: bye", "--config", '{"name": "bar"}'])
run(MixiedArgs(args, args.config))

# jsonのパスを指定する場合
args = parser.parse_args(["--config=file://config.json"])
run(MixiedArgs(args, args.config))

gist

argparse_loader.py · GitHub