Python 使用argparse时从环境变量设置选项
我有一个脚本,它有一些选项,可以通过命令行传递,也可以通过环境变量传递。如果两者都存在,则CLI应优先;如果两者都未设置,则会发生错误 我可以检查解析后是否分配了该选项,但我更喜欢让argparse来完成繁重的工作,并在解析失败时负责显示usage语句 我已经提出了一些替代方法(我将在下面作为答案发布,以便单独讨论),但它们让我感到非常困惑,我认为我遗漏了一些东西 有没有公认的“最佳”方法Python 使用argparse时从环境变量设置选项,python,argparse,Python,Argparse,我有一个脚本,它有一些选项,可以通过命令行传递,也可以通过环境变量传递。如果两者都存在,则CLI应优先;如果两者都未设置,则会发生错误 我可以检查解析后是否分配了该选项,但我更喜欢让argparse来完成繁重的工作,并在解析失败时负责显示usage语句 我已经提出了一些替代方法(我将在下面作为答案发布,以便单独讨论),但它们让我感到非常困惑,我认为我遗漏了一些东西 有没有公认的“最佳”方法 (当CLI选项和环境变量都未设置时,编辑以明确所需的行为)一个选项是检查是否设置了环境变量,并相应地修改添
(当CLI选项和环境变量都未设置时,编辑以明确所需的行为)一个选项是检查是否设置了环境变量,并相应地修改添加_参数的调用 e、 g
我经常使用此模式,因此打包了一个简单的action类来处理它:
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()
可以使用要获取的环境变量将参数的
default=
设置为of
您还可以在.get()
调用中传递第二个参数,如果.get()
未找到具有该名称的环境变量,则该参数是默认值(在这种情况下,默认情况下.get()
返回None
)
我想我会发布我的解决方案,因为原来的问题/答案给了我很多帮助 我的问题和罗素的有点不同。我使用OptionParser,而不是每个参数的环境变量,我只有一个模拟命令行的变量 i、 e 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
这个话题很老了,但我也有类似的问题,我想我会和你们分享我的解决方案。不幸的是,@Russell Heilling建议的自定义操作解决方案不适合我,原因有两个:
- 它阻止我使用(如
)store\u true
- 当
不在envvar
中时,我更希望它返回到os.environ
(这很容易修复)default
- 我希望所有参数都具有这种行为,而不指定
或action
(应该始终是envvar
)action.dest.upper()
p = configargparse.ArgParser()
p.add('-m', '--moo', help='Path of cow', env_var='MOO_PATH')
options = p.parse_args()
我通常必须对多个参数(身份验证和API密钥)执行此操作。。这是简单而直接的。使用**夸格
def环境或所需(键):
返回(
{'default':os.environ.get(key)}如果os.environ.get(key)
else{'required':True}
)
parser.add_参数('--thing',**environ_或_required('thing'))
有一种方法,可以将默认值、环境变量和命令行参数合并在一起
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)
我是从一个关于美丽而地道的蟒蛇的故事中学到的
但是,我不知道如何区分小写和大写字典键。如果将
-u foobar
都作为参数传递,并且将环境设置为USER=bazbaz
,那么组合的
字典将看起来像{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)
您可以使用
OptionParser()
外壳:
export foo=1
export bar=2
python3 script.py
库显式地处理此问题:
import click
@click.command()
@click.argument('src', envvar='SRC', type=click.File('r'))
def echo(src):
"""Print value of SRC environment variable."""
click.echo(src.read())
并从命令行执行以下操作:
$ export SRC=hello.txt
$ echo
Hello World!
您可以使用安装
pip install click
另一种选择:
parser = argparse.ArgumentParser()
env = os.environ
def add_argument(key, *args, **kwargs):
if key in env:
kwargs['default'] = env[key]
parser.add_argument(*args, **kwargs)
add_argument('--type', type=str)
或者使用os.getenv
设置默认值:
parser = argparse.ArgumentParser()
parser.add_argument('--type', type=int, default=os.getenv('type',100))
None是.get()的默认值,因此不需要像这样显式声明。在这个问题上,我可能应该更清楚-该选项至少需要位于一个环境变量或CLI中。如果您像这样设置默认值,那么args.url很可能最终为None,这就是我想要避免的…啊,我知道您在寻找什么了。老实说,我会使用我写的东西,在解析完args之后,只需检查
如果不是args.url:exit(parser.print_usage())
并退出。这是一种很好的快速处理方法。我已经打包了我自己的动作处理程序,因为我经常使用这种模式,但这肯定是我快速简单脚本的退路。在我看来,这是一种更具表现力的方式。如果没有一个非常具体的原因让事情变得更复杂,那么这就是优雅和简短。因此,我认为这应该是“正确”的答案,尽管@RussellHeilling也提出了一个好的替代方案。这个答案提出的方式是不好的。“默认值”是指“应用程序的默认值”,而不是“当前环境中的默认值”。人们可以查看--help,查看特定的默认值,假设它永远是默认值,转到另一台机器/会话,获取kaboom。是否应该编辑此默认值以在os.environ中查找?“if envvar in os.environ:default=envvar”-->“if envvar in os.environ:default=os.environ[envvar]”如果没有默认值,为什么只使用envvar指定的值?调用方显式地提供了一个值,它不应该覆盖默认值吗?这个解决方案很棒。但是,它有一个副作用:根据您的环境(无论是否设置了env var),用法文本可能会有所不同。这是一个很好的答案,因为您可以轻松地添加其他功能
$ export SRC=hello.txt
$ echo
Hello World!
pip install click
parser = argparse.ArgumentParser()
env = os.environ
def add_argument(key, *args, **kwargs):
if key in env:
kwargs['default'] = env[key]
parser.add_argument(*args, **kwargs)
add_argument('--type', type=str)
parser = argparse.ArgumentParser()
parser.add_argument('--type', type=int, default=os.getenv('type',100))