コマンドラインまたは環境変数から渡すことができる特定のオプションを持つスクリプトがあります。両方が存在する場合はCLIが優先され、どちらも設定されていない場合はエラーが発生します。
解析後にオプションが割り当てられていることを確認することもできますが、argparseに重い処理を行わせ、解析が失敗した場合に使用法ステートメントを表示するようにします。
私はこれに対するいくつかの代替アプローチを考え出しました(それらは個別に議論できるように回答として以下に投稿します)が、私にはかなり不機嫌に感じ、何かが足りないと思います。
これを行うための受け入れられた「最良の」方法はありますか?
(CLIオプションと環境変数の両方が設定されていない場合に編集して、望ましい動作を明確にします)
このパターンを頻繁に使用して、単純なアクションクラスをパッケージ化して処理しています。
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)
次に、これを私のコードから呼び出すことができます:
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()
取得したい変数を使用してos.environのgetに引数を追加するときに、default
変数を設定するだけです。 .get()
がその名前の環境変数を見つけられない場合、.get()
呼び出しの2番目の引数がデフォルト値になります。
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 は、argparseに環境変数のサポートを追加するため、次のようなことができます。
p = configargparse.ArgParser()
p.add('-m', '--moo', help='Path of cow', env_var='MOO_PATH')
options = p.parse_args()
私は通常、これを複数の引数(認証とAPIキー)に対して行う必要があります。これは単純で簡単です。 ** 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'))
1つのオプションは、環境変数が設定されているかどうかを確認し、それに応じてadd_argumentの呼び出しを変更することです。
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()
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))
シェル:
export foo=1
export bar=2
python3 script.py
トピックはかなり古いですが、同様の問題があり、解決策をあなたと共有したいと思いました。残念ながら、@ Russell Heillingによって提案されたカスタムアクションソリューションは、いくつかの理由で機能しません。
store_true
_など)を使用できなくなりますdefault
が_os.environ
_にない場合、envvar
にフォールバックしたい(それは簡単に修正できる)action
またはenvvar
(常にaction.dest.upper()
である必要があります)を指定せずに、すべての引数に対してこの動作を行いたいこれが私の解決策です(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)
_
元の質問/回答が私に多くの助けを与えたので、私は私の解決策を投稿すると思いました。
私の問題は、ラッセルのものとは少し異なります。 OptionParserを使用していますが、各引数の環境変数の代わりに、コマンドラインをシミュレートする1つしかありません。
つまり.
MY_ENVIRONMENT_ARGS = --arg1 "マルタ語" --arg2 "ファルコン" -r "1930" -h
解決:
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:])
ここでは、引数が見つからない場合でもエラーをスローしません。しかし、私が望むなら、私はただ次のようなことをすることができます
for key in options.__dict__:
if options.__dict__[key] is None:
# raise error/log problem/print to console/etc
ChainMap
の使用例 があり、デフォルト、環境変数、コマンドライン引数をマージします。
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)
素晴らしい話 について美しく、慣用的なpythonから来ました。
ただし、小文字の辞書キーと大文字の辞書キーの違いにどう対処するかはわかりません。両方の-u foobar
が引数として渡され、環境がUSER=bazbaz
に設定されている場合、combined
辞書は{'user': 'foobar', 'USER': 'bazbaz'}
のようになります。
比較的単純な(コメントが多いため長く見える)が、parse_args
の名前空間引数を使用してdefault
をくぐり抜けない完全なソリューションを次に示します。デフォルトでは、コマンドライン引数と同じように環境変数を解析しますが、簡単に変更できます。
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
プログラム例:
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)
プログラム出力の例:
$ 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)