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


87

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

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

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

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


6
Обновление : пакет ConfigArgParse является заменой argparse, который позволяет также устанавливать параметры через файлы конфигурации и / или переменные среды. См. Ответ ниже @ user553965
nealmcb

Ответы:


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
Меня попросили повторно использовать приведенный выше код, и я помещаю его в публичный домен.
Фон

22
"лобковая область" меня рассмешила. Я просто глупый ребенок.
SylvainD

1
ах! это действительно классный код, но интерполяция SafeConfigParser свойств, переопределенных командной строкой , не работает . Например, если вы добавите следующую строку в argparse-partial.config, another=%(option)s you are cruelтогда anotherона всегда будет разрешена, Hello world you are cruelдаже если optionона переопределена чем-то другим в командной строке .. argghh-parser!
ihadanny

Обратите внимание, что set_defaults работает, только если имена аргументов не содержат дефисов или подчеркиваний. Таким образом, можно выбрать --myVar вместо --my-var (что, к сожалению, довольно некрасиво). Чтобы включить чувствительность к регистру для файла конфигурации, используйте config.optionxform = str перед синтаксическим анализом файла, чтобы myVar не преобразовывался в myvar.
Кевин Бейдер,

1
Обратите внимание: если вы хотите добавить --versionпараметр в свое приложение, лучше добавить его, conf_parserчем в, parserи выйти из приложения после печати справки. Если добавить --versionк parserи запуску приложения с --versionфлагом, чем ваше приложение без необходимости пытаться открыть и синтаксический анализ args.conf_fileфайл конфигурации (который может быть неправильно сформирован или даже несуществующим, что приводит к исключению).
patryk.beza

20

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


2
просто попробовал, и остроумие отлично работает :) Спасибо, что указали на это.
red_tiger

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

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 кажется именно тем, о чем спрашивает этот вопрос, и является прозрачной заменой.

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

Вот код 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

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

# 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.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.