Який найкращий спосіб дозволити перевизначення параметрів конфігурації в командному рядку в 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-partіal.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, налаштування додатків, 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.