哪一种是允许在Python命令行中覆盖配置选项的最佳方法?


87

我有一个需要大量(〜30)配置参数的Python应用程序。到目前为止,我使用OptionParser类在应用程序本身中定义默认值,并且可以在调用应用程序时在命令行中更改单个参数。

现在,我想使用“适当的”配置文件,例如ConfigParser类中的文件。同时,用户仍然应该能够在命令行上更改单个参数。

我想知道是否有任何方法可以将两个步骤结合起来,例如使用optparse(或较新的argparse)来处理命令行选项,但是要以ConfigParse语法从配置文件中读取默认值。

有什么想法如何轻松地做到这一点?我不太喜欢手动调用ConfigParse,然后手动将所有选项的所有默认值设置为适当的值...


6
更新ConfigArgParse软件包是argparse的直接替代,它允许通过配置文件和/或环境变量来设置选项。 请参阅以下@ user553965的答案
nealmcb

Answers:


88

我刚刚发现您可以使用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
上面有人问我重用上面的代码,并将其放到公共领域。
冯·2013年

22
“公共领域”使我发笑。我只是个傻孩子。
SylvainD 2014年

1
啊!这确实是很酷的代码,但是对命令行覆盖的属性进行SafeConfigParser插值不起作用。例如,如果将以下行添加到argparse-partial.config中,another=%(option)s you are cruel则即使在命令行中被其他内容覆盖,它another也始终会解析为。Hello world you are crueloption
ihadanny 2014年

请注意,仅当参数名称不包含破折号或下划线时,set_defaults才有效。因此,可以选择--myVar而不是--my-var(不幸的是,这很丑陋)。要为配置文件启用区分大小写功能,请在解析文件之前使用config.optionxform = str,这样myVar不会转换为myvar。
凯文·巴德

1
请注意,如果要向--version应用程序中添加选项,最好将其添加到打印帮助后添加conf_parserparser并退出应用程序。如果您添加--versionparser您开始应用--version的标志,不是你的应用程序不必要尝试打开和解析args.conf_file配置文件(可畸形,甚至不存在的,这导致例外)。
patryk.beza

20

请查看ConfigArgParse-它是一个新的PyPI软件包(开放源代码),该软件包可替代argparse,并提供对配置文件和环境变量的支持。


2
刚刚尝试了一下,机智的作品很棒:)感谢您指出这一点。
red_tiger16's

1
谢谢-看起来不错!该网页还将ConfigArgParse与其他选项进行比较,包括argparse,ConfArgParse,appsettings,argparse_cnfig,yconf,hieropt和configurati
nealmcb

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'

ChainMap相比说链的优势是什么? dicts与按所需的优先顺序更新?与defaultdict可能有一个优点是新颖的或不支持的选项可以被设置但这是从分离ChainMap。我以为我缺少什么。

2

更新:这个答案仍然有问题;例如,它不能处理required参数,并且需要笨拙的配置语法。相反,ConfigArgParse似乎正是这个问题所要的,并且是透明的直接替换。

当前的一个问题是,如果配置文件中的参数无效,则不会出错。这是一个具有不同缺点的版本:您需要在键中包含--or-前缀。

这是python代码(带有MIT许可证的Gist链接):

# 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获得错误检查,但是我不知道有任何替代方法。


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")

1

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,Ubuntu 18.04上测试。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.