Подтвердить что ты не робот

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

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

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

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

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

4b9b3361

Ответ 1

Я только что обнаружил, что вы можете сделать это с помощью 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())

Ответ 2

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

Ответ 3

Я использую 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

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

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

Ответ 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")

И создайте пример config:

# Example config: /etc/myservice.conf
import os
host = os.getenv("HOST_NAME", "localhost")

Ответ 6

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

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

Вот код 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, но я не знаю каких-либо альтернатив этому.

Ответ 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'