103 votes

Quel est le meilleur moyen de permettre la substitution des options de configuration à la ligne de commande en Python ?

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

Maintenant, j'aimerais utiliser des fichiers de configuration « appropriés », par exemple de la classe ConfigParser. En même temps, les utilisateurs devraient toujours pouvoir changer des paramètres individuels en 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 en ligne de commande, mais lire les valeurs par défaut à partir d'un fichier de configuration au format ConfigParse.

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

3voto

Ciro Santilli Points 3341

fromfile_prefix_chars

Peut-être pas l'API la plus propre, mais vaut la peine 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

Cette convention @opts.txt a quelques précédents, par exemple dans la chaîne d'outils GCC : Qu'est-ce que "@" signifie en ligne de commande ?

Comment utiliser une option CLI appropriée pour indiquer le fichier d'options plutôt que le truc moche @ : comment faire en sorte qu'argparse lise les arguments à partir d'un fichier avec une option plutôt qu'un préfixe

Testé avec Python 3.6.5, Ubuntu 18.04.

3voto

Vlad Bezden Points 5024

Vous pouvez utiliser ChainMap

Un ChainMap regroupe plusieurs dictionnaires ou autres mappings ensemble pour créer une vue unique et modifiable. Si aucun mapping n'est spécifié, un seul dictionnaire vide est fourni afin qu'une nouvelle chaîne ait toujours au moins un mapping.

Vous pouvez combiner des valeurs de la ligne de commande, des variables d'environnement, d'un fichier de configuration et, si la valeur n'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: 'valeur-par-défaut'))
valeur = options['nomopt']
valeur2 = options['autre-option']

print(valeur, valeur2)
'valeur-opt', 'valeur-par-défaut'

2voto

mauvilsa Points 120

Il convient de mentionner ici jsonargparse (Note I am an author of the library), avec une licence MIT et disponible sur PyPI. Il s'agit également d'une extension de argparse qui prend en charge le chargement à partir de fichiers de configuration et des variables d'environnement. Il est similaire à ConfigArgParse, mais il est plus récent, avec de nombreuses fonctionnalités utiles et bien entretenu.

Un exemple main.py serait:

from jsonargparse import ArgumentParser, ActionConfigFile

parser = ArgumentParser()
parser.add_argument("--config", action=ActionConfigFile)
parser.add_argument("--opt1", default="default 1")
parser.add_argument("--opt2", default="default 2")
args = parser.parse_args()
print(args.opt1, args.opt2)

Avoir un fichier de configuration config.yaml avec le contenu:

opt1: one
opt2: two

Ensuite un exemple d'exécution à partir de la ligne de commande:

$ python main.py --config config.yaml --opt1 ONE
ONE two

1voto

1voto

mosquito Points 13

Essayez de cette manière

# encodage: 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 = "Charger la configuration à partir du fichier"

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

Utilisez-le:

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

Et créez un exemple de configuration:

# Exemple de configuration: /etc/myservice.conf
import os
host = os.getenv("HOST_NAME", "localhost")

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X