La bibliothèque standard de Python comprend des modules pour l'analyse des fichiers de configuration ( configparser ), lecture des variables d'environnement ( os.environ ) et analyse des arguments de la ligne de commande ( argparse ). Je veux écrire un programme qui fait tout ça, et aussi:
A une cascade de valeurs d'option :
Permet un ou plusieurs emplacements de fichier de configuration spécifiés sur la ligne de commande avec par exemple --config-file foo.conf
, et le lit (à la place ou en complément du fichier de configuration habituel). Cela doit toujours obéir à la cascade ci-dessus.
Permet les définitions d'options en un seul endroit pour déterminer le comportement d'analyse des fichiers de configuration et de la ligne de commande.
Unifie les options analysées en une seule collection de valeurs d'options auxquelles le reste du programme peut accéder sans se soucier d'où elles viennent.
Tout ce dont j'ai besoin se trouve apparemment dans la bibliothèque standard Python, mais ils ne fonctionnent pas correctement.
Comment puis-je y parvenir avec un écart minimum par rapport à la bibliothèque standard Python?
Il semble que la bibliothèque standard ne résout pas ce problème, laissant chaque programmeur bricoler configparser
et argparse
et os.environ
tous ensemble de manière maladroite.
Le module argparse ne rend pas cela fou, tant que vous êtes satisfait d'un fichier de configuration qui ressemble à la ligne de commande. (Je pense que c'est un avantage, car les utilisateurs n'auront à apprendre qu'une seule syntaxe.) Réglage fromfile_prefix_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 demander à votre code de rechercher 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 en créant une sous-classe de ArgumentParser et en ajoutant une méthode convert_arg_line_to_args .
MISE À JOUR: J'ai finalement réussi à mettre cela sur pypi. Installez la dernière version via:
pip install configargparser
L'aide complète et les instructions sont ici .
Message d'origine
Voici un petit quelque chose que j'ai piraté ensemble. N'hésitez pas à suggérer des améliorations/rapports de bugs 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 __== '__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')
Cette implémentation est encore incomplète. Voici une liste partielle de TODO:
argparse
gère messages d'erreurdest
à partir de args
dans add_argument
, au lieu de s'appuyer sur l'objet Action
parse_args
fonction qui utilise parse_known_args
. (par exemple, copier parse_args
à partir de l'implémentation cpython
pour garantir qu'il appelle parse_known_args
.)Je n'ai encore rien essayé de tout cela. Il est peu probable - mais toujours possible! - que cela puisse simplement fonctionner ...
section
dans le fichier de configuration.)section
dans le fichier de configuration.)Il y a une bibliothèque qui fait exactement cela appelée configglue .
configglue est une bibliothèque qui colle les options optparse.OptionParser et ConfigParser.ConfigParser de python, afin que vous n'ayez pas à vous répéter lorsque vous souhaitez exporter les mêmes options dans un fichier de configuration et une interface de ligne de commande.
Il prend en charge les variables d'environnement.
Il y a aussi une autre bibliothèque appelée ConfigArgParse qui est
Un remplacement direct pour argparse qui permet également de définir des options via des fichiers de configuration et/ou des variables d'environnement.
Vous pourriez être intéressé par PyCon parler de la configuration par Łukasz Langa - Laissez-les se configurer!
Bien que je ne l'ai pas essayé par moi-même, il existe une bibliothèque ConfigArgParse qui indique qu'il fait la plupart des choses que vous voulez:
Un remplacement direct pour argparse qui permet également de définir des options via des fichiers de configuration et/ou des variables d'environnement.
La bibliothèque standard Python ne fournit pas cela, pour autant que je sache. J'ai résolu cela par moi-même en écrivant du code pour utiliser optparse
et ConfigParser
pour analyser le la ligne de commande et les fichiers de configuration, et fournissent une couche d'abstraction au-dessus d'eux. Cependant, vous en aurez besoin en tant que dépendance distincte, ce qui, d'après 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 ma bibliothèque "Framework d'application de ligne de commande", car c'est une grande partie de ce que le framework doit faire.
J'ai récemment essayé quelque chose comme ça, en utilisant "optparse".
Je l'ai configuré comme une sous-classe d'OptonParser, avec une commande '--Store' et '--Check'.
Le code ci-dessous devrait à peu près vous couvrir. Vous avez juste besoin de définir vos propres méthodes de chargement et de stockage qui acceptent/renvoient les dictionnaires et vous êtes la proie bien définie.
class SmartParse(optparse.OptionParser):
def __init__(self,defaults,*args,**kwargs):
self.smartDefaults=defaults
optparse.OptionParser.__init__(self,*args,**kwargs)
fileGroup = optparse.OptionGroup(self,'handle stored defaults')
fileGroup.add_option(
'-S','--Store',
dest='Action',
action='store_const',const='Store',
help='store command line settings'
)
fileGroup.add_option(
'-C','--Check',
dest='Action',
action='store_const',const='Check',
help ='check stored settings'
)
self.add_option_group(fileGroup)
def parse_args(self,*args,**kwargs):
(options,arguments) = optparse.OptionParser.parse_args(self,*args,**kwargs)
action = options.__dict__.pop('Action')
if action == 'Check':
assert all(
value is None
for (key,value) in options.__dict__.iteritems()
)
print 'defaults:',self.smartDefaults
print 'config:',self.load()
sys.exit()
Elif action == 'Store':
self.store(options.__dict__)
sys.exit()
else:
config=self.load()
commandline=dict(
[key,val]
for (key,val) in options.__dict__.iteritems()
if val is not None
)
result = {}
result.update(self.defaults)
result.update(config)
result.update(commandline)
return result,arguments
def load(self):
return {}
def store(self,optionDict):
print 'Storing:',optionDict
Pour atteindre toutes ces exigences, je recommanderais d'écrire votre propre bibliothèque qui utilise à la fois [opt | arg] parse et configparser pour la fonctionnalité sous-jacente.
Compte tenu des deux premières et de la dernière exigence, je dirais que vous voulez:
Première étape: effectuez une analyse en ligne de commande qui ne recherche que l'option --config-file.
Deuxième étape: analyser le fichier de configuration.
Étape trois: configurer une deuxième passe d'analyseur de ligne de commande en utilisant la sortie de la passe du fichier de configuration comme valeurs par défaut.
La troisième exigence signifie probablement que vous devez concevoir votre propre système de définition d'options pour exposer toutes les fonctionnalités d'optparse et de configparser qui vous intéressent, et écrire de la plomberie pour effectuer des conversions entre les deux.
Voici un module que j'ai piraté ensemble qui lit également les arguments de ligne de commande, les paramètres d'environnement, les fichiers ini et les valeurs des porte-clés. Il est également disponible dans un Gist .
"""
Configuration Parser
Configurable parser that will parse config files, environment variables,
keyring, and command-line arguments.
Example test.ini file:
[defaults]
gini=10
[app]
xini = 50
Example test.arg file:
--xfarg=30
Example test.py file:
import os
import sys
import config
def main(argv):
'''Test.'''
options = [
config.Option("xpos",
help="positional argument",
nargs='?',
default="all",
env="APP_XPOS"),
config.Option("--xarg",
help="optional argument",
default=1,
type=int,
env="APP_XARG"),
config.Option("--xenv",
help="environment argument",
default=1,
type=int,
env="APP_XENV"),
config.Option("--xfarg",
help="@file argument",
default=1,
type=int,
env="APP_XFARG"),
config.Option("--xini",
help="ini argument",
default=1,
type=int,
ini_section="app",
env="APP_XINI"),
config.Option("--gini",
help="global ini argument",
default=1,
type=int,
env="APP_GINI"),
config.Option("--karg",
help="secret keyring arg",
default=-1,
type=int),
]
ini_file_paths = [
'/etc/default/app.ini',
os.path.join(os.path.dirname(os.path.abspath(__file__)),
'test.ini')
]
# default usage
conf = config.Config(prog='app', options=options,
ini_paths=ini_file_paths)
conf.parse()
print conf
# advanced usage
cli_args = conf.parse_cli(argv=argv)
env = conf.parse_env()
secrets = conf.parse_keyring(namespace="app")
ini = conf.parse_ini(ini_file_paths)
sources = {}
if ini:
for key, value in ini.iteritems():
conf[key] = value
sources[key] = "ini-file"
if secrets:
for key, value in secrets.iteritems():
conf[key] = value
sources[key] = "keyring"
if env:
for key, value in env.iteritems():
conf[key] = value
sources[key] = "environment"
if cli_args:
for key, value in cli_args.iteritems():
conf[key] = value
sources[key] = "command-line"
print '\n'.join(['%s:\t%s' % (k, v) for k, v in sources.items()])
if __== "__main__":
if config.keyring:
config.keyring.set_password("app", "karg", "13")
main(sys.argv)
Example results:
$APP_XENV=10 python test.py api --xarg=2 @test.arg
<Config xpos=api, gini=1, xenv=10, xini=50, karg=13, xarg=2, xfarg=30>
xpos: command-line
xenv: environment
xini: ini-file
karg: keyring
xarg: command-line
xfarg: command-line
"""
import argparse
import ConfigParser
import copy
import os
import sys
try:
import keyring
except ImportError:
keyring = None
class Option(object):
"""Holds a configuration option and the names and locations for it.
Instantiate options using the same arguments as you would for an
add_arguments call in argparse. However, you have two additional kwargs
available:
env: the name of the environment variable to use for this option
ini_section: the ini file section to look this value up from
"""
def __init__(self, *args, **kwargs):
self.args = args or []
self.kwargs = kwargs or {}
def add_argument(self, parser, **override_kwargs):
"""Add an option to a an argparse parser."""
kwargs = {}
if self.kwargs:
kwargs = copy.copy(self.kwargs)
try:
del kwargs['env']
except KeyError:
pass
try:
del kwargs['ini_section']
except KeyError:
pass
kwargs.update(override_kwargs)
parser.add_argument(*self.args, **kwargs)
@property
def type(self):
"""The type of the option.
Should be a callable to parse options.
"""
return self.kwargs.get("type", str)
@property
def name(self):
"""The name of the option as determined from the args."""
for arg in self.args:
if arg.startswith("--"):
return arg[2:].replace("-", "_")
Elif arg.startswith("-"):
continue
else:
return arg.replace("-", "_")
@property
def default(self):
"""The default for the option."""
return self.kwargs.get("default")
class Config(object):
"""Parses configuration sources."""
def __init__(self, options=None, ini_paths=None, **parser_kwargs):
"""Initialize with list of options.
:param ini_paths: optional paths to ini files to look up values from
:param parser_kwargs: kwargs used to init argparse parsers.
"""
self._parser_kwargs = parser_kwargs or {}
self._ini_paths = ini_paths or []
self._options = copy.copy(options) or []
self._values = {option.name: option.default
for option in self._options}
self._parser = argparse.ArgumentParser(**parser_kwargs)
self.pass_thru_args = []
@property
def prog(self):
"""Program name."""
return self._parser.prog
def __getitem__(self, key):
return self._values[key]
def __setitem__(self, key, value):
self._values[key] = value
def __delitem__(self, key):
del self._values[key]
def __contains__(self, key):
return key in self._values
def __iter__(self):
return iter(self._values)
def __len__(self):
return len(self._values)
def get(self, key, *args):
"""
Return the value for key if it exists otherwise the default.
"""
return self._values.get(key, *args)
def __getattr__(self, attr):
if attr in self._values:
return self._values[attr]
else:
raise AttributeError("'config' object has no attribute '%s'"
% attr)
def build_parser(self, options, **override_kwargs):
"""."""
kwargs = copy.copy(self._parser_kwargs)
kwargs.update(override_kwargs)
if 'fromfile_prefix_chars' not in kwargs:
kwargs['fromfile_prefix_chars'] = '@'
parser = argparse.ArgumentParser(**kwargs)
if options:
for option in options:
option.add_argument(parser)
return parser
def parse_cli(self, argv=None):
"""Parse command-line arguments into values."""
if not argv:
argv = sys.argv
options = []
for option in self._options:
temp = Option(*option.args, **option.kwargs)
temp.kwargs['default'] = argparse.SUPPRESS
options.append(temp)
parser = self.build_parser(options=options)
parsed, extras = parser.parse_known_args(argv[1:])
if extras:
valid, pass_thru = self.parse_passthru_args(argv[1:])
parsed, extras = parser.parse_known_args(valid)
if extras:
raise AttributeError("Unrecognized arguments: %s" %
' ,'.join(extras))
self.pass_thru_args = pass_thru + extras
return vars(parsed)
def parse_env(self):
results = {}
for option in self._options:
env_var = option.kwargs.get('env')
if env_var and env_var in os.environ:
value = os.environ[env_var]
results[option.name] = option.type(value)
return results
def get_defaults(self):
"""Use argparse to determine and return dict of defaults."""
parser = self.build_parser(options=self._options)
parsed, _ = parser.parse_known_args([])
return vars(parsed)
def parse_ini(self, paths=None):
"""Parse config files and return configuration options.
Expects array of files that are in ini format.
:param paths: list of paths to files to parse (uses ConfigParse logic).
If not supplied, uses the ini_paths value supplied on
initialization.
"""
results = {}
config = ConfigParser.SafeConfigParser()
config.read(paths or self._ini_paths)
for option in self._options:
ini_section = option.kwargs.get('ini_section')
if ini_section:
try:
value = config.get(ini_section, option.name)
results[option.name] = option.type(value)
except ConfigParser.NoSectionError:
pass
return results
def parse_keyring(self, namespace=None):
"""."""
results = {}
if not keyring:
return results
if not namespace:
namespace = self.prog
for option in self._options:
secret = keyring.get_password(namespace, option.name)
if secret:
results[option.name] = option.type(secret)
return results
def parse(self, argv=None):
"""."""
defaults = self.get_defaults()
args = self.parse_cli(argv=argv)
env = self.parse_env()
secrets = self.parse_keyring()
ini = self.parse_ini()
results = defaults
results.update(ini)
results.update(secrets)
results.update(env)
results.update(args)
self._values = results
return self
@staticmethod
def parse_passthru_args(argv):
"""Handles arguments to be passed thru to a subprocess using '--'.
:returns: Tuple of two lists; args and pass-thru-args
"""
if '--' in argv:
dashdash = argv.index("--")
if dashdash == 0:
return argv[1:], []
Elif dashdash > 0:
return argv[0:dashdash], argv[dashdash + 1:]
return argv, []
def __repr__(self):
return "<Config %s>" % ', '.join([
'%s=%s' % (k, v) for k, v in self._values.iteritems()])
def comma_separated_strings(value):
"""Handles comma-separated arguments passed in command-line."""
return map(str, value.split(","))
def comma_separated_pairs(value):
"""Handles comma-separated key/values passed in command-line."""
pairs = value.split(",")
results = {}
for pair in pairs:
key, pair_value = pair.split('=')
results[key] = pair_value
return results
Vous pouvez utiliser ChainMap pour cela. Jetez un œil à mon exemple que j'ai fourni dans "Quelle est la meilleure façon d'autoriser les options de configuration à être remplacées sur la ligne de commande en Python?" SO question.