Python의 명령 줄에서 구성 옵션을 재정의하는 가장 좋은 방법은 무엇입니까?


87

꽤 많은 (~ 30) 구성 매개 변수가 필요한 Python 애플리케이션이 있습니다. 지금까지는 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
위의 코드를 재사용하도록 요청을 받았으며 이로써이를 공개 도메인에 배치합니다.
Von

22
'퍼블릭 도메인'이 나를 웃게 만들었다. 난 그냥 멍청한 아이 야.
SylvainD

1
으악! 이것은 정말 멋진 코드이지만 명령 줄에 의해 재정의 된 속성의 SafeConfigParser 보간은 작동하지 않습니다 . 예 : 당신은-partial.config을 argparse에 다음 행을 추가하는 경우 another=%(option)s you are cruel다음 another항상로 해석 할 Hello world you are cruel경우에도 option명령 행 .. argghh 파서에서 다른 것으로 오버라이드 (override)입니다!
ihadanny 2014

set_defaults는 인수 이름에 대시 나 밑줄이 포함되지 않은 경우에만 작동합니다. 따라서 --my-var 대신 --myVar를 선택할 수 있습니다 (불행히도 매우 추합니다). 구성 파일의 대소 문자 구분을 활성화하려면 파일을 구문 분석하기 전에 config.optionxform = str을 사용하여 myVar가 myvar로 변환되지 않도록하십시오.
Kevin Bader 2016 년

1
참고 추가 할 경우 해당 --version응용 프로그램에 옵션을,이에 추가하는 것이 좋습니다 conf_parser보다는 parser도움을 인쇄 한 후 응용 프로그램을 종료합니다. 당신은 추가하는 경우 --versionparser당신은과 응용 프로그램 시작 --version응용 프로그램이 불필요하게 열고 구문 분석을 시도보다, 플래그 args.conf_file(리드 예외, 부정 또는 존재하지 않는 수 있습니다) 구성 파일.
patryk.beza

20

ConfigArgParse를 확인하세요 . 구성 파일 및 환경 변수에 대한 지원이 추가 된 argparse를 대체 하는 새로운 PyPI 패키지 ( 오픈 소스 )입니다.


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'

dicts원하는 우선 순위 로 업데이트 된 체인보다 ChainMap의 장점은 무엇입니까 ? 으로 defaultdict이점 가능성이있다 소설이나 지원되지 않는 옵션이 설정 될 수 있지만, 그의는 분리로 ChainMap. 내가 뭔가를 놓치고 있다고 생각합니다.
Dan

2

업데이트 :이 답변에는 여전히 문제가 있습니다. 예를 들어 required인수를 처리 할 수없고 어색한 구성 구문이 필요합니다. 대신 ConfigArgParse 는이 질문이 요구하는 것과 정확히 같으며 투명한 드롭 인 대체품입니다.

현재의 한 가지 문제 는 구성 파일의 인수가 유효하지 않은 경우 오류가 발생하지 않는다는 것입니다. 다음은 다른 단점이있는 버전입니다 . 키에 --또는 -접두사 를 포함해야 합니다.

다음은 파이썬 코드입니다 ( MIT 라이센스가있는 Gist 링크 ) :

# 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_argsArgumentParser에서 오류 검사를 얻기 위해 다소 해킹을 사용 하지만 이에 대한 대안을 알지 못합니다.


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에서 테스트되었습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.