在Python中,如何最佳地允许命令行覆盖配置选项?

103 投票
11 回答
53412 浏览
提问于 2025-04-16 03:32

我有一个Python应用程序,需要设置大约30个配置参数。到目前为止,我一直使用OptionParser类在应用程序内部定义默认值,同时允许用户在命令行中调用应用程序时更改个别参数。

现在,我想使用“正式的”配置文件,比如ConfigParser类来管理这些参数。同时,用户仍然应该能够在命令行中更改个别参数。

我在想有没有办法把这两者结合起来,比如使用optparse(或者更新的argparse)来处理命令行选项,但又能从ConfigParse格式的配置文件中读取默认值。

有没有简单的方法可以做到这一点?我不太想手动调用ConfigParse,然后再手动设置所有选项的默认值...

11 个回答

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')
28

看看这个叫 ConfigArgParse 的东西吧——它是一个新的PyPI包(开源的),可以直接替代argparse,并且增加了对配置文件和环境变量的支持。

110

我刚发现可以用 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())

撰写回答