【问题标题】:Which is the best way to allow configuration options be overridden at the command line in Python?在 Python 的命令行中允许覆盖配置选项的最佳方法是什么?
【发布时间】:2011-04-06 07:24:13
【问题描述】:

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

现在我想使用“正确的”配置文件,例如来自 ConfigParser 类。同时,用户应该仍然可以在命令行中更改个别参数。

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

任何想法如何以简单的方式做到这一点?我真的不喜欢手动调用 ConfigParse,然后手动将所有选项的所有默认值设置为适当的值...

【问题讨论】:

标签: python command-line configuration-files


【解决方案1】:

我不能说这是最好的方法,但我有一个我制作的 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))

Feel free to browse the source。测试位于同级目录中。

【讨论】:

    【解决方案2】:

    我正在使用带有子命令的 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设置了config文件路径,而config文件设置了argparse defaults...愚蠢的鸡蛋问题
    【解决方案3】:

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

    【讨论】:

    • 我被要求重用上面的代码,特此将其放入公共域。
    • “公共领域”让我发笑。我只是个傻孩子。
    • 啊!这是非常酷的代码,但是命令行覆盖的属性的 SafeConfigParser 插值不起作用。例如。如果您将以下行添加到 argparse-partial.config another=%(option)s you are cruel 然后 another 将始终解析为 Hello world you are cruel 即使 option 在命令行中被覆盖为其他内容..argghh-parser!
    • 请注意,set_defaults 仅在参数名称不包含破折号或下划线时才有效。因此,可以选择 --myVar 而不是 --my-var (不幸的是,这很丑陋)。要为配置文件启用区分大小写,请在解析文件之前使用 config.optionxform = str,因此 myVar 不会转换为 myvar。
    • 请注意,如果您想将--version 选项添加到您的应用程序,最好将其添加到conf_parser,而不是添加到parser,并在打印帮助后退出应用程序。如果您将--version 添加到parser 并使用--version 标志启动应用程序,那么您的应用程序会不必要地尝试打开和解析args.conf_file 配置文件(可能格式错误甚至不存在,这会导致异常) .
    【解决方案4】:

    查看ConfigArgParse - 它是一个新的 PyPI 包 (open source),它作为 argparse 的替代品,增加了对配置文件和环境变量的支持。

    【讨论】:

    • 刚刚尝试过,机智的效果很好:) 感谢您指出这一点。
    • 谢谢 - 看起来不错!该网页还将 ConfigArgParse 与其他选项进行比较,包括 argparse、ConfArgParse、appsettings、argparse_cnfig、yconf、hieropt 和 configurati
    【解决方案5】:

    试试这个方法

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

    【讨论】:

    【解决方案6】:

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

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

    这是 python 代码(Gist link 使用 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 获得错误检查,但我不知道有任何替代方法。

    【讨论】:

      【解决方案7】:

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

      【讨论】:

        【解决方案8】:

        您可以使用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 是分开的。我想我错过了什么。
        【解决方案9】:

        parse_args() 可以采用现有的命名空间并将现有的命名空间与它当前正在解析的参数/选项合并; “当前解析”中的选项 args/options 优先于覆盖现有命名空间中的任何内容:

        foo_parser = argparse.ArgumentParser()
        foo_parser.add_argument('--foo')
        
        ConfigNamespace = argparse.Namespace()
        setattr(ConfigNamespace, 'foo', 'foo')
        
        args = foo_parser.parse_args([], namespace=ConfigNamespace)
        print(args)
        # Namespace(foo='foo')
        
        # value `bar` will override value `foo` from ConfigNamespace
        args = foo_parser.parse_args(['--foo', 'bar'], namespace=ConfigNamespace)
        print(args)
        # Namespace(foo='bar')
        

        我已经模拟了一个真正的配置文件选项。我解析了两次,一次作为“预解析”来查看用户是否传递了一个配置文件,然后再次进行了集成可选配置文件命名空间的“最终解析”。

        我有这个非常简单的 JSON 配置文件,config.ini

        [DEFAULT]
        delimiter = |
        

        当我运行这个时:

        import argparse
        import configparser
        
        parser = argparse.ArgumentParser()
        parser.add_argument('-c', '--config-file', type=str)
        parser.add_argument('-d', '--delimiter', type=str, default=',')
        
        # Parse cmd-line args to see if config-file is specified
        pre_args = parser.parse_args()
        
        # Even if config is not specified, need empty Namespace to pass to final `parse_args()`
        ConfigNamespace = argparse.Namespace()
        
        if pre_args.config_file:
            config = configparser.ConfigParser()
            config.read(pre_args.config_file)
        
            for name, val in config['DEFAULT'].items():
                setattr(ConfigNamespace, name, val)
        
        
        # Parse cmd-line args again, merging with ConfigNamespace, 
        # cmd-line args take precedence
        args = parser.parse_args(namespace=ConfigNamespace)
        
        print(args)
        

        使用各种命令行设置,我得到:

        ./main.py
        Namespace(config_file=None, delimiter=',')
        
        ./main.py -c config.ini
        Namespace(config_file='config.ini', delimiter='|')
        
        ./main.py -c config.ini -d \;
        Namespace(config_file='config.ini', delimiter=';')
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-01-08
          • 2021-08-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-10-04
          • 2023-03-25
          相关资源
          最近更新 更多