Wie lassen sich Konfigurationsoptionen in der Befehlszeile in Python am besten überschreiben?

87

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, einzelne Parameter in der Befehlszeile beim Aufrufen der Anwendung zu ändern.

Jetzt möchte ich 'richtige' Konfigurationsdateien verwenden, zum Beispiel aus 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. B. optparse (oder das neuere argparse), um Befehlszeilenoptionen zu verarbeiten, aber die Standardwerte aus einer Konfigurationsdatei in der ConfigParse-Syntax zu lesen.

Irgendwelche Ideen, wie man das auf einfache Weise macht? Ich habe keine Lust, ConfigParse manuell aufzurufen und dann alle Standardeinstellungen aller Optionen manuell auf die entsprechenden Werte zu setzen ...

andreas-h
quelle
6
Update : Das ConfigArgParse- Paket ist ein Drop-In-Ersatz für argparse, mit dem Optionen auch über Konfigurationsdateien und / oder Umgebungsvariablen festgelegt werden können. Siehe die Antwort unten von @ user553965
nealmcb

Antworten:

88

Ich habe gerade entdeckt, dass Sie dies tun können argparse.ArgumentParser.parse_known_args(). Beginnen Sie damit parse_known_args(), eine Konfigurationsdatei über die Befehlszeile zu analysieren, lesen Sie sie dann mit ConfigParser, legen Sie die Standardeinstellungen fest und analysieren Sie die restlichen Optionen mit parse_args(). Auf diese Weise 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"

Standard aus der Konfigurationsdatei:

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

Standard aus der Konfigurationsdatei, überschrieben von der Befehlszeile:

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

argprase-partial.py folgt. Es ist etwas kompliziert, um -hHilfe richtig zu handhaben .

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())
Von
quelle
20
Ich wurde oben gebeten, den obigen Code wiederzuverwenden, und platziere ihn hiermit in der öffentlichen Domäne.
Von
22
"Schamdomäne" brachte mich zum Lachen. Ich bin nur ein dummes Kind.
SylvainD
1
argh! Das ist wirklich cooler Code, aber die SafepolonfigParser-Interpolation von Eigenschaften, die durch die Befehlszeile überschrieben werden, funktioniert nicht . Wenn Sie beispielsweise die folgende Zeile in dem argparse-partial.config another=%(option)s you are crueldann anotherwürde immer lösen , Hello world you are cruelauch wenn optionsonst in der Befehlszeile , um etwas außer Kraft gesetzt wird .. argghh-Parser!
Ihadanny
Beachten Sie, dass set_defaults nur funktioniert, wenn die Argumentnamen keine Bindestriche oder Unterstriche enthalten. Man kann sich also für --myVar anstelle von --my-var entscheiden (was leider ziemlich hässlich ist). Um die Groß- und Kleinschreibung für die Konfigurationsdatei zu aktivieren, verwenden Sie config.optionxform = str, bevor Sie die Datei analysieren, damit myVar nicht in myvar umgewandelt wird.
Kevin Bader
1
Beachten Sie, dass --versiones besser ist , eine Option zu Ihrer Anwendung hinzuzufügen, conf_parserals sie hinzuzufügen , parserund die Anwendung nach dem Drucken der Hilfe zu beenden. Wenn Sie hinzufügen --versionzu parserund starten Sie Anwendung mit --versionFlagge, als unnötig Ihre Anwendung versuchen zu öffnen und Parse - args.conf_fileKonfigurationsdatei (die malformed werden kann oder gar nicht vorhanden, was dazu führt, Ausnahme).
patryk.beza
20

Schauen Sie sich ConfigArgParse an - ein neues PyPI-Paket ( Open Source ), das als Ersatz für argparse dient und zusätzliche Unterstützung für Konfigurationsdateien und Umgebungsvariablen bietet.

user553965
quelle
2
habe es gerade ausprobiert und Witz funktioniert super :) Danke, dass du darauf hingewiesen hast.
red_tiger
1
Danke - sieht gut aus! Diese Webseite vergleicht ConfigArgParse auch mit anderen Optionen, einschließlich argparse, ConfArgParse, appsettings, argparse_cnfig, yconf, hieropt und configure
nealmcb
9

Ich verwende ConfigParser und argparse mit Unterbefehlen, um solche Aufgaben zu erledigen. Die wichtige Zeile im folgenden Code lautet:

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

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

Ein vollständigeres Beispiel finden Sie 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')
xubuntix
quelle
Mein Problem ist, dass argparse den Pfad der Konfigurationsdatei festlegt und die Konfigurationsdatei die Standardeinstellungen für argparse festlegt ... dummes Hühnerei-Problem
olivervbk
4

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 mit Standardeinstellungen aus einem Konfigurationsdateibereich. 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, die Quelle zu durchsuchen . Tests befinden sich in einem Geschwisterverzeichnis.

Blair Conrad
quelle
3

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 der Befehlszeile, Umgebungsvariablen und der Konfigurationsdatei kombinieren und, falls 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'
Vlad Bezden
quelle
Was ist der Vorteil einer ChainMap gegenüber einer Kette, dictsdie in der gewünschten Rangfolge aktualisiert wurde? Mit defaultdictgibt es möglicherweise einen Vorteil, da neuartige oder nicht unterstützte Optionen eingestellt werden können, aber das ist getrennt von ChainMap. Ich nehme an, ich vermisse etwas.
Dan
2

Update: Diese Antwort hat immer noch Probleme. Beispielsweise kann es keine requiredArgumente verarbeiten und erfordert eine umständliche Konfigurationssyntax. Stattdessen scheint ConfigArgParse genau das zu sein, was diese Frage verlangt, und ist ein transparenter Ersatz.

Ein Problem mit dem aktuellen 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 ( Hauptlink mit MIT-Lizenz):

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

Hier ist ein Beispiel für eine Konfigurationsdatei:

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

Jetzt läuft

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

Wenn unsere Konfigurationsdatei jedoch einen Fehler aufweist:

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

Das Ausführen des Skripts führt wie gewünscht zu einem Fehler:

> 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 dies parser.parse_argsetwas hackig verwendet wird, um die Fehlerprüfung von ArgumentParser zu erhalten, aber mir sind keine Alternativen dazu bekannt.

Achal Dave
quelle
1

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 Beispielkonfiguration:

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

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 unter Python 3.6.5, Ubuntu 18.04.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
quelle