Pythonのコマンドラインで構成オプションを上書きできるようにするための最良の方法はどれですか?


89

かなりの数(〜30)の構成パラメーターを必要とするPythonアプリケーションがあります。これまで、OptionParserクラスを使用してアプリ自体のデフォルト値を定義し、アプリケーションを呼び出すときにコマンドラインで個々のパラメーターを変更する可能性がありました。

ここで、たとえばConfigParserクラスの「適切な」構成ファイルを使用したいと思います。同時に、ユーザーはコマンドラインで個々のパラメーターを変更できる必要があります。

2つのステップを組み合わせる方法があるかどうか疑問に思いました。たとえば、optparse(または新しいargparse)を使用してコマンドラインオプションを処理しますが、ConfigParse構文で構成ファイルからデフォルト値を読み取ります。

これを簡単な方法で行う方法はありますか?ConfigParseを手動で呼び出してから、すべてのオプションのすべてのデフォルトを適切な値に手動で設定するのはあまり好きではありません...


6
更新ConfigArgParseパッケージは、argparseのドロップイン代替品であり、構成ファイルや環境変数を介してオプションを設定することもできます。 @ user553965による以下の回答を参照してください
nealmcb

回答:


89

でこれを実行できることを発見しましたargparse.ArgumentParser.parse_known_args()。を使用parse_known_args()してコマンドラインから構成ファイルを解析することから始め、次にConfigParserでそれを読み取り、デフォルトを設定してから、残りのオプションをparse_args()。で解析します。これにより、デフォルト値を設定し、それを構成ファイルでオーバーライドしてから、コマンドラインオプションでオーバーライドすることができます。例えば:

ユーザー入力なしのデフォルト:

$ ./argparse-partial.py
Option is "default"

構成ファイルからのデフォルト:

$ cat argparse-partial.config 
[Defaults]
option=Hello world!
$ ./argparse-partial.py -c argparse-partial.config 
Option is "Hello world!"

構成ファイルからのデフォルト、コマンドラインでオーバーライド:

$ ./argparse-partial.py -c argparse-partial.config --option override
Option is "override"

argprase-partial.pyが続きます。-h助けを適切に扱うのは少し複雑です。

import argparse
import ConfigParser
import sys

def main(argv=None):
    # Do argv default this way, as doing it in the functional
    # declaration sets it at compile time.
    if argv is None:
        argv = sys.argv

    # Parse any conf_file specification
    # We make this parser with add_help=False so that
    # it doesn't parse -h and print help.
    conf_parser = argparse.ArgumentParser(
        description=__doc__, # printed with -h/--help
        # Don't mess with format of description
        formatter_class=argparse.RawDescriptionHelpFormatter,
        # Turn off help, so we print all options in response to -h
        add_help=False
        )
    conf_parser.add_argument("-c", "--conf_file",
                        help="Specify config file", metavar="FILE")
    args, remaining_argv = conf_parser.parse_known_args()

    defaults = { "option":"default" }

    if args.conf_file:
        config = ConfigParser.SafeConfigParser()
        config.read([args.conf_file])
        defaults.update(dict(config.items("Defaults")))

    # Parse rest of arguments
    # Don't suppress add_help here so it will handle -h
    parser = argparse.ArgumentParser(
        # Inherit options from config_parser
        parents=[conf_parser]
        )
    parser.set_defaults(**defaults)
    parser.add_argument("--option")
    args = parser.parse_args(remaining_argv)
    print "Option is \"{}\"".format(args.option)
    return(0)

if __name__ == "__main__":
    sys.exit(main())

20
上記のコードを再利用するように求められたので、ここにそれをpubicドメインに配置します。
フォン

22
「パブリックドメイン」は私を笑わせました。私はただの愚かな子供です。
SylvainD 2014年

1
ああ!これは本当にクールなコードですが、コマンドラインでオーバーライドされたプロパティのSafeConfigParser補間は機能しません。あなたはargparse-partial.configをするには、以下の行を追加した場合などは、another=%(option)s you are cruelその後、anotherいつもに解決するだろうHello world you are cruel場合でもoption.. argghhパーサーのコマンドラインで何か他のものに上書きされます!
ihadanny 2014

set_defaultsは、引数名にダッシュまたはアンダースコアが含まれていない場合にのみ機能することに注意してください。したがって、-my-varの代わりに--myVarを選択できます(残念ながら、これは非常に醜いです)。構成ファイルで大文字と小文字を区別できるようにするには、ファイルを解析する前にconfig.optionxform = strを使用して、myVarがmyvarに変換されないようにします。
Kevin Bader 2016年

1
--versionアプリケーションにオプションを追加する場合は、ヘルプを印刷した後でアプリケーションに追加して終了するconf_parserよりも、オプションを追加する方がよいことに注意してくださいparser。追加--versionparser--versionフラグを付けてアプリケーションを起動した場合、アプリケーションは不必要にargs.conf_file構成ファイルを開いて解析しようとします(これは、形式が正しくないか、存在しない場合もあり、例外が発生します)。
patryk.beza 2017

21

ConfigArgParseをチェックしてください-その新しいPyPIパッケージ(オープンソース)は、構成ファイルと環境変数のサポートが追加されたargparseの代わりに機能します。


3
試してみたところ、ウィットはうまく機能しました:)これを指摘してくれてありがとう。
red_tiger 2016

2
ありがとう-よさそうだ!このWebページでは、ConfigArgParseを、argparse、ConfArgParse、appsettings、argparse_cnfig、yconf、hieropt、configuratiなどの他のオプションとも比較しています
nealmcb 2017年

9

このようなタスクを処理するために、ConfigParserとargparseをサブコマンドとともに使用しています。以下のコードの重要な行は次のとおりです。

subp.set_defaults(**dict(conffile.items(subn)))

これにより、サブコマンドのデフォルト(argparseから)が構成ファイルのセクションの値に設定されます。

より完全な例を以下に示します。

####### content of example.cfg:
# [sub1]
# verbosity=10
# gggg=3.5
# [sub2]
# host=localhost

import ConfigParser
import argparse

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()

parser_sub1 = subparsers.add_parser('sub1')
parser_sub1.add_argument('-V','--verbosity', type=int, dest='verbosity')
parser_sub1.add_argument('-G', type=float, dest='gggg')

parser_sub2 = subparsers.add_parser('sub2')
parser_sub2.add_argument('-H','--host', dest='host')

conffile = ConfigParser.SafeConfigParser()
conffile.read('example.cfg')

for subp, subn in ((parser_sub1, "sub1"), (parser_sub2, "sub2")):
    subp.set_defaults(**dict(conffile.items(subn)))

print parser.parse_args(['sub1',])
# Namespace(gggg=3.5, verbosity=10)
print parser.parse_args(['sub1', '-V', '20'])
# Namespace(gggg=3.5, verbosity=20)
print parser.parse_args(['sub1', '-V', '20', '-G','42'])
# Namespace(gggg=42.0, verbosity=20)
print parser.parse_args(['sub2', '-H', 'www.example.com'])
# Namespace(host='www.example.com')
print parser.parse_args(['sub2',])
# Namespace(host='localhost')

私の問題は、argparseが設定ファイルのパスを設定し、設定ファイルがargparseのデフォルトを設定することです...愚かな鶏が先か卵が先か問題
Olivervbk 2011年

4

それが最善の方法であるとは言えませんが、私が作成したOptionParserクラスがあります。これは、設定ファイルセクションからのデフォルトでoptparse.OptionParserのように機能します。きみにあげる...

class OptionParser(optparse.OptionParser):
    def __init__(self, **kwargs):
        import sys
        import os
        config_file = kwargs.pop('config_file',
                                 os.path.splitext(os.path.basename(sys.argv[0]))[0] + '.config')
        self.config_section = kwargs.pop('config_section', 'OPTIONS')

        self.configParser = ConfigParser()
        self.configParser.read(config_file)

        optparse.OptionParser.__init__(self, **kwargs)

    def add_option(self, *args, **kwargs):
        option = optparse.OptionParser.add_option(self, *args, **kwargs)
        name = option.get_opt_string()
        if name.startswith('--'):
            name = name[2:]
            if self.configParser.has_option(self.config_section, name):
                self.set_default(name, self.configParser.get(self.config_section, name))

ソースを自由に閲覧してください。テストは兄弟ディレクトリにあります。


3

あなたはChainMapを使うことができます

A ChainMap groups multiple dicts or other mappings together to create a single, updateable view. If no maps are specified, a single empty dictionary is provided so that a new chain always has at least one mapping.

コマンドライン、環境変数、構成ファイルの値を組み合わせることができ、値がない場合はデフォルト値を定義します。

import os
from collections import ChainMap, defaultdict

options = ChainMap(command_line_options, os.environ, config_file_options,
               defaultdict(lambda: 'default-value'))
value = options['optname']
value2 = options['other-option']


print(value, value2)
'optvalue', 'default-value'

dicts希望する優先順位で更新されたチェーンと言うよりも、ChainMapの利点は何ですか?defaultdict利点は、おそらくそこの小説や、サポートされていないオプションを設定することができますが、その者からは別としてChainMap。私は何かが足りないと思います。
ダン

2

更新:この回答にはまだ問題があります。たとえば、required引数を処理できず、厄介な構成構文が必要です。代わりに、ConfigArgParseはまさにこの質問が求めるもののようであり、透過的なドロップインの代替品です。

現在の問題の1つは、構成ファイルの引数が無効な場合でもエラーが発生しないことです。欠点が異なるバージョンを次に示します。キーに--または-プレフィックスを含める必要があります。

Pythonコードは次のとおりです(MITライセンスの要点リンク):

# Filename: main.py
import argparse

import configparser

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--config_file', help='config file')
    args, left_argv = parser.parse_known_args()
    if args.config_file:
        with open(args.config_file, 'r') as f:
            config = configparser.SafeConfigParser()
            config.read([args.config_file])

    parser.add_argument('--arg1', help='argument 1')
    parser.add_argument('--arg2', type=int, help='argument 2')

    for k, v in config.items("Defaults"):
        parser.parse_args([str(k), str(v)], args)

    parser.parse_args(left_argv, args)
print(args)

設定ファイルの例を次に示します。

# Filename: config_correct.conf
[Defaults]
--arg1=Hello!
--arg2=3

今、実行しています

> python main.py --config_file config_correct.conf --arg1 override
Namespace(arg1='override', arg2=3, config_file='test_argparse.conf')

ただし、構成ファイルにエラーがある場合:

# config_invalid.conf
--arg1=Hello!
--arg2='not an integer!'

スクリプトを実行すると、必要に応じてエラーが発生します。

> python main.py --config_file config_invalid.conf --arg1 override
usage: test_argparse_conf.py [-h] [--config_file CONFIG_FILE] [--arg1 ARG1]
                             [--arg2 ARG2]
main.py: error: argument --arg2: invalid int value: 'not an integer!'

主な欠点は、これparser.parse_argsがArgumentParserからエラーチェックを取得するためにややハッキーに使用されることですが、これに代わるものはありません。


2

fromfile_prefix_chars

完璧なAPIではないかもしれませんが、知っておく価値はあります。

main.py

#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
parser.add_argument('-a', default=13)
parser.add_argument('-b', default=42)
print(parser.parse_args())

次に:

$ printf -- '-a\n1\n-b\n2\n' > opts.txt
$ ./main.py
Namespace(a=13, b=42)
$ ./main.py @opts.txt
Namespace(a='1', b='2')
$ ./main.py @opts.txt -a 3 -b 4
Namespace(a='3', b='4')
$ ./main.py -a 3 -b 4 @opts.txt
Namespace(a='1', b='2')

ドキュメント:https//docs.python.org/3.6/library/argparse.html#fromfile-prefix-chars

Python 3.6.5、Ubuntu18.04でテスト済み。


1

このようにしてみてください

# encoding: utf-8
import imp
import argparse


class LoadConfigAction(argparse._StoreAction):
    NIL = object()

    def __init__(self, option_strings, dest, **kwargs):
        super(self.__class__, self).__init__(option_strings, dest)
        self.help = "Load configuration from file"

    def __call__(self, parser, namespace, values, option_string=None):
        super(LoadConfigAction, self).__call__(parser, namespace, values, option_string)

        config = imp.load_source('config', values)

        for key in (set(map(lambda x: x.dest, parser._actions)) & set(dir(config))):
            setattr(namespace, key, getattr(config, key))

これを使って:

parser.add_argument("-C", "--config", action=LoadConfigAction)
parser.add_argument("-H", "--host", dest="host")

そして、設定例を作成します。

# Example config: /etc/myservice.conf
import os
host = os.getenv("HOST_NAME", "localhost")
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.