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

43

在使用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
4个回答

48
argparse.FileType是一个类型工厂类,可以打开文件,在此过程中如果文件不存在或无法创建,则会引发错误。您可以查看其代码以了解如何创建自己的类(或函数)来测试输入。
参数type是可调用的(函数等),它接受一个字符串,根据需要对其进行测试,并将其(如有必要)转换为要保存到args命名空间的值类型。因此,它可以执行任何所需的测试。如果type引发错误,则解析器将创建一个错误消息(和用法)并退出。
现在,是否在该位置进行测试取决于您的情况。有时,使用FileType打开文件很好,但然后您必须自行关闭它,或者等待程序结束。您不能在with open(filename) as f:上下文中使用该打开的文件。同样的情况也适用于您的数据库。在复杂的程序中,您可能不希望立即打开或创建文件。
我为Python的bug/issue编写了一个变体的FileType,它创建了一个上下文对象,可以在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')

我认为,无论你是在type还是Action中实现这样的测试,都不会产生太大影响。我认为type更简单,更符合开发者的意图。


7
好的回答!我建议使用argparse.ArgumentTypeError(message)函数,以便在控制台上打印“message”。 - tabata
请查看我的上面的答案,这是在操作方法中进行的修改。你和他的操作都会在控制台中引起回溯,而使用操作方法返回parser.error(message)则会给出适当的响应。 - miigotu

25

当然可以!您只需将自定义操作指定为一个类,并覆盖__call__(..)方法。 链接到文档。

就像这样:

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(或类似的内容)。


9

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

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

2

这是一个更好的版本:https://dev59.com/6FoU5IYBdhLWcg3wamga#37471954。我不能在一行注释中很好地解释区别。引发ValueError将导致终端中的回溯。您应该调用parser.error并传递消息,而不是引发ValueError,如下所示:

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")

我不确定您在控制台中的回溯是什么意思?您是在运行ipython或其他交互式控制台吗?引发ArgumentErrorArgumentTypeError是预期的argparse操作,然后通过error方法显示。在解析后引发错误时,我使用parser.error,但从未尝试过与Action类(或type)一起使用。 - hpaulj
我已经添加了一个答案,展示了提议的替代方案。 - hpaulj
我可以在有时间的时候展示异常,但现在我只是在pyvenv中使用普通的cpython。我将在Python 3.11中进行测试,也会稍后测试3.7-3.10版本。这个异常和你的ValueError例子类似。由于Action提供了解析器实例,我认为最好使用它。 - miigotu

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