以 python 方式终止带有有用消息的 Python 命令行脚本

Terminating a Python command-line script with helpful messages, pythonically

如果要处理的数据不完全正确,我正在编写的脚本应该返回到 shell 提示符并显示一条有用的消息。用户应该修复标记的问题,直到脚本满意并且不再退出并显示错误消息。我正在使用 TTD 开发脚本,所以我在编写函数之前写了一个 pytest 测试。

most heavily up-voted answer here 建议通过调用 sys.exit 或提高 SystemExit 来编辑脚本。

函数:

def istext(file_to_test):
    try:
        open(file_to_test).read(512)
    except UnicodeDecodeError:
        sys.exit('File {} must be encoded in UTF-8 (Unicode); try converting.'.format(file_to_test))

通过此测试(其中 _non-text.png 是 PNG 文件,即未以 UTF-8 编码):

def test_istext():
    with pytest.raises(SystemExit):
        istext('_non-text.png')

但是,脚本会继续 运行,并执行位于 try/except 块之后的语句。

我希望脚本每次都完全退出,这样用户就可以调试数据直到正确为止,脚本会做它应该做的事情(就是处理一个全是UTF-8的目录文本文件,而不是 PNG、JPG、PPTX... 文件)。

也试过:

下面的代码也通过了上面的测试,引发了一个 SystemExit 的子 class 异常,但它也没有退出脚本:

def istext(file_to_test):
    class NotUTF8Error(SystemExit): pass
    try:
        open(file_to_test).read(512)
    except UnicodeDecodeError:
        raise NotUTF8Error('File {} must be UTF-8.'.format(file_to_test))

try...except 块用于捕获错误并在内部进行处理。您要做的是重新引发错误。

def istext(file_to_test):
try:
    open(file_to_test).read(512)
except UnicodeDecodeError:
    print(('File {} must be encoded in UTF-8 (Unicode); try converting.'.format(file_to_test)))
    raise

这将打印您的消息,然后自动重新提出您发现的错误。

您可能还想更改错误类型,而不是仅仅重新引发旧错误。对于这种情况,您可以进一步指定加薪,例如:

raise NameError('I'm the shown error message')

您可以使用 raise Exception from exception 语法:

class MyException(SystemExit):
    pass


def istext(file_to_test):
    try:
        open(file_to_test).read(512)
    except UnicodeDecodeError as exception:
        raise MyException(f'File {file_to_test} must be encoded in UTF-8 (Unicode); try converting.') \
            from exception 

在这种情况下,您不会更改原始错误消息并添加您自己的消息。

您的问题不是如何退出程序(sys.exit() 可以正常工作)。你的问题是你的测试场景没有引发 UnicodeDecodeError.

这是您示例的简化版本。它按预期工作:

import pytest
import sys

def foo(n):
    try:
        1/n
    except ZeroDivisionError as e:
        sys.exit('blah')

def test_foo():
    # Assertion passes.
    with pytest.raises(SystemExit):
        foo(0)
    # Assertion fails: "DID NOT RAISE <type 'exceptions.SystemExit'>"
    with pytest.raises(SystemExit):
        foo(9)

在您的代码中添加一些诊断打印以了解更多信息。例如:

def istext(file_to_test):
    try:
        content = open(file_to_test).read(512)
        # If you see this, no error occurred. Maybe your source
        # file needs different content to trigger UnicodeDecodeError.
        print('CONTENT len()', len(content))
    except UnicodeDecodeError:
        sys.exit('blah')
    except Exception as e:
        # Maybe some other type of error should also be handled?
        ...

最后,工作与@ADR 提出的相似,有一个区别:我无法使上面显示的格式化字符串语法正常工作 (f'File {file_to_test} must...'),我也找不到f 字符串前缀的文档。

那么,对于(重命名的)函数,我的解决方案稍微不太优雅:

def is_utf8(file):
    class NotUTF8Error(SystemExit): pass
    try:
        open(file).read(512)
    except UnicodeDecodeError as e:
        raise NotUTF8Error('File {} not UTF-8: convert or delete, then retry.'.format(file)) from e

通过 pytest:

def test_is_utf81():
    with pytest.raises(SystemExit):
        is_utf8('/Users/tbaker/github/tombaker/mklists/mklists/_non-text.png')