web-dev-qa-db-fra.com

Quel est le meilleur moyen d'autoriser le remplacement des options de configuration sur la ligne de commande en Python?

J'ai une application Python qui nécessite quelques paramètres de configuration (~ 30). Jusqu'à présent, j'avais 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, j'aimerais utiliser les fichiers de configuration "appropriés", par exemple ceux 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. utilisez 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 comment faire cela d'une manière simple? Je n'ai pas vraiment envie d'appeler manuellement ConfigParse, puis de régler manuellement tous les défauts de tous les optinos sur les valeurs appropriées ...

58
andreas-h

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, lisez-le ensuite 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 par un fichier de configuration, puis de la remplacer par une option en ligne de commande. Par exemple.:

Par défaut sans entrée d'utilisateur:

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

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 légèrement compliqué de manipuler correctement -h 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 __== "__main__":
    sys.exit(main())
62
Von

Check out ConfigArgParse - est un nouveau package PyPI ( open source ) qui sert de remplacement instantané pour argparse avec un support supplémentaire pour les fichiers de configuration et les variables d’environnement.

13
user553965

J'utilise ConfigParser et argparse avec des sous-commandes pour gérer de telles 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 (from 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')
9
xubuntix

Je ne peux pas dire que ce soit la meilleure solution, mais j'ai une classe OptionParser que j'ai créée et qui correspond exactement à cela - agit comme optparse.OptionParser avec des valeurs par défaut provenant d'une section du 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.

4
Blair Conrad

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

Le problème avec current est qu'il ne provoquera pas d'erreur si les arguments du fichier de configuration ne sont pas valides. Voici une version avec un inconvénient différent: vous devez inclure le préfixe -- ou - dans les clés.

Voici le code python ( Lien Gist avec la licence MIT):

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

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

L'inconvénient principal est que cela utilise parser.parse_args de manière assez furtive afin d'obtenir la vérification d'erreur de ArgumentParser, mais je ne suis au courant d'aucune autre solution.

1
Achal Dave

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
mosquito

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’y est pas définie, 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'
0
Vlad Bezden

fromfile_prefix_chars

Peut-être pas l’API parfaite, mais mérite d’être connue. 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.