Qual è il modo migliore per consentire l'override delle opzioni di configurazione dalla riga di comando in Python?


89

Ho un'applicazione Python che necessita di parecchi (~ 30) parametri di configurazione. Finora ho utilizzato la classe OptionParser per definire i valori predefiniti nell'app stessa, con la possibilità di modificare i singoli parametri dalla riga di comando quando si richiama l'applicazione.

Ora vorrei utilizzare i file di configurazione "corretti", ad esempio dalla classe ConfigParser. Allo stesso tempo, gli utenti dovrebbero comunque essere in grado di modificare i singoli parametri dalla riga di comando.

Mi chiedevo se esiste un modo per combinare i due passaggi, ad esempio utilizzare optparse (o il più recente argparse) per gestire le opzioni della riga di comando, ma leggendo i valori predefiniti da un file di configurazione nella sintassi ConfigParse.

Qualche idea su come farlo in modo semplice? Non mi piace invocare manualmente ConfigParse e quindi impostare manualmente tutti i valori predefiniti di tutte le opzioni sui valori appropriati ...


6
Aggiornamento : il pacchetto ConfigArgParse è un sostituto immediato di argparse che consente di impostare le opzioni anche tramite file di configurazione e / o variabili di ambiente. Vedi la risposta sotto di @ user553965
nealmcb

Risposte:


89

Ho appena scoperto che puoi farlo con argparse.ArgumentParser.parse_known_args(). Inizia usando parse_known_args()per analizzare un file di configurazione dalla riga di comando, quindi leggilo con ConfigParser e imposta i valori predefiniti, quindi analizza il resto delle opzioni conparse_args() . Ciò ti consentirà di avere un valore predefinito, sovrascriverlo con un file di configurazione e quindi sovrascriverlo con un'opzione della riga di comando. Per esempio:

Predefinito senza input dell'utente:

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

Default dal file di configurazione:

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

Predefinito dal file di configurazione, sovrascritto dalla riga di comando:

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

argprase-partial.py segue. È leggermente complicato da gestire -hadeguatamente per ricevere aiuto.

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
Mi è stato chiesto sopra di riutilizzare il codice sopra e con la presente lo inserisco nel dominio pubblico.
Von

22
"dominio pubico" mi ha fatto ridere. Sono solo uno stupido ragazzo.
SylvainD

1
argh! questo è un codice davvero interessante, ma l'interpolazione di SafeConfigParser delle proprietà sovrascritte dalla riga di comando non funziona . Ad esempio, se si aggiunge la seguente riga alla argparse-partial.config another=%(option)s you are cruelpoi anothersarebbe sempre risolvere a Hello world you are cruelanche se optionè sovrascritto a qualcos'altro nella linea di comando .. argghh-parser!
ihadanny

Nota che set_defaults funziona solo se i nomi degli argomenti non contengono trattini o trattini bassi. Quindi si può optare per --myVar invece di --my-var (che è, sfortunatamente, piuttosto brutto). Per abilitare la distinzione tra maiuscole e minuscole per il file di configurazione, utilizzare config.optionxform = str prima di analizzare il file, in modo che myVar non venga trasformato in myvar.
Kevin Bader

1
Nota che se vuoi aggiungere --versionun'opzione alla tua applicazione, è meglio aggiungerla conf_parsera parsere uscire dall'applicazione dopo aver stampato la guida. Se si aggiunge --versiona parsere si avvia l'applicazione con --versionflag, l'applicazione tenta inutilmente di aprire e analizzare il args.conf_filefile di configurazione (che può essere malformato o addirittura inesistente, il che porta a un'eccezione).
patryk.beza

21

Dai un'occhiata a ConfigArgParse : è un nuovo pacchetto PyPI ( open source ) che funge da sostituto per argparse con supporto aggiunto per file di configurazione e variabili di ambiente.


3
l'ho appena provato e l'arguzia funziona alla grande :) Grazie per averlo sottolineato.
red_tiger

2
Grazie - sembra buono! Quella pagina web confronta anche ConfigArgParse con altre opzioni, tra cui argparse, ConfArgParse, appsettings, argparse_cnfig, yconf, hieropt e configurati
nealmcb

9

Sto usando ConfigParser e argparse con sottocomandi per gestire tali attività. La riga importante nel codice seguente è:

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

Questo imposterà i valori predefiniti del sottocomando (da argparse) ai valori nella sezione del file di configurazione.

Di seguito è riportato un esempio più completo:

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

il mio problema è che argparse imposta il percorso del file di configurazione e il file di configurazione imposta i valori predefiniti di argparse ... stupido problema dell'uovo di gallina
olivervbk

4

Non posso dire che sia il modo migliore, ma ho una classe OptionParser che ho creato che fa proprio questo: si comporta come optparse.OptionParser con i valori predefiniti provenienti da una sezione del file di configurazione. Puoi averlo...

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

Sentiti libero di sfogliare la fonte . I test sono in una directory di pari livello.


3

Puoi usare 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.

È possibile combinare valori dalla riga di comando, variabili di ambiente, file di configurazione e, nel caso in cui il valore non sia presente, definire un valore predefinito.

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'

Qual è il vantaggio di una ChainMap rispetto ad una catena di dictsaggiornamenti nell'ordine di precedenza desiderato? Con defaultdictforse c'è un vantaggio in quanto è possibile impostare opzioni nuove o non supportate, ma questo è separato da ChainMap. Presumo che mi manchi qualcosa.
Dan

2

Aggiornamento: questa risposta ha ancora problemi; ad esempio, non può gestire requiredargomenti e richiede una sintassi di configurazione scomoda. Invece, ConfigArgParse sembra essere esattamente ciò che questa domanda richiede ed è una sostituzione trasparente e .

Un problema con la corrente è che non si verificherà un errore se gli argomenti nel file di configurazione non sono validi. Ecco una versione con un aspetto negativo diverso: dovrai includere il prefisso --o -nelle chiavi.

Ecco il codice python ( collegamento Gist con licenza 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)

Ecco un esempio di un file di configurazione:

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

Adesso corro

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

Tuttavia, se il nostro file di configurazione ha un errore:

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

L'esecuzione dello script produrrà un errore, come desiderato:

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

Lo svantaggio principale è che questo utilizza in parser.parse_argsmodo un po 'hackly per ottenere il controllo degli errori da ArgumentParser, ma non sono a conoscenza di alcuna alternativa a questo.


2

fromfile_prefix_chars

Forse non l'API perfetta, ma vale la pena conoscerla.

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

Poi:

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

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

Testato su Python 3.6.5, Ubuntu 18.04.


1

Prova in questo modo

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

Usalo:

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

E crea una configurazione di esempio:

# Example config: /etc/myservice.conf
import os
host = os.getenv("HOST_NAME", "localhost")
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.