默认值或给定值为None会导致ArgParse参数错误

4

在解析程序参数时,我希望能够在未指定为程序参数的情况下从环境中获取某些参数。

我目前的代码如下:

parser = argparse.ArgumentParser(
    description="Foo Bar Baz")
parser.add_argument('--metadata-api-user', type=str,
                    help='Username for basic authentication access to the metadata API. If not given, will be extracted from env variable {}'.format("API_USER"),
                    default=os.environ.get("API_USER"))
parser.add_argument('--metadata-api-password', type=str,
                    help='Password for basic authentication access to the metadata API. If not given, will be extracted from env variable {}'.format("API_PASS"),
                    default=os.environ.get("API_PASS"))
parser.add_argument('--version', type=int, help='Version number', default=1)
parser.add_argument('--location', type=str, help='Location to place thing (default: ./)',default="./")

args = parser.parse_args(args)

这提供了我想要的功能,但如果没有给定env变量并且也没有在命令行中给出,我希望argparse会引发ArgumentError。
如果仅在命令行中指定并未在环境变量中指定,则environ[keyname]在创建参数时将引发keyerror,这并不理想。
在创建参数时设置类似“allow-none'=false”的参数将是很好的选择,但如果有其他解决方案那就太棒了。

2
我认为自定义操作可能会对您有所帮助。https://docs.python.org/3/library/argparse.html#action - vishal
@serbia99,我试着为此实现一个自定义操作,但不幸的是,如果使用默认值,则不会调用该操作,而且唯一的方法是指定一个值,这意味着用户必须提供一个值或者会出现错误。无论如何,感谢您! - Jordan Mackie
1
后处理是更好的方法,正如您所指出的,“action.__call__”仅在用户提供参数时运行。默认处理中没有任何内容会触发ArgumentError,尤其不是带有自定义消息的ArgumentError。 - hpaulj
子类化是一个好方法,在子类中当defaultNone时,将参数设为必需。 - Eric
4个回答

4
default会在用户没有提供值时简单地返回给定的值,但它对于None和字符串有一个特殊情况。
None传递给default会短路并返回None
将字符串传递给default会使用type构造函数返回解析后的字符串。
我实现了一个自定义参数化类型称为not_none,它将尝试使用给定的类型构造函数解析字符串,如果字符串为空,则会响亮地失败。
import argparse
from os import environ
from typing import Callable, TypeVar
T = TypeVar('T')

def not_none(type_constructor: Callable[[str], T] = str):
    """Convert a string to a type T."""
    def parse(s: str) -> T:
        """argparse will call this function if `s` is an empty string, but not when `s` is None."""
        if not s:
            raise ValueError
        else:
            return type_constructor(s)
    return parse

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--foo', help="A foo...", type=not_none(int), default='1')
    parser.add_argument('--bar', help="A bar...", type=not_none(), default=environ.get('BAR', ''))
    # args = parser.parse_args(['--foo', '1'])
    args = parser.parse_args(['--bar', 'BAR'])
    print(args.foo, args.bar)

2
一个优雅的解决方案是通过子类化参数解析器来透明地实现你所描述的逻辑。
class SaneArgumentParser(argparse.ArgumentParser):
    """
    Argument parser for which arguments are required on the CLI unless:
      - required=False is provided
        and/or
      - a default value is provided that is not None.
    """
    def add_argument(self, *args, default=None, **kwargs):
        if default is None:
            # Tentatively make this argument required
            kwargs.setdefault("required", True)
        super().add_argument(*args, **kwargs, default=default)

你会像平常一样使用它:

if __name__ == "__main__":
    parser = SaneArgumentParser()
    parser.add_argument("--foo", default=os.environ.get("FOO"))
    args = parser.parse_args()
    print(args)

当然,在命令行界面上提供参数的方式与往常一样。
$ ./prog.py --foo=42
Namespace(foo='42')

在环境中提供一个参数也是有效的:
$ FOO=42 ./prog.py
Namespace(foo='42')

而且不提供任何参数会引发一个ArgumentError

$ ./prog.py 
usage: prog.py [-h] --foo FOO
prog.py: error: the following arguments are required: --foo

请注意,只有在使用 exit_on_error=False 时,才能捕获 ArgumentError,这仅适用于 Python 3.9 及更高版本:https://docs.python.org/3/library/argparse.html#exit-on-error

1
很遗憾我无法找到解决方案,所以我只能为每个可以从环境变量中获取的参数添加以下内容:
api_user_action = parser.add_argument('--metadata-api-user', type=str,
                    help='Username for basic authentication access to the metadata API. If not given, will be extracted from env variable {}'.format("API_USER"),
                    default=os.environ.get("API_USER"))

...

if args.metadata_api_user is None:
    raise argparse.ArgumentError(api_user_action,
                                 "API Username was not supplied in either command line or environment variables. \n" + parser.format_help())

如果没有提供必要的参数,该命令将打印用法和相应的错误消息,但需要从适用于每个参数的位置重复执行此操作。

2
你可以将这些“actions”收集到一个列表中,在解析后迭代该列表。然后测试可以写成:if getattr(args,action.dest) is None: ... - hpaulj
1
你甚至可以将 environ.get 留在这个阶段 - 除非你想在 help 行中显示它。 - hpaulj
@hpaulj,这听起来肯定是一种更好的方法,谢谢 :) - Jordan Mackie
添加代码:required = os.environ.get("API_USER", None) is None - kdubs

0
为什么不在构建参数时检查环境变量呢?
if "API_USER" in os.environ:
    parser.add_argument('--metadata-api-user', type=str, default=os.environ.get("API_USER"))
else:
    parser.add_argument('--metadata-api-user', type=str)

这似乎是获得所需结果最简单的方法。


这并没有解决原帖中的问题:当环境变量未设置时(因此default=None),他希望parse_args()引发一个ArgumentError异常(或者更准确地说是退出,这实际上是默认行为)。但在你的情况下,参数只是被设置为None。实际上,你需要在“环境变量未设置”情况下添加required=True来完成所要求的操作。 - Eric

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接