Manakah cara terbaik untuk mengizinkan opsi konfigurasi diganti pada baris perintah dengan Python?


88

Saya memiliki aplikasi Python yang membutuhkan beberapa (~ 30) parameter konfigurasi. Hingga saat ini, saya menggunakan kelas OptionParser untuk menentukan nilai default dalam aplikasi itu sendiri, dengan kemungkinan untuk mengubah parameter individu pada baris perintah saat menjalankan aplikasi.

Sekarang saya ingin menggunakan file konfigurasi yang 'tepat', misalnya dari kelas ConfigParser. Pada saat yang sama, pengguna tetap dapat mengubah parameter individu di baris perintah.

Saya bertanya-tanya apakah ada cara untuk menggabungkan dua langkah, misalnya menggunakan optparse (atau argparse yang lebih baru) untuk menangani opsi baris perintah, tetapi membaca nilai default dari file konfigurasi dalam sintaks ConfigParse.

Ada ide bagaimana melakukan ini dengan cara yang mudah? Saya tidak terlalu suka menggunakan ConfigParse secara manual, dan kemudian secara manual mengatur semua default dari semua opsi ke nilai yang sesuai ...


6
Pembaruan : paket ConfigArgParse adalah pengganti drop-in untuk argparse yang memungkinkan opsi juga disetel melalui file config dan / atau variabel lingkungan. Lihat jawaban di bawah oleh @ user553965
nealmcb

Jawaban:


89

Saya baru tahu Anda bisa melakukan ini dengan argparse.ArgumentParser.parse_known_args(). Mulailah dengan menggunakan parse_known_args()untuk mengurai file konfigurasi dari baris perintah, lalu membacanya dengan ConfigParser dan menyetel defaultnya, lalu mengurai sisa opsi dengan parse_args(). Ini akan memungkinkan Anda untuk memiliki nilai default, menimpanya dengan file konfigurasi dan kemudian menimpanya dengan opsi baris perintah. Misalnya:

Default tanpa masukan pengguna:

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

Default dari file konfigurasi:

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

Default dari file konfigurasi, diganti dengan baris perintah:

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

argprase-paration.py berikut. Agak rumit untuk menangani -hbantuan dengan benar.

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
Saya telah ditanya di atas menggunakan kembali kode di atas dan saya dengan ini menempatkannya ke dalam domain publik.
Von

22
'domain kemaluan' membuatku tertawa. Aku hanya anak bodoh
SylvainD

1
argh! ini adalah kode yang sangat keren, tetapi interpolasi SafeConfigParser dari properti yang diganti dengan baris perintah tidak berfungsi . Misalnya jika Anda menambahkan baris berikut ke argparse-partial.config, another=%(option)s you are cruelmaka anotherakan selalu diselesaikan Hello world you are cruelbahkan jika diganti ke baris optionlain di baris perintah .. argghh-parser!
ihadanny

Perhatikan bahwa set_defaults hanya berfungsi jika nama argumen tidak berisi tanda hubung atau garis bawah. Jadi seseorang dapat memilih --myVar daripada --my-var (yang sayangnya, sangat jelek). Untuk mengaktifkan sensitivitas huruf besar-kecil pada file konfigurasi, gunakan config.optionxform = str sebelum mengurai file, sehingga myVar tidak diubah menjadi myvar.
Kevin Bader

1
Perhatikan bahwa jika Anda ingin menambahkan --versionopsi ke aplikasi Anda, lebih baik menambahkannya conf_parserdaripada ke parserdan keluar dari aplikasi setelah bantuan pencetakan. Jika Anda menambahkan --versionuntukparser dan Anda memulai aplikasi dengan --versionbendera, dari aplikasi Anda sia-sia mencoba untuk membuka dan parse args.conf_filefile konfigurasi (yang dapat mengalami kelainan bentuk atau bahkan tidak ada, yang mengarah ke pengecualian).
patryk.beza

21

Lihat ConfigArgParse - ini adalah paket PyPI baru ( open source ) yang berfungsi sebagai pengganti argparse dengan dukungan tambahan untuk file konfigurasi dan variabel lingkungan.


3
baru saja mencobanya dan hasilnya bagus :) Terima kasih telah menunjukkan hal ini.
red_tiger

2
Terima kasih - terlihat bagus! Halaman web tersebut juga membandingkan ConfigArgParse dengan opsi lain, termasuk argparse, ConfArgParse, appsettings, argparse_cnfig, yconf, hieropt, dan configurati
nealmcb

9

Saya menggunakan ConfigParser dan argparse dengan sub-perintah untuk menangani tugas-tugas semacam itu. Baris penting dalam kode di bawah ini adalah:

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

Ini akan menyetel default dari subcommand (dari argparse) ke nilai di bagian file konfigurasi.

Contoh yang lebih lengkap ada di bawah ini:

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

masalah saya adalah bahwa argparse menetapkan jalur file konfigurasi, dan file konfigurasi menetapkan default argparse ... masalah telur ayam bodoh
olivervbk

4

Saya tidak bisa mengatakan itu cara terbaik, tetapi saya memiliki kelas OptionParser yang saya buat yang melakukan hal itu - bertindak seperti optparse.OptionParser dengan default yang berasal dari bagian file konfigurasi. Kamu dapat memilikinya...

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

Jangan ragu untuk menelusuri sumbernya . Pengujian berada dalam direktori saudara.


3

Anda dapat menggunakan 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.

Anda dapat menggabungkan nilai dari baris perintah, variabel lingkungan, file konfigurasi, dan jika nilainya tidak ada, tentukan nilai default.

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'

Apa keuntungan dari ChainMap dibandingkan dengan rantai yang dictsdiperbarui dalam urutan prioritas yang diinginkan? Dengan defaultdictkemungkinan keuntungan karena opsi baru atau tidak didukung dapat diatur tetapi itu terpisah dari ChainMap. Saya menganggap saya melewatkan sesuatu.
Dan

2

Pembaruan: Jawaban ini masih bermasalah; misalnya, tidak dapat menangani requiredargumen, dan membutuhkan sintaks konfigurasi yang canggung. Sebaliknya, ConfigArgParse tampaknya persis seperti yang diminta pertanyaan ini, dan merupakan pengganti drop-in yang transparan.

Satu masalah dengan arus ini adalah tidak akan terjadi kesalahan jika argumen dalam file konfigurasi tidak valid. Ini adalah versi dengan sisi negatif yang berbeda: Anda harus menyertakan awalan --atau -di tombol.

Berikut kode python ( tautan inti dengan lisensi 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)

Berikut contoh file konfigurasi:

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

Sekarang, lari

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

Namun, jika file konfigurasi kami mengalami kesalahan:

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

Menjalankan skrip akan menghasilkan kesalahan, seperti yang diinginkan:

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

Kelemahan utama adalah bahwa ini menggunakan parser.parse_argsagak sembarangan untuk mendapatkan pemeriksaan kesalahan dari ArgumentParser, tetapi saya tidak mengetahui alternatif apa pun untuk ini.


2

fromfile_prefix_chars

Mungkin bukan API yang sempurna, tetapi perlu diketahui.

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

Kemudian:

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

Dokumentasi: https://docs.python.org/3.6/library/argparse.html#fromfile-prefix-chars

Diuji pada Python 3.6.5, Ubuntu 18.04.


1

Cobalah cara ini

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

Gunakan:

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

Dan buat contoh config:

# Example config: /etc/myservice.conf
import os
host = os.getenv("HOST_NAME", "localhost")
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.