Quelle est la meilleure façon d'autoriser le remplacement des options de configuration sur la ligne de commande en Python?


87

J'ai une application Python qui a besoin de quelques paramètres de configuration (~ 30). Jusqu'à présent, j'ai utilisé la classe OptionParser pour définir les valeurs par défaut dans l'application elle-même, avec la possibilité de modifier des paramètres individuels sur la ligne de commande lors de l'appel de l'application.

Maintenant, je voudrais utiliser des fichiers de configuration «appropriés», par exemple de la classe ConfigParser. Dans le même temps, les utilisateurs devraient toujours pouvoir modifier des paramètres individuels sur la ligne de commande.

Je me demandais s'il y avait un moyen de combiner les deux étapes, par exemple utiliser optparse (ou le plus récent argparse) pour gérer les options de ligne de commande, mais en lisant les valeurs par défaut à partir d'un fichier de configuration dans la syntaxe ConfigParse.

Des idées pour faire cela de manière simple? Je n'ai pas vraiment envie d'appeler manuellement ConfigParse, puis de définir manuellement toutes les valeurs par défaut de toutes les options sur les valeurs appropriées ...


6
Mise à jour : le package ConfigArgParse est un substitut à argparse qui permet également de définir des options via des fichiers de configuration et / ou des variables d'environnement. Voir la réponse ci-dessous par @ user553965
nealmcb

Réponses:


88

Je viens de découvrir que vous pouvez le faire avec argparse.ArgumentParser.parse_known_args(). Commencez par utiliser parse_known_args()pour analyser un fichier de configuration à partir de la ligne de commande, puis lisez-le avec ConfigParser et définissez les valeurs par défaut, puis analysez le reste des options avec parse_args(). Cela vous permettra d'avoir une valeur par défaut, de la remplacer avec un fichier de configuration, puis de la remplacer avec une option de ligne de commande. Par exemple:

Par défaut sans entrée utilisateur:

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

Valeur par défaut du fichier de configuration:

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

Valeur par défaut du fichier de configuration, remplacée par la ligne de commande:

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

argprase-partial.py suit. Il est un peu compliqué à gérer -hcorrectement pour obtenir de l'aide.

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
On m'a demandé ci-dessus de réutiliser le code ci-dessus et je le place par la présente dans le domaine public.
Von

22
«domaine public» m'a fait rire. Je ne suis qu'un gamin stupide.
SylvainD

1
argh! c'est du code vraiment cool, mais l'interpolation SafeConfigParser des propriétés remplacées par la ligne de commande ne fonctionne pas . Par exemple , si vous ajoutez la ligne suivante à argparse-partial.config another=%(option)s you are cruelalors anotherserait toujours à résoudre , Hello world you are cruelmême si optionest overriden à quelque chose d' autre dans la ligne de commande .. argghh-analyseur!
ihadanny

Notez que set_defaults ne fonctionne que si les noms d'argument ne contiennent pas de tirets ni de traits de soulignement. On peut donc opter pour --myVar au lieu de --my-var (ce qui est malheureusement assez moche). Pour activer le respect de la casse pour le fichier de configuration, utilisez config.optionxform = str avant d'analyser le fichier, afin que myVar ne soit pas transformé en myvar.
Kevin Bader

1
Notez que si vous souhaitez ajouter une --versionoption à votre application, il est préférable de l'ajouter conf_parserà parseret de quitter l'application après avoir imprimé l'aide. Si vous ajoutez --versionà parseret vous commencez l' application avec le --versiondrapeau, que votre application essayer inutilement d'ouvrir et d' analyser le args.conf_filefichier de configuration (qui peut être malformé ou même inexistante, ce qui conduit à exception).
patryk.beza

20

Découvrez ConfigArgParse - c'est un nouveau package PyPI ( open source ) qui remplace argparse avec un support supplémentaire pour les fichiers de configuration et les variables d'environnement.


2
juste essayé et l'esprit fonctionne très bien :) Merci de l'avoir signalé.
red_tiger

1
Merci - ça a l'air bien! Cette page Web compare également ConfigArgParse avec d'autres options, y compris argparse, ConfArgParse, appsettings, argparse_cnfig, yconf, hieropt et configurati
nealmcb

9

J'utilise ConfigParser et argparse avec des sous-commandes pour gérer ces tâches. La ligne importante dans le code ci-dessous est:

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

Cela définira les valeurs par défaut de la sous-commande (de argparse) sur les valeurs de la section du fichier de configuration.

Un exemple plus complet est ci-dessous:

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

mon problème est que argparse définit le chemin du fichier de configuration, et le fichier de configuration définit les valeurs par défaut d'argparse ... stupide problème d'oeuf de poule
olivervbk

4

Je ne peux pas dire que c'est le meilleur moyen, mais j'ai une classe OptionParser que j'ai créée qui fait exactement cela - agit comme optparse.OptionParser avec des valeurs par défaut provenant d'une section de fichier de configuration. Vous pouvez l'avoir...

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

N'hésitez pas à parcourir la source . Les tests sont dans un répertoire frère.


3

Vous pouvez utiliser 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.

Vous pouvez combiner les valeurs de la ligne de commande, des variables d'environnement, du fichier de configuration et, si la valeur n'est pas là, définir une valeur par défaut.

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'

Quel est l'avantage d'un ChainMap par rapport à une chaîne de dictsmise à jour dans l'ordre de priorité souhaité? Il defaultdicty a peut-être un avantage car des options nouvelles ou non prises en charge peuvent être définies, mais elles sont distinctes de ChainMap. Je suppose qu'il me manque quelque chose.
Dan

2

Mise à jour: cette réponse a toujours des problèmes; par exemple, il ne peut pas gérer les requiredarguments et nécessite une syntaxe de configuration peu pratique. Au lieu de cela, ConfigArgParse semble être exactement ce que cette question demande et constitue un remplacement transparent et instantané .

Un problème avec le courant est qu'il n'entraînera pas d'erreur si les arguments dans le fichier de configuration sont invalides. Voici une version avec un inconvénient différent: vous devrez inclure le préfixe --ou -dans les clés.

Voici le code python ( lien Gist avec licence 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)

Voici un exemple de fichier de configuration:

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

Maintenant, en cours d'exécution

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

Cependant, si notre fichier de configuration contient une erreur:

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

L'exécution du script produira une erreur, comme vous le souhaitez:

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

Le principal inconvénient est que cela utilise parser.parse_argsquelque peu hackly afin d'obtenir la vérification des erreurs d'ArgumentParser, mais je ne connais aucune alternative à cela.


1

Essayez de cette façon

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

Utilise le:

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

Et créez un exemple de configuration:

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

1

fromfile_prefix_chars

Peut-être pas l'API parfaite, mais cela vaut la peine d'être connu. 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())

Ensuite:

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

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

Testé sur Python 3.6.5, Ubuntu 18.04.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.