วิธีใดเป็นวิธีที่ดีที่สุดในการอนุญาตให้แทนที่ตัวเลือกการกำหนดค่าที่บรรทัดคำสั่งใน Python


89

ฉันมีแอปพลิเคชั่น Python ซึ่งต้องการพารามิเตอร์การกำหนดค่า (~ 30) ค่อนข้างน้อย จนถึงตอนนี้ฉันใช้คลาส OptionParser เพื่อกำหนดค่าเริ่มต้นในแอปเองโดยมีความเป็นไปได้ที่จะเปลี่ยนพารามิเตอร์แต่ละตัวที่บรรทัดคำสั่งเมื่อเรียกใช้แอปพลิเคชัน

ตอนนี้ฉันต้องการใช้ไฟล์คอนฟิกูเรชัน 'ที่เหมาะสม' ตัวอย่างเช่นจากคลาส ConfigParser ในขณะเดียวกันผู้ใช้ควรยังคงสามารถเปลี่ยนพารามิเตอร์แต่ละตัวได้ที่บรรทัดคำสั่ง

ฉันสงสัยว่ามีวิธีใดบ้างในการรวมสองขั้นตอนเช่นใช้ optparse (หรือ argparse ที่ใหม่กว่า) เพื่อจัดการตัวเลือกบรรทัดคำสั่ง แต่การอ่านค่าเริ่มต้นจากไฟล์ config ในไวยากรณ์ ConfigParse

มีความคิดอย่างไรในการทำสิ่งนี้ด้วยวิธีง่ายๆ? ฉันไม่คิดจะเรียกใช้ ConfigParse ด้วยตนเองจากนั้นตั้งค่าเริ่มต้นทั้งหมดของตัวเลือกทั้งหมดเป็นค่าที่เหมาะสม ...


6
อัปเดต : แพ็คเกจConfigArgParseเป็นการแทนที่แบบดรอปอินสำหรับ argparse ที่อนุญาตให้ตั้งค่าตัวเลือกผ่านไฟล์ config และ / หรือตัวแปรสภาพแวดล้อม ดูคำตอบด้านล่างโดย @ user553965
nealmcb

คำตอบ:


89

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

ดีฟอลต์จากไฟล์คอนฟิกูเรชันถูกแทนที่โดย commandline:

$ ./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
ฉันถูกถามข้างต้นว่าให้ใช้รหัสข้างบนซ้ำและฉันขอวางไว้ในโดเมน pubic
ฟอน

22
"โดเมนสาธารณะ" ทำให้ฉันหัวเราะ ฉันเป็นแค่เด็กโง่
SylvainD

1
อ๊าก! นี่คือจริงๆเย็นรหัส แต่ SafeConfigParser interpolating ของคุณสมบัติแทนที่โดยบรรทัดคำสั่งไม่ทำงาน เช่นถ้าคุณเพิ่มบรรทัดต่อไปนี้เป็น argparse-partial.config another=%(option)s you are cruelก็anotherจะแก้ไขได้เสมอHello world you are cruelแม้ว่าoptionจะถูก overriden เป็นอย่างอื่นในบรรทัดคำสั่ง .. argghh-parser!
ihadanny

โปรดทราบว่า set_defaults จะใช้ได้ก็ต่อเมื่อชื่ออาร์กิวเมนต์ไม่มีขีดกลางหรือขีดล่าง ดังนั้นเราสามารถเลือกใช้ --myVar แทน --my-var (ซึ่งน่าเสียดายที่ค่อนข้างน่าเกลียด) ในการเปิดใช้งาน case-sensitive สำหรับไฟล์ config ให้ใช้ config.optionxform = str ก่อนที่จะแยกวิเคราะห์ไฟล์ดังนั้น myVar จะไม่ถูกแปลงเป็น myvar
Kevin Bader

1
โปรดทราบว่าหากคุณต้องการเพิ่ม--versionตัวเลือกในการใช้งานของคุณจะดีกว่าที่จะเพิ่มไปยังconf_parserกว่าparserและออกจากโปรแกรมหลังจากการพิมพ์ความช่วยเหลือ ถ้าคุณเพิ่ม--versionไปparserและคุณเริ่มโปรแกรมประยุกต์กับ--versionธงกว่าใบสมัครของคุณไม่จำเป็นต้องพยายามที่จะเปิดและแยกargs.conf_fileไฟล์การกำหนดค่า (ซึ่งอาจจะเป็นในรูปแบบหรือแม้กระทั่งที่ไม่มีอยู่จริงซึ่งนำไปสู่ข้อยกเว้น)
patryk.beza

21

ตรวจสอบConfigArgParseซึ่งเป็นแพ็คเกจ PyPI ใหม่ ( โอเพนซอร์ส ) ที่ทำหน้าที่แทนการลดลงของ argparse พร้อมการสนับสนุนเพิ่มเติมสำหรับไฟล์กำหนดค่าและตัวแปรสภาพแวดล้อม


3
เพิ่งลองใช้และใช้งานได้ดี :) ขอบคุณที่ชี้ให้เห็น
red_tiger

2
ขอบคุณ - ดูดี! หน้าเว็บนั้นยังเปรียบเทียบ ConfigArgParse กับตัวเลือกอื่น ๆ เช่น argparse, ConfArgParse, appsettings, argparse_cnfig, yconf, hieropt และ configurati ด้วย
nealmcb

9

ฉันใช้ ConfigParser และ argparse พร้อมคำสั่งย่อยเพื่อจัดการงานดังกล่าว บรรทัดสำคัญในโค้ดด้านล่างคือ:

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

ซึ่งจะตั้งค่าเริ่มต้นของคำสั่งย่อย (จาก argparse) เป็นค่าในส่วนของไฟล์ config

ตัวอย่างที่สมบูรณ์เพิ่มเติมอยู่ด้านล่าง:

####### 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 ตั้งค่าเส้นทางไฟล์ config และไฟล์ config ตั้งค่าเริ่มต้น argparse ... ปัญหาไก่ไข่โง่
olivervbk

4

ฉันไม่สามารถพูดได้ว่าเป็นวิธีที่ดีที่สุด แต่ฉันมีคลาส OptionParser ที่ฉันทำแบบนั้น - ทำหน้าที่เหมือน optparseOptionParser ที่มีค่าเริ่มต้นมาจากส่วนไฟล์ config คุณสามารถมีได้ ...

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 แต่ฉันไม่ทราบทางเลือกอื่นใด


2

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


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

และสร้างตัวอย่าง config:

# Example config: /etc/myservice.conf
import os
host = os.getenv("HOST_NAME", "localhost")
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.