Python:参数解析验证最佳实践

Python: Argument Parsing Validation Best Practices

是否可以使用argparse模块在解析参数时添加验证?

from argparse import ArgumentParser

parser = ArgumentParser(description='Argument parser for PG restore')

parser.add_argument('--database', dest='database',
                    default=None, required=False, help='Database to restore')

parser.add_argument('--backup', dest='backup',
                    required=True, help='Location of the backup file')

parsed_args = parser.parse_args()

是否可以向该参数解析器添加验证检查,以确保备份文件/数据库存在?不必在此之后为每个参数添加额外的检查,例如:

from os.path import exists
if not database_exists(parsed_args.database):
    raise DatabaseNotFoundError
if not exists(parsed_args.backup):
    raise FileNotFoundError

当然可以!您只需将自定义操作指定为 class,并覆盖 __call__(..)Link to documentation.

类似于:

import argparse

class FooAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        if values != "bar":
            print("Got value:", values)
            raise ValueError("Not a bar!")
        setattr(namespace, self.dest, values)


parser = argparse.ArgumentParser()
parser.add_argument("--foo", action=FooAction)

parsed_args = parser.parse_args()

在你的具体情况下,我想你会有 DatabaseActionFileAction(或类似的东西)。

argparse.FileType是一个type工厂class,可以打开文件,当然,如果文件不存在或无法创建,过程中会报错.您可以查看其代码,了解如何创建您自己的 class(或函数)来测试您的输入。

参数 type 参数是一个可调用的(函数等),它接受一个字符串,根据需要对其进行测试,并将其(根据需要)转换为您想要保存到 args命名空间。所以它可以做任何你想要的测试。如果 type 引发错误,则解析器会创建一条错误消息(和用法)并退出。

现在是否是进行测试的正确位置取决于您的情况。有时用 FileType 打开文件没问题,但你必须自己关闭它,或者等待程序结束。您不能在 with open(filename) as f: 上下文中使用该打开的文件。这同样适用于您的数据库。在复杂的程序中,您可能不想立即打开或创建文件。

我为 Python bug/issue 编写了 FileType 的变体,它创建了 context,一个可以在 with 上下文中使用的对象.我还使用 os 测试来检查文件是否存在或是否可以创建,但实际上并没有这样做。但是,如果 file 是您不想关闭的 stdin/out,则需要进一步的技巧。有时在 argparse 中尝试做这样的事情只是比它值得做的更多工作。

无论如何,如果你有一个简单的测试方法,你可以将它包装在一个简单的 type 函数中,如下所示:

def database(astring):
    from os.path import exists
    if not database_exists(astring):
        raise ValueError  # or TypeError, or `argparse.ArgumentTypeError
    return astring

parser.add_argument('--database', dest='database',
                type = database, 
                default=None, required=False, help='Database to restore')

我认为是否在 typeAction 中实施这样的测试并不重要。我觉得type更简单,更符合开发者的意图。

这是 的更好版本 我无法在一行评论中很好地解释差异。引发 ValueError 将导致终端中的回溯。 您应该使用消息调用 parser.error,而不是引发 ValueErrror,例如:

from validators.url import url
class ValidateUrl(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        for value in values:
            if url(value) != True:
                parser.error(f"Please enter a valid url. Got: {value}")
        setattr(namespace, self.dest, values)

# In your parser code: 
parser.add_argument("-u", "--url", dest="url", action=ValidateUrl, help="A url to download")

使用这个脚本我可以测试建议的替代方案。

import argparse

class ValidateUrl(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        if values != "bar":
            parser.error(f"Please enter a valid. Got: {values}")
        setattr(namespace, self.dest, values)

class FooAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        if values != "bar":
            print("Got value:", values)
            #raise ValueError("Not a bar!")  # shows a traceback, not usage
            raise argparse.ArgumentError(self, 'Not a bar')
        setattr(namespace, self.dest, values)

def database(astring):
    if astring != "bar":
        #raise argparse.ArgumentTypeError("not a bar")   # sustom message
        raise ValueError('not a bar') # standard error
        # error: argument --data: invalid database value: 'xxx'
    return astring

parser = argparse.ArgumentParser()
parser.add_argument("--url", action=ValidateUrl)
parser.add_argument("--foo", action = FooAction)
parser.add_argument('--data', type = database)

if __name__=='__main__':
    args = parser.parse_args()
    print(args)

一个工作案例:

1254:~/mypy$ python3 stack37471636.py --url bar --foo bar --data bar
Namespace(data='bar', foo='bar', url='bar')

错误

parser.error 案例的使用和退出

1255:~/mypy$ python3 stack37471636.py --url xxx
usage: stack37471636.py [-h] [--url URL] [--foo FOO] [--data DATA]
stack37471636.py: error: Please enter a valid. Got: xxx

来自 type 函数 ValueError 的标准化消息

1256:~/mypy$ python3 stack37471636.py --data xxx
usage: stack37471636.py [-h] [--url URL] [--foo FOO] [--data DATA]
stack37471636.py: error: argument --data: invalid database value: 'xxx'

使用ArgumentTypeError,消息按原样显示:

1246:~/mypy$ python3 stack37471636.py --url bar --foo bar --data xxx
usage: stack37471636.py [-h] [--url URL] [--foo FOO] [--data DATA]
stack37471636.py: error: argument --data: not a bar

FooActionArgumentError:

1257:~/mypy$ python3 stack37471636.py --foo xxx
Got value: xxx
usage: stack37471636.py [-h] [--url URL] [--foo FOO] [--data DATA]
stack37471636.py: error: argument --foo: Not a bar

type 中的错误被转换为 ArgumentError。请注意 ArgumentError 标识 argument。调用 parser.error 不会。

如果 FooAction 引发 ValueError,则显示常规回溯,不使用。

1246:~/mypy$ python3 stack37471636.py --url bar --foo xxx --data bar
Got value: xxx
Traceback (most recent call last):
  File "stack37471636.py", line 27, in <module>
    args = parser.parse_args()
  File "/usr/lib/python3.8/argparse.py", line 1780, in parse_args
    args, argv = self.parse_known_args(args, namespace)
  File "/usr/lib/python3.8/argparse.py", line 1812, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
  File "/usr/lib/python3.8/argparse.py", line 2018, in _parse_known_args
    start_index = consume_optional(start_index)
  File "/usr/lib/python3.8/argparse.py", line 1958, in consume_optional
    take_action(action, args, option_string)
  File "/usr/lib/python3.8/argparse.py", line 1886, in take_action
    action(self, namespace, argument_values, option_string)
  File "stack37471636.py", line 13, in __call__
    raise ValueError("Not a bar!")
ValueError: Not a bar!

我相信 ArgumentErrorArgumentTypeError 是首选,或者至少是预期的选择。自动生成的错误使用这些。

通常在解析后使用parser.error,结果例如

1301:~/mypy$ python3 stack37471636.py
Namespace(data=None, foo=None, url=None)
usage: stack37471636.py [-h] [--url URL] [--foo FOO] [--data DATA]
stack37471636.py: error: not a bar