it-swarm.com.de

Welches ist der beste Weg, um Konfigurationsoptionen auf der Kommandozeile in Python überschreiben zu lassen?

Ich habe eine Python-Anwendung, die einige (~ 30) Konfigurationsparameter benötigt. Bisher habe ich die OptionParser-Klasse verwendet, um Standardwerte in der App selbst zu definieren, mit der Möglichkeit, beim Aufruf der Anwendung einzelne Parameter in der Befehlszeile zu ändern.

Jetzt möchte ich 'richtige' Konfigurationsdateien verwenden, zum Beispiel von der ConfigParser-Klasse. Gleichzeitig sollten Benutzer weiterhin in der Lage sein, einzelne Parameter in der Befehlszeile zu ändern.

Ich habe mich gefragt, ob es eine Möglichkeit gibt, die beiden Schritte zu kombinieren, z. Verwenden Sie optparse (oder das neuere Argument argparse), um Befehlszeilenoptionen zu verarbeiten, lesen Sie jedoch die Standardwerte aus einer Konfigurationsdatei in der ConfigParse-Syntax.

Irgendwelche Ideen, wie man das auf einfache Weise macht? Ich habe nicht wirklich Lust, ConfigParse manuell aufzurufen und dann alle Standardeinstellungen aller Optinos manuell auf die entsprechenden Werte einzustellen ...

58
andreas-h

Ich habe gerade entdeckt, dass Sie dies mit argparse.ArgumentParser.parse_known_args() machen können. Beginnen Sie, indem Sie parse_known_args() verwenden, um eine Konfigurationsdatei von der Befehlszeile aus zu analysieren, dann lesen Sie sie mit ConfigParser aus, legen Sie die Standardwerte fest, und parsen Sie die übrigen Optionen mit parse_args(). Dadurch können Sie einen Standardwert festlegen, diesen mit einer Konfigurationsdatei überschreiben und diesen dann mit einer Befehlszeilenoption überschreiben. Z.B.:

Standard ohne Benutzereingabe:

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

Standardwert aus der Konfigurationsdatei:

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

Standardwert aus der Konfigurationsdatei, wird von der Befehlszeile überschrieben:

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

argprase-partial.py folgt. Es ist etwas kompliziert, -h um Hilfe zu bitten. 

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 __== "__main__":
    sys.exit(main())
62
Von

Check out ConfigArgParse - Dies ist ein neues PyPI-Paket ( open source ), das als Ersatz für argparse dient und zusätzliche Unterstützung für Konfigurationsdateien und Umgebungsvariablen bietet.

13
user553965

Ich verwende ConfigParser und argparse mit Unterbefehlen, um solche Aufgaben auszuführen. Die wichtige Zeile in dem folgenden Code lautet:

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

Dadurch werden die Standardwerte des Unterbefehls (von argparse) auf die Werte im Abschnitt der Konfigurationsdatei gesetzt. 

Ein vollständigeres Beispiel ist unten:

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

Ich kann nicht sagen, dass dies der beste Weg ist, aber ich habe eine OptionParser-Klasse erstellt, die genau das tut - verhält sich wie optparse.OptionParser, wobei die Standardwerte aus einem Konfigurationsdatei-Abschnitt stammen. Du kannst es haben...

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

Fühlen Sie sich frei, um die Quelle zu durchsuchen . Tests befinden sich in einem gleichnamigen Verzeichnis.

4
Blair Conrad

Update: Diese Antwort hat noch Probleme. Es kann beispielsweise nicht mit required-Argumenten umgehen und erfordert eine umständliche Konfigurationssyntax. Stattdessen scheint ConfigArgParse genau das zu sein, was diese Frage verlangt, und ist ein transparenter Drop-In-Ersatz.

Ein Problem mit current ist, dass es keinen Fehler gibt, wenn die Argumente in der Konfigurationsdatei ungültig sind. Hier ist eine Version mit einem anderen Nachteil: Sie müssen das Präfix -- oder - in die Schlüssel einfügen.

Hier ist der Python-Code ( Gist Link mit MIT Lizenz):

# Filename: main.py
import argparse

import configparser

if __== "__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)

Hier ist ein Beispiel für eine Konfigurationsdatei:

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

Jetzt rennen

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

Wenn jedoch unsere Konfigurationsdatei einen Fehler aufweist:

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

Beim Ausführen des Skripts wird wie gewünscht ein Fehler ausgegeben:

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

Der Hauptnachteil ist, dass parser.parse_args etwas hässlich verwendet wird, um die Fehlerüberprüfung von ArgumentParser zu erhalten, aber ich kenne keine Alternativen dazu.

1
Achal Dave

Versuchen Sie es auf diese Weise

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

Benutze es:

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

Und erstellen Sie eine Beispielskonfiguration:

# Example config: /etc/myservice.conf
import os
Host = os.getenv("Host_NAME", "localhost")
1
mosquito

Sie können ChainMap verwenden

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.

Sie können Werte aus Befehlszeile, Umgebungsvariablen, Konfigurationsdatei kombinieren und für den Fall, dass der Wert nicht vorhanden ist, einen Standardwert definieren.

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'
0
Vlad Bezden

fromfile_prefix_chars

Vielleicht nicht die perfekte API, aber wissenswert. 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())

Dann:

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

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

Getestet auf Python 3.6.5, Ubuntu 18.04.