Какой лучший способ разрешить переопределение параметров конфигурации в командной строке в Python? - PullRequest
62 голосов
/ 31 августа 2010

У меня есть приложение на Python, которому нужно немало (~ 30) параметров конфигурации. До сих пор я использовал класс OptionParser для определения значений по умолчанию в самом приложении с возможностью изменения отдельных параметров в командной строке при вызове приложения.

Теперь я хотел бы использовать «правильные» файлы конфигурации, например, из класса ConfigParser. В то же время пользователи по-прежнему должны иметь возможность изменять отдельные параметры в командной строке.

Мне было интересно, есть ли способ объединить два шага, например, используйте optparse (или более новый argparse) для обработки параметров командной строки, но читая значения по умолчанию из файла конфигурации в синтаксисе ConfigParse.

Есть идеи, как это сделать легко? Мне не очень нравится вручную вызывать ConfigParse, а затем вручную устанавливать все значения по умолчанию для всех оптино на соответствующие значения ...

Ответы [ 8 ]

65 голосов
/ 29 апреля 2011

Я только что обнаружил, что вы можете сделать это с argparse.ArgumentParser.parse_known_args(). Начните с использования parse_known_args() для анализа файла конфигурации из командной строки, затем прочитайте его с ConfigParser и установите значения по умолчанию, а затем проанализируйте остальные параметры с помощью parse_args(). Это позволит вам иметь значение по умолчанию, переопределить его с помощью файла конфигурации, а затем переопределить его с помощью параметра командной строки. E.g.:

По умолчанию без ввода пользователя:

$ ./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())
13 голосов
/ 25 июня 2014

Извлечение ConfigArgParse - это новый пакет PyPI ( с открытым исходным кодом ), который служит заменой для argparse с добавленной поддержкой файлов конфигурации и переменных среды.

9 голосов
/ 01 декабря 2010

Я использую 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')
4 голосов
/ 31 августа 2010

Я не могу сказать, что это лучший способ, но у меня есть класс 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))

Не стесняйтесь просматривать источник .Тесты находятся в одном каталоге.

1 голос
/ 13 февраля 2018

Обновление: у этого ответа все еще есть проблемы; например, он не может обрабатывать required аргументы и требует неуклюжего синтаксиса конфигурации. Вместо этого ConfigArgParse кажется именно тем, о чем спрашивает этот вопрос, и является прозрачной заменой.

Одна проблема с current заключается в том, что она не выдаст ошибку, если аргументы в файле конфигурации недопустимы. Вот версия с другим недостатком: вам нужно включить префикс -- или - в ключи.

Вот код Python ( Ссылка Gist с лицензией 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, но я не знаю никаких альтернатив этому.

1 голос
/ 25 ноября 2015

Попробуйте так

# 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")
0 голосов
/ 15 июня 2019

Вы можете использовать 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'
0 голосов

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.

...