Python'da yapılandırma seçeneklerinin komut satırında geçersiz kılınmasına izin vermenin en iyi yolu hangisidir?


89

Epeyce (~ 30) yapılandırma parametresi gerektiren bir Python uygulamam var. Şimdiye kadar, uygulamanın kendisinde varsayılan değerleri tanımlamak için OptionParser sınıfını kullandım, uygulamayı çalıştırırken komut satırında tek tek parametreleri değiştirme olanağı sağladım.

Şimdi, örneğin ConfigParser sınıfından 'uygun' yapılandırma dosyalarını kullanmak istiyorum. Aynı zamanda, kullanıcılar komut satırından bireysel parametreleri değiştirebilmelidir.

İki adımı birleştirmenin herhangi bir yolu olup olmadığını merak ediyordum, örneğin komut satırı seçeneklerini işlemek için optparse (veya daha yeni argparse) kullanmak, ancak ConfigParse sözdizimindeki bir yapılandırma dosyasından varsayılan değerleri okumak.

Bunun nasıl kolay bir şekilde yapılacağına dair bir fikriniz var mı? ConfigParse'ı manuel olarak çağırmaktan ve sonra tüm seçeneklerin tüm varsayılanlarını uygun değerlere manuel olarak ayarlamaktan gerçekten hoşlanmıyorum ...


6
Güncelleme : ConfigArgParse paketi, seçeneklerin yapılandırma dosyaları ve / veya ortam değişkenleri aracılığıyla da ayarlanmasına izin veren, argparse yerine açılan bir pakettir. @ User553965 tarafından verilen cevaba bakın
nealmcb

Yanıtlar:


89

Bunu onunla yapabileceğini keşfettim argparse.ArgumentParser.parse_known_args(). parse_known_args()Komut satırından bir yapılandırma dosyasını ayrıştırmak için kullanarak başlayın , ardından ConfigParser ile okuyun ve varsayılanları ayarlayın ve ardından seçeneklerin geri kalanınıparse_args() . Bu, varsayılan bir değere sahip olmanıza, bunu bir yapılandırma dosyasıyla geçersiz kılmanıza ve ardından bunu bir komut satırı seçeneğiyle geçersiz kılmanıza olanak tanır. Örneğin:

Kullanıcı girişi olmadan varsayılan:

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

Yapılandırma dosyasından varsayılan:

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

Yapılandırma dosyasından varsayılan, komut satırı tarafından geçersiz kılındı:

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

argprase-partial.py izler. -hDoğru bir şekilde yardım almak biraz karmaşıktır .

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
Yukarıda yukarıdaki kodu yeniden kullanmam istendi ve bu vesileyle onu kasık bölgesine yerleştiriyorum.
Von

22
"kasık alanı" beni güldürdü. Ben sadece aptal bir çocuğum
SylvainD

1
argh! bu gerçekten harika bir koddur, ancak SafeConfigParser komut satırı tarafından geçersiz kılınan özelliklerin enterpolasyonu çalışmaz . Örneğin Eğer-partial.config argparse aşağıdaki satırı eklerseniz another=%(option)s you are crueldaha sonra anotherhep için çözümlenebileceğini Hello world you are cruelbile optionkomut satırında .. argghh-ayrıştırıcı başka bir şeye değiştirileceğini!
ihadanny

Set_defaults'un yalnızca bağımsız değişken adları tire veya alt çizgi içermediğinde çalıştığını unutmayın. Yani --my-var yerine --myVar tercih edilebilir (ki bu ne yazık ki oldukça çirkin). Yapılandırma dosyası için büyük / küçük harf duyarlılığını etkinleştirmek için, dosyayı ayrıştırmadan önce config.optionxform = str kullanın, böylece myVar, myvar'a dönüştürülmez.
Kevin Bader

1
Not Eklemek istediğiniz eğer --versionuygulamanıza seçeneği, bu eklemek daha iyidir conf_parserdaha parseryardım baskı sonrası uygulama ve çıkış. Eğer eklerseniz --versioniçin parserve uygulamayı başlatmak --versioniçin uygulama gereksiz yere açın ve ayrıştırma çalışmak yerine, bayrak args.conf_file(potansiyel müşteriler istisna şekil bozukluğuna, hatta varolmayan edilebilir) yapılandırma dosyası.
patryk.beza

21

ConfigArgParse'a göz atın - yeni bir PyPI paketi ( açık kaynak ), argparse yerine konfigürasyon dosyaları ve ortam değişkenleri için ek destekle birlikte bir düşüş görevi görür.


3
sadece denedim ve zekâ harika çalışıyor :) Bunu belirttiğiniz için teşekkürler.
red_tiger

2
Teşekkürler - iyi görünüyor! Bu web sayfası aynı zamanda ConfigArgParse'ı argparse, ConfArgParse, appsettings, argparse_cnfig, yconf, hieropt ve configureati gibi diğer seçeneklerle de karşılaştırır
nealmcb

9

Bu tür görevleri yerine getirmek için ConfigParser ve alt komutlarla argparse kullanıyorum. Aşağıdaki koddaki önemli satır:

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

Bu, alt komutun (argparse'den) varsayılanlarını yapılandırma dosyasının bölümündeki değerlere ayarlayacaktır.

Daha eksiksiz bir örnek aşağıdadır:

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

benim sorunum argparse yapılandırma dosyası yolunu ayarlaması ve yapılandırma dosyasının argparse varsayılanlarını ayarlaması ... aptal tavuk yumurtası sorunu
olivervbk

4

Bunun en iyi yol olduğunu söyleyemem, ancak bunu yapan bir OptionParser sınıfım var - varsayılanları bir yapılandırma dosyası bölümünden gelen optparse.OptionParser gibi davranıyor. Alabilirsin...

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

Kaynağa göz atmaktan çekinmeyin . Testler kardeş dizinindedir.


3

ChainMap'i kullanabilirsiniz

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.

Komut satırından, ortam değişkenlerinden, yapılandırma dosyasından değerleri birleştirebilir ve değer yoksa varsayılan bir değer tanımlayabilirsiniz.

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'

Bir ChainMap'in dictsistenen öncelik sırasına göre bir güncelleme zincirine göre avantajı nedir ? Bununla birlikte defaultdict, yeni veya desteklenmeyen seçenekler ayarlanabileceğinden muhtemelen bir avantaj vardır, ancak bu ondan ayrıdır ChainMap. Bir şeyi kaçırdığımı varsayıyorum.
Dan

2

Güncelleme: Bu yanıtın hala sorunları var; örneğin, requiredargümanları işleyemez ve garip bir yapılandırma sözdizimi gerektirir. Bunun yerine, ConfigArgParse bu sorunun tam olarak istediği gibi görünüyor ve şeffaf, açılan bir yedek.

Bir konu akımı yapılandırma dosyasında tezlerinin geçersiz olduğunu eğer hata olmaz olmasıdır. İşte farklı bir dezavantajı olan bir sürüm: tuşlara --veya -önekini eklemeniz gerekecek .

İşte python kodu ( MIT lisansıyla ana bağlantı ):

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

Aşağıda bir yapılandırma dosyası örneği verilmiştir:

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

Şimdi koşuyor

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

Ancak, yapılandırma dosyamızda bir hata varsa:

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

Komut dosyasını çalıştırmak, istenildiği gibi bir hata üretecektir:

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

Başlıca dezavantajı, bunun parser.parse_argsArgumentParser'dan hata kontrolünü elde etmek için biraz hilekarca kullanılmasıdır, ancak bunun herhangi bir alternatifinin farkında değilim.


2

fromfile_prefix_chars

Belki mükemmel API değil, ancak bilmeye değer.

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

Sonra:

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

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

Python 3.6.5, Ubuntu 18.04'te test edilmiştir.


1

Bu şekilde dene

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

Kullanın:

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

Ve örnek yapılandırma oluşturun:

# Example config: /etc/myservice.conf
import os
host = os.getenv("HOST_NAME", "localhost")
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.