121 votes

Analyser le fichier config, de l'environnement, et les arguments de ligne de commande, pour obtenir une collection unique d'options

Python standard library a des modules pour le fichier de configuration de l'analyse (configparser), la variable d'environnement de lecture (os.environ), et l' argument de ligne de commande d'analyse (argparse). Je veux écrire un programme qui fait tous ceux, et aussi:

  • A une cascade de valeurs de l'option:

    • par défaut les valeurs de l'option, remplacée par
    • options du fichier de configuration, remplacée par
    • les variables d'environnement, remplacée par
    • options de ligne de commande.
  • Permet à un fichier de configuration de l'emplacement spécifié sur la ligne de commande avec, par exemple, --config-file foo.conf, et de lectures qui (à la place ou en complément, l'habitude fichier de configuration). Cela doit toujours obéir au-dessus de la cascade.

  • Permet l'option de définitions dans un seul endroit afin de déterminer la analyse le comportement des fichiers de configuration et de la ligne de commande.

  • Unifie l'analyse des options en une seule collection de config valeurs de l'option pour le reste du programme d'accès sans prendre soin d'où ils venaient.

Tout ce que je besoin est, apparemment, le Python de la bibliothèque standard, mais ils ne travaillent pas ensemble sans heurts.

Comment puis-je réaliser cela avec un minimum de déviation de la Python standard library?

36voto

Alex Szatmary Points 745

Le module argparse fait ce pas de noix, aussi longtemps que vous êtes heureux avec un fichier de config qui ressemble à la ligne de commande. (Je pense que c'est un avantage, car les utilisateurs n'ont qu'à apprendre l'un de la syntaxe.) Réglage fromfile-préfixe-chars , par exemple, @, fait en sorte que,

my_prog --foo=bar

est équivalent à

my_prog @baz.conf

si @baz.conf est,

--foo
bar

Vous pouvez même avoir votre code pour foo.conf automatiquement en modifiant argv

if os.path.exists('foo.conf'):
    argv = ['@foo.conf'] + argv
args = argparser.parse_args(argv)

Le format de ces fichiers de configuration est modifiable par une sous-classe de ArgumentParser et l'ajout d'un convert_arg_line_to_args méthode.

35voto

mgilson Points 92954

Voici un petit quelque chose que j'ai bidouillé. Se sentir libre de suggérer des améliorations/rapports de bogues dans les commentaires:

import argparse
import ConfigParser
import os

def _identity(x):
    return x

_SENTINEL = object()


class AddConfigFile(argparse.Action):
    def __call__(self,parser,namespace,values,option_string=None):
        # I can never remember if `values` is a list all the time or if it
        # can be a scalar string; this takes care of both.
        if isinstance(values,basestring):
            parser.config_files.append(values)
        else:
            parser.config_files.extend(values)


class ArgumentConfigEnvParser(argparse.ArgumentParser):
    def __init__(self,*args,**kwargs):
        """
        Added 2 new keyword arguments to the ArgumentParser constructor:

           config --> List of filenames to parse for config goodness
           default_section --> name of the default section in the config file
        """
        self.config_files = kwargs.pop('config',[])  #Must be a list
        self.default_section = kwargs.pop('default_section','MAIN')
        self._action_defaults = {}
        argparse.ArgumentParser.__init__(self,*args,**kwargs)


    def add_argument(self,*args,**kwargs):
        """
        Works like `ArgumentParser.add_argument`, except that we've added an action:

           config: add a config file to the parser

        This also adds the ability to specify which section of the config file to pull the 
        data from, via the `section` keyword.  This relies on the (undocumented) fact that
        `ArgumentParser.add_argument` actually returns the `Action` object that it creates.
        We need this to reliably get `dest` (although we could probably write a simple
        function to do this for us).
        """

        if 'action' in kwargs and kwargs['action'] == 'config':
            kwargs['action'] = AddConfigFile
            kwargs['default'] = argparse.SUPPRESS

        # argparse won't know what to do with the section, so 
        # we'll pop it out and add it back in later.
        #
        # We also have to prevent argparse from doing any type conversion,
        # which is done explicitly in parse_known_args.  
        #
        # This way, we can reliably check whether argparse has replaced the default.
        #
        section = kwargs.pop('section', self.default_section)
        type = kwargs.pop('type', _identity)
        default = kwargs.pop('default', _SENTINEL)

        if default is not argparse.SUPPRESS:
            kwargs.update(default=_SENTINEL)
        else:  
            kwargs.update(default=argparse.SUPPRESS)

        action = argparse.ArgumentParser.add_argument(self,*args,**kwargs)
        kwargs.update(section=section, type=type, default=default)
        self._action_defaults[action.dest] = (args,kwargs)
        return action

    def parse_known_args(self,args=None, namespace=None):
        # `parse_args` calls `parse_known_args`, so we should be okay with this...
        ns, argv = argparse.ArgumentParser.parse_known_args(self, args=args, namespace=namespace)
        config_parser = ConfigParser.SafeConfigParser()
        config_files = [os.path.expanduser(os.path.expandvars(x)) for x in self.config_files]
        config_parser.read(config_files)

        for dest,(args,init_dict) in self._action_defaults.items():
            type_converter = init_dict['type']
            default = init_dict['default']
            obj = default

            if getattr(ns,dest,_SENTINEL) is not _SENTINEL: # found on command line
                obj = getattr(ns,dest)
            else: # not found on commandline
                try:  # get from config file
                    obj = config_parser.get(init_dict['section'],dest)
                except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): # Nope, not in config file
                    try: # get from environment
                        obj = os.environ[dest.upper()]
                    except KeyError:
                        pass

            if obj is _SENTINEL:
                setattr(ns,dest,None)
            elif obj is argparse.SUPPRESS:
                pass
            else:
                setattr(ns,dest,type_converter(obj))

        return ns, argv


if __name__ == '__main__':
    fake_config = """
[MAIN]
foo:bar
bar:1
"""
    with open('_config.file','w') as fout:
        fout.write(fake_config)

    parser = ArgumentConfigEnvParser()
    parser.add_argument('--config-file', action='config', help="location of config file")
    parser.add_argument('--foo', type=str, action='store', default="grape", help="don't know what foo does ...")
    parser.add_argument('--bar', type=int, default=7, action='store', help="This is an integer (I hope)")
    parser.add_argument('--baz', type=float, action='store', help="This is an float(I hope)")
    parser.add_argument('--qux', type=int, default='6', action='store', help="this is another int")
    ns = parser.parse_args([])

    parser_defaults = {'foo':"grape",'bar':7,'baz':None,'qux':6}
    config_defaults = {'foo':'bar','bar':1}
    env_defaults = {"baz":3.14159}

    # This should be the defaults we gave the parser
    print ns
    assert ns.__dict__ == parser_defaults

    # This should be the defaults we gave the parser + config defaults
    d = parser_defaults.copy()
    d.update(config_defaults)
    ns = parser.parse_args(['--config-file','_config.file'])
    print ns
    assert ns.__dict__ == d

    os.environ['BAZ'] = "3.14159"

    # This should be the parser defaults + config defaults + env_defaults
    d = parser_defaults.copy()
    d.update(config_defaults)
    d.update(env_defaults)
    ns = parser.parse_args(['--config-file','_config.file'])
    print ns
    assert ns.__dict__ == d

    # This should be the parser defaults + config defaults + env_defaults + commandline
    commandline = {'foo':'3','qux':4} 
    d = parser_defaults.copy()
    d.update(config_defaults)
    d.update(env_defaults)
    d.update(commandline)
    ns = parser.parse_args(['--config-file','_config.file','--foo=3','--qux=4'])
    print ns
    assert ns.__dict__ == d

    os.remove('_config.file')

TODO

Cette mise en œuvre est encore incomplète. Voici une partielle TODO liste:

Se conformer au comportement documenté

  • (facile) Écrire une fonction qui détermine dest de args en add_argument, au lieu de compter sur l' Action objet
  • (trivial) Écrire un parse_args fonction qui utilise parse_known_args. (par exemple, une copie parse_args de la cpython mise en œuvre pour garantir des appels parse_known_args.)

Moins De Choses Simples...

Je n'ai pas essayé encore. C'est peu probable mais toujours possible!-qu'il pourrait travailler...

  • (difficile?) Exclusion Mutuelle
  • (difficile?) Argument Groupes (Si mis en œuvre, ces groupes doivent obtenir un section dans le fichier de configuration.)
  • (difficile?) Sous-Commandes (Sous-commandes doivent également obtenir un section dans le fichier de configuration.)

12voto

Piotr Dobrogost Points 14412

Il y a de la bibliothèque qui fait exactement ce qu'on appelle configglue.

configglue est une bibliothèque qui colle ensemble python optparse.OptionParser et ConfigParser.ConfigParser, de sorte que vous n'avez pas avoir à répéter à vous-même lorsque vous souhaitez exporter les mêmes options que pour un fichier de configuration et une interface en ligne de commande.

Elle aussi prend en charge les variables d'environnement.

Intéressant PyCon parler de la configuration par Łukasz Langa - Laissez-les Configurer!

5voto

Lars Wirzenius Points 12197

Le Python de la bibliothèque standard ne fournit pas de ce, pour autant que je sais. J'ai résolu ce problème par moi-même par l'écriture de code pour utiliser optparse et ConfigParser analyser la ligne de commande et des fichiers de configuration, et de fournir une couche d'abstraction au-dessus d'eux. Cependant, vous avez besoin de ce en tant que distincte de la dépendance, qui, à partir de votre commentaire précédent semble être désagréable.

Si vous voulez regarder le code que j'ai écrit, c'est à http://liw.fi/cliapp/. Il est intégré dans mon "application en ligne de commande-cadre" de la bibliothèque, car c'est une grande partie de ce que le cadre doit faire.

4voto

Russell Borogove Points 8423

Frapper à toutes ces exigences, je vous recommande de rédiger votre propre bibliothèque qui utilise à la fois [opt|arg]analyser et configparser pour la fonctionnalité sous-jacente.

Étant donné les deux premières et la dernière condition, je dirais que vous souhaitez:

Première étape: Faire une ligne de commande analyseur de passe qui ne regarde que les --config-file.

Étape deux: Parse le fichier de configuration.

Troisième étape: mettre en place une deuxième ligne de commande analyseur de passe à l'aide de la sortie du fichier de config passer comme paramètres par défaut.

La troisième exigence signifie probablement que vous devez concevoir votre propre définition de l'option et à l'exposer à toutes les fonctionnalités de optparse et configparser que vous vous souciez, et écrire une partie de la plomberie pour faire des conversions entre les deux.

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