J'ai un script qui a certaines options qui peuvent être passées sur la ligne de commande ou à partir de variables d'environnement. La CLI doit avoir la priorité si les deux sont présentes et une erreur se produit si aucune n'est définie.
Je pourrais vérifier que l'option est attribuée après l'analyse, mais je préfère laisser argparse faire le gros du travail et être responsable de l'affichage de la déclaration d'utilisation si l'analyse échoue.
J'ai trouvé quelques approches alternatives à cela (que je posterai ci-dessous en tant que réponses afin qu'elles puissent être discutées séparément) mais elles me semblent assez compliquées et je pense que je manque quelque chose.
Existe-t-il une "meilleure" façon acceptée de procéder?
(Modifier pour rendre le comportement souhaité clair lorsque l'option CLI et la variable d'environnement ne sont pas définies)
J'utilise ce modèle assez fréquemment pour avoir mis en place une classe d'action simple pour le gérer:
import argparse
import os
class EnvDefault(argparse.Action):
def __init__(self, envvar, required=True, default=None, **kwargs):
if not default and envvar:
if envvar in os.environ:
default = os.environ[envvar]
if required and default:
required = False
super(EnvDefault, self).__init__(default=default, required=required,
**kwargs)
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
Je peux ensuite appeler cela depuis mon code avec:
import argparse
from envdefault import EnvDefault
parser=argparse.ArgumentParser()
parser.add_argument(
"-u", "--url", action=EnvDefault, envvar='URL',
help="Specify the URL to process (can also be specified using URL environment variable)")
args=parser.parse_args()
Je voudrais simplement définir la variable default
lors de l'ajout d'un argument à un get d'os.environ avec la variable que vous souhaitez saisir. Le 2e argument de l'appel .get()
est la valeur par défaut si .get()
ne trouve pas de variable d'environnement de ce nom.
import argparse
import os
parser = argparse.ArgumentParser(description='test')
parser.add_argument('--url', default=os.environ.get('URL', None))
args = parser.parse_args()
if not args.url:
exit(parser.print_usage())
ConfigArgParse ajoute la prise en charge des variables d'environnement à argparse, vous pouvez donc faire des choses comme:
p = configargparse.ArgParser()
p.add('-m', '--moo', help='Path of cow', env_var='MOO_PATH')
options = p.parse_args()
Je dois généralement le faire pour plusieurs arguments (authentification et clés API) .. c'est simple et direct. Utilise ** kwargs.
def environ_or_required(key):
return (
{'default': os.environ.get(key)} if os.environ.get(key)
else {'required': True}
)
parser.add_argument('--thing', **environ_or_required('THING'))
Une option consiste à vérifier si la variable d'environnement est définie et à modifier les appels à add_argument en conséquence, par exemple.
import argparse
import os
parser=argparse.ArgumentParser()
if 'CVSWEB_URL' in os.environ:
cvsopt = { 'default': os.environ['CVSWEB_URL'] }
else:
cvsopt = { 'required': True }
parser.add_argument(
"-u", "--cvsurl", help="Specify url (overrides CVSWEB_URL environment variable)",
**cvsopt)
args=parser.parse_args()
Vous pouvez utiliser OptionParser()
from optparse import OptionParser
def argument_parser(self, parser):
parser.add_option('--foo', dest="foo", help="foo", default=os.environ.get('foo', None))
parser.add_option('--bar', dest="bar", help="bar", default=os.environ.get('bar', None))
return(parser.parse_args())
parser = OptionParser()
(options, args) = argument_parser(parser)
foo = options.foo
bar = options.bar
print("foo: {}".format(foo))
print("bar: {}".format(bar))
Coquille:
export foo=1
export bar=2
python3 script.py
Le sujet est assez ancien, mais j'ai eu un problème similaire et j'ai pensé partager ma solution avec vous. Malheureusement, la solution d'action personnalisée suggérée par @Russell Heilling ne fonctionne pas pour moi pour deux raisons:
store_true
)default
lorsque envvar
n'est pas dans os.environ
(Cela pourrait être facilement corrigé)action
ou envvar
(qui devrait toujours être action.dest.upper()
)Voici ma solution (en Python 3):
class CustomArgumentParser(argparse.ArgumentParser):
class _CustomHelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
help = super()._get_help_string(action)
if action.dest != 'help':
help += ' [env: {}]'.format(action.dest.upper())
return help
def __init__(self, *, formatter_class=_CustomHelpFormatter, **kwargs):
super().__init__(formatter_class=formatter_class, **kwargs)
def _add_action(self, action):
action.default = os.environ.get(action.dest.upper(), action.default)
return super()._add_action(action)
Je pensais publier ma solution car la question/réponse d'origine m'a beaucoup aidé.
Mon problème est un peu différent de celui de Russell. J'utilise OptionParser et au lieu d'une variable d'environnement pour chaque argument, j'en ai juste un qui simule la ligne de commande.
c'est à dire.
MY_ENVIRONMENT_ARGS = --arg1 "maltais" --arg2 "Falcon" -r "1930" -h
Solution:
def set_defaults_from_environment(oparser):
if 'MY_ENVIRONMENT_ARGS' in os.environ:
environmental_args = os.environ[ 'MY_ENVIRONMENT_ARGS' ].split()
opts, _ = oparser.parse_args( environmental_args )
oparser.defaults = opts.__dict__
oparser = optparse.OptionParser()
oparser.add_option('-a', '--arg1', action='store', default="Consider")
oparser.add_option('-b', '--arg2', action='store', default="Phlebas")
oparser.add_option('-r', '--release', action='store', default='1987')
oparser.add_option('-h', '--hardback', action='store_true', default=False)
set_defaults_from_environment(oparser)
options, _ = oparser.parse_args(sys.argv[1:])
Ici, je ne lance pas d'erreur si aucun argument n'est trouvé. Mais si je le souhaite, je pourrais simplement faire quelque chose comme
for key in options.__dict__:
if options.__dict__[key] is None:
# raise error/log problem/print to console/etc
Il existe un exemple d'utilisation pour ChainMap
où vous fusionnez les valeurs par défaut, les variables d'environnement et les arguments de ligne de commande.
import os, argparse
defaults = {'color': 'red', 'user': 'guest'}
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args()
command_line_args = {k:v for k, v in vars(namespace).items() if v}
combined = ChainMap(command_line_args, os.environ, defaults)
Je suis venu de n grand discours sur beau et idiomatique python.
Cependant, je ne sais pas comment faire la différence entre les clés de dictionnaire minuscules et majuscules. Dans le cas où les deux -u foobar
est passé en argument et l'environnement est défini sur USER=bazbaz
, le dictionnaire combined
ressemblera à {'user': 'foobar', 'USER': 'bazbaz'}
.
Voici une solution relativement simple (qui semble plus longue car elle est bien commentée) et pourtant complète qui évite de surcharger default
en utilisant l'argument d'espace de nom de parse_args
. Par défaut, il n'analyse pas les variables d'environnement différemment des arguments de ligne de commande, mais cela peut facilement être modifié.
import shlex
# Notes:
# * Based on https://github.com/python/cpython/blob/
# 15bde92e47e824369ee71e30b07f1624396f5cdc/
# Lib/argparse.py
# * Haven't looked into handling "required" for mutually exclusive groups
# * Probably should make new attributes private even though it's ugly.
class EnvArgParser(argparse.ArgumentParser):
# env_k: The keyword to "add_argument" as well as the attribute stored
# on matching actions.
# env_f: The keyword to "add_argument". Defaults to "env_var_parse" if
# not provided.
# env_i: Basic container type to identify unfilled arguments.
env_k = "env_var"
env_f = "env_var_parse"
env_i = type("env_i", (object,), {})
def add_argument(self, *args, **kwargs):
map_f = (lambda m,k,f=None,d=False:
(k, k in m, m.pop(k,f) if d else m.get(k,f)))
env_k = map_f(kwargs, self.env_k, d=True, f="")
env_f = map_f(kwargs, self.env_f, d=True, f=self.env_var_parse)
if env_k[1] and not isinstance(env_k[2], str):
raise ValueError(f"Parameter '{env_k[0]}' must be a string.")
if env_f[1] and not env_k[1]:
raise ValueError(f"Parameter '{env_f[0]}' requires '{env_k[0]}'.")
if env_f[1] and not callable(env_f[2]):
raise ValueError(f"Parameter '{env_f[0]}' must be callable.")
action = super().add_argument(*args, **kwargs)
if env_k[1] and not action.option_strings:
raise ValueError(f"Positional parameters may not specify '{env_k[0]}'.")
# We can get the environment now:
# * We need to know now if the keys exist anyway
# * os.environ is static
env_v = map_f(os.environ, env_k[2], f="")
# Examples:
# env_k:
# ("env_var", True, "FOO_KEY")
# env_v:
# ("FOO_KEY", False, "")
# ("FOO_KEY", True, "FOO_VALUE")
#
# env_k:
# ("env_var", False, "")
# env_v:
# ("" , False, "")
# ("", True, "RIDICULOUS_VALUE")
# Add the identifier to all valid environment variable actions for
# later access by i.e. the help formatter.
if env_k[1]:
if env_v[1] and action.required:
action.required = False
i = self.env_i()
i.a = action
i.k = env_k[2]
i.f = env_f[2]
i.v = env_v[2]
i.p = env_v[1]
setattr(action, env_k[0], i)
return action
# Overriding "_parse_known_args" is better than "parse_known_args":
# * The namespace will already have been created.
# * This method runs in an exception handler.
def _parse_known_args(self, arg_strings, namespace):
"""precedence: cmd args > env var > preexisting namespace > defaults"""
for action in self._actions:
if action.dest is argparse.SUPPRESS:
continue
try:
i = getattr(action, self.env_k)
except AttributeError:
continue
if not i.p:
continue
setattr(namespace, action.dest, i)
namespace, arg_extras = super()._parse_known_args(arg_strings, namespace)
for k,v in vars(namespace).copy().items():
# Setting "env_i" on the action is more effective than using an
# empty unique object() and mapping namespace attributes back to
# actions.
if isinstance(v, self.env_i):
fv = v.f(v.a, v.k, v.v, arg_extras)
if fv is argparse.SUPPRESS:
delattr(namespace, k)
else:
# "_parse_known_args::take_action" checks for action
# conflicts. For simplicity we don't.
v.a(self, namespace, fv, v.k)
return (namespace, arg_extras)
def env_var_parse(self, a, k, v, e):
# Use shlex, yaml, whatever.
v = shlex.split(v)
# From "_parse_known_args::consume_optional".
n = self._match_argument(a, "A"*len(v))
# From the main loop of "_parse_known_args". Treat additional
# environment variable arguments just like additional command-line
# arguments (which will eventually raise an exception).
e.extend(v[n:])
return self._get_values(a, v[:n])
# Derived from "ArgumentDefaultsHelpFormatter".
class EnvArgHelpFormatter(argparse.HelpFormatter):
"""Help message formatter which adds environment variable keys to
argument help.
"""
env_k = EnvArgParser.env_k
# This is supposed to return a %-style format string for "_expand_help".
# Since %-style strings don't support attribute access we instead expand
# "env_k" ourselves.
def _get_help_string(self, a):
h = super()._get_help_string(a)
try:
i = getattr(a, self.env_k)
except AttributeError:
return h
s = f" ({self.env_k}: {i.k})"
if s not in h:
h += s
return h
# An example mix-in.
class DefEnvArgHelpFormatter\
( EnvArgHelpFormatter
, argparse.ArgumentDefaultsHelpFormatter
):
pass
Exemple de programme:
parser = EnvArgParser\
( prog="Test Program"
, formatter_class=DefEnvArgHelpFormatter
)
parser.add_argument\
( '--bar'
, required=True
, env_var="BAR"
, type=int
, nargs="+"
, default=22
, help="Help message for bar."
)
parser.add_argument\
( 'baz'
, type=int
)
args = parser.parse_args()
print(args)
Exemple de sortie de programme:
$ BAR="1 2 3 '45 ' 6 7" ./envargparse.py 123
Namespace(bar=[1, 2, 3, 45, 6, 7], baz=123)
$ ./envargparse.py -h
usage: Test Program [-h] --bar BAR [BAR ...] baz
positional arguments:
baz
optional arguments:
-h, --help show this help message and exit
--bar BAR [BAR ...] Help message for bar. (default: 22) (env_var: BAR)