Cách nào tốt nhất để cho phép các tùy chọn cấu hình được ghi đè tại dòng lệnh trong Python?


87

Tôi có một ứng dụng Python cần khá nhiều (~ 30) tham số cấu hình. Cho đến nay, tôi đã sử dụng lớp OptionParser để xác định các giá trị mặc định trong chính ứng dụng, với khả năng thay đổi các tham số riêng lẻ tại dòng lệnh khi gọi ứng dụng.

Bây giờ tôi muốn sử dụng các tệp cấu hình 'thích hợp', ví dụ như từ lớp ConfigParser. Đồng thời, người dùng vẫn có thể thay đổi các tham số riêng lẻ tại dòng lệnh.

Tôi đã tự hỏi liệu có cách nào để kết hợp hai bước, ví dụ: sử dụng optparse (hoặc argparse mới hơn) để xử lý các tùy chọn dòng lệnh, nhưng đọc các giá trị mặc định từ tệp cấu hình trong cú pháp ConfigParse.

Bất kỳ ý tưởng làm thế nào để làm điều này một cách dễ dàng? Tôi không thực sự ưa thích việc gọi ConfigParse theo cách thủ công, và sau đó đặt thủ công tất cả các giá trị mặc định của tất cả các tùy chọn thành các giá trị thích hợp ...


6
Cập nhật : gói ConfigArgParsemột giải pháp thay thế cho argparse cho phép các tùy chọn cũng được thiết lập thông qua các tệp cấu hình và / hoặc các biến môi trường. Xem câu trả lời dưới đây bởi @ user553965
nealmcb

Câu trả lời:


88

Tôi vừa phát hiện ra bạn có thể làm điều này với argparse.ArgumentParser.parse_known_args(). Bắt đầu bằng cách sử dụng parse_known_args()để phân tích cú pháp tệp cấu hình từ dòng lệnh, sau đó đọc tệp đó bằng ConfigParser và đặt giá trị mặc định, rồi phân tích cú pháp phần còn lại của các tùy chọn với parse_args(). Điều này sẽ cho phép bạn có giá trị mặc định, ghi đè giá trị đó bằng tệp cấu hình và sau đó ghi đè giá trị đó bằng tùy chọn dòng lệnh. Ví dụ:

Mặc định không có đầu vào của người dùng:

$ ./argparse-partial.py
Option is "default"

Mặc định từ tệp cấu hình:

$ cat argparse-partial.config 
[Defaults]
option=Hello world!
$ ./argparse-partial.py -c argparse-partial.config 
Option is "Hello world!"

Mặc định từ tệp cấu hình, bị ghi đè bởi dòng lệnh:

$ ./argparse-partial.py -c argparse-partial.config --option override
Option is "override"

argprase-partial.py sau. Nó hơi phức tạp để xử lý -hđể được trợ giúp đúng cách.

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
Tôi đã được yêu cầu ở trên sử dụng lại mã trên và do đó tôi đặt nó vào miền pubic.
Von

22
'miền mu' khiến tôi bật cười. Tôi chỉ là một đứa trẻ ngốc nghếch.
SylvainD

1
argh! đây thực sự là một đoạn mã tuyệt vời, nhưng việc nội suy SafeConfigParser của các thuộc tính bị ghi đè bởi dòng lệnh không hoạt động . Ví dụ: nếu bạn thêm dòng sau vào argparse-part.config another=%(option)s you are cruelthì anothersẽ luôn giải quyết được Hello world you are cruelngay cả khi optionđược ghi đè thành một thứ khác trong dòng lệnh .. argghh-parser!
ihadanny

Lưu ý rằng set_defaults chỉ hoạt động nếu tên đối số không chứa dấu gạch ngang hoặc dấu gạch dưới. Vì vậy, người ta có thể chọn --myVar thay vì --my-var (thật không may, khá xấu). Để bật phân biệt chữ hoa chữ thường cho tệp cấu hình, hãy sử dụng config.optionxform = str trước khi phân tích cú pháp tệp, để myVar không bị chuyển đổi thành myvar.
Kevin Bader

1
Lưu ý rằng nếu bạn muốn thêm --versiontùy chọn vào ứng dụng của mình, tốt hơn nên thêm tùy chọn đó conf_parservào parservà thoát khỏi ứng dụng sau khi trợ giúp in. Nếu bạn thêm --versionvào parservà bạn khởi động ứng dụng với --versioncờ, ứng dụng của bạn không cần cố gắng mở và phân tích cú pháp args.conf_filetệp cấu hình (tệp này có thể bị sai hoặc thậm chí không tồn tại, dẫn đến ngoại lệ).
patryk.beza

20

Hãy xem ConfigArgParse - một gói PyPI mới của nó ( mã nguồn mở ) đóng vai trò là một phần mềm thay thế cho argparse với hỗ trợ bổ sung cho các tệp cấu hình và biến môi trường.


2
chỉ cần thử nó và thông minh hoạt động tuyệt vời :) Cảm ơn vì đã chỉ ra điều này.
red_tiger

1
Cảm ơn - có vẻ tốt! Trang web đó cũng so sánh ConfigArgParse với các tùy chọn khác, bao gồm argparse, ConfArgParse, appsettings, argparse_cnfig, yconf, hieropt và configurati
nealmcb

9

Tôi đang sử dụng ConfigParser và lập luận với các lệnh con để xử lý các tác vụ như vậy. Dòng quan trọng trong đoạn mã dưới đây là:

subp.set_defaults(**dict(conffile.items(subn)))

Điều này sẽ đặt giá trị mặc định của lệnh con (từ argparse) thành các giá trị trong phần của tệp cấu hình.

Dưới đây là một ví dụ đầy đủ hơn:

####### 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')

Vấn đề của tôi là bộ argparse đường dẫn tập tin cấu hình, và các bộ tập tin cấu hình mặc định argparse ... vấn đề con gà trứng ngu ngốc
olivervbk

4

Tôi không thể nói đó là cách tốt nhất, nhưng tôi có một lớp OptionParser mà tôi đã tạo ra để thực hiện điều đó - hoạt động giống như optparse.OptionParser với các mặc định đến từ phần tệp cấu hình. Bạn có thể có nó...

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))

Vui lòng duyệt qua nguồn . Các bài kiểm tra nằm trong một thư mục anh chị em.


3

Bạn có thể sử dụng 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.

Bạn có thể kết hợp các giá trị từ dòng lệnh, biến môi trường, tệp cấu hình và trong trường hợp nếu không có giá trị thì hãy xác định một giá trị mặc định.

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'

Lợi thế của Bản đồ chuỗi là gì so với việc một chuỗi dictsđược cập nhật theo thứ tự ưu tiên mong muốn? Có defaultdictthể có một lợi thế là các tùy chọn mới lạ hoặc không được hỗ trợ có thể được thiết lập nhưng điều đó tách biệt khỏi ChainMap. Tôi cho rằng tôi đang thiếu một cái gì đó.
Dan

2

Cập nhật: Câu trả lời này vẫn còn vấn đề; ví dụ, nó không thể xử lý các requiredđối số và yêu cầu một cú pháp cấu hình khó hiểu. Thay vào đó, ConfigArgParse dường như là chính xác những gì câu hỏi này yêu cầu và là một sự thay thế trong suốt, trong suốt.

Một vấn đề với hiện tại là nó sẽ không lỗi nếu các đối số trong tệp cấu hình không hợp lệ. Đây là một phiên bản có một nhược điểm khác: bạn sẽ cần bao gồm tiền tố --hoặc -trong các phím.

Đây là mã python ( liên kết Gist với giấy phép 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)

Đây là ví dụ về tệp cấu hình:

# Filename: config_correct.conf
[Defaults]
--arg1=Hello!
--arg2=3

Bây giờ, đang chạy

> python main.py --config_file config_correct.conf --arg1 override
Namespace(arg1='override', arg2=3, config_file='test_argparse.conf')

Tuy nhiên, nếu tệp cấu hình của chúng tôi có lỗi:

# config_invalid.conf
--arg1=Hello!
--arg2='not an integer!'

Chạy tập lệnh sẽ tạo ra lỗi, như mong muốn:

> 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!'

Nhược điểm chính là điều này sử dụng parser.parse_argshơi khó khăn để nhận được kiểm tra lỗi từ ArgumentParser, nhưng tôi không biết bất kỳ giải pháp thay thế nào cho điều này.


1

Cố gắng theo cách này

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

Sử dụng nó:

parser.add_argument("-C", "--config", action=LoadConfigAction)
parser.add_argument("-H", "--host", dest="host")

Và tạo cấu hình ví dụ:

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

1

fromfile_prefix_chars

Có thể không phải là API hoàn hảo, nhưng đáng để biết. 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())

Sau đó:

$ 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')

Tài liệu: https://docs.python.org/3.6/library/argparse.html#fromfile-prefix-chars

Đã thử nghiệm trên Python 3.6.5, Ubuntu 18.04.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.