免责声明
虽然肯定有办法重用pytest
以所需格式打印回溯的代码,您需要使用的东西不是公共 API 的一部分,因此生成的解决方案将太脆弱,需要调用不相关的pytest
代码(用于初始化目的)并且可能在包更新时中断。最好的选择是重写关键部分,使用pytest
代码为例。
Notes
基本上,下面的概念验证代码做了三件事:
-
替换默认值sys.excepthook使用自定义的:这是改变默认回溯格式所必需的。例子:
import sys
orig_hook = sys.excepthook
def myhook(*args):
orig_hook(*args)
print('hello world')
if __name__ == '__main__':
sys.excepthook = myhook
raise ValueError()
将输出:
Traceback (most recent call last):
File "example.py", line 11, in <module>
raise ValueError()
ValueError
hello world
代替hello world
,将打印格式化的异常信息。我们用ExceptionInfo.getrepr()为了那个原因。
要访问断言中的附加信息,pytest
重写了assert
语句(你可以得到一些关于它们重写后的样子的粗略信息这篇旧文章)。为了实现这一目标,pytest
注册一个自定义导入钩子,如指定的PEP 302。钩子是最有问题的部分,因为它与Config对象,我还注意到一些模块导入会导致问题(我猜它不会失败pytest
只是因为在注册钩子时模块已经导入;将尝试编写一个测试来重现该问题pytest
运行并创建一个新问题)。因此,我建议编写一个自定义导入钩子来调用AssertionRewriter。这个 AST 树遍历器类是断言重写中的重要部分,而AssertionRewritingHook
没那么重要。
Code
so-51839452
├── hooks.py
├── main.py
└── pytest_assert.py
hooks.py
import sys
from pluggy import PluginManager
import _pytest.assertion.rewrite
from _pytest._code.code import ExceptionInfo
from _pytest.config import Config, PytestPluginManager
orig_excepthook = sys.excepthook
def _custom_excepthook(type, value, tb):
orig_excepthook(type, value, tb) # this is the original traceback printed
# preparations for creation of pytest's exception info
tb = tb.tb_next # Skip *this* frame
sys.last_type = type
sys.last_value = value
sys.last_traceback = tb
info = ExceptionInfo(tup=(type, value, tb, ))
# some of these params are configurable via pytest.ini
# different params combination generates different output
# e.g. style can be one of long|short|no|native
params = {'funcargs': True, 'abspath': False, 'showlocals': False,
'style': 'long', 'tbfilter': False, 'truncate_locals': True}
print('------------------------------------')
print(info.getrepr(**params)) # this is the exception info formatted
del type, value, tb # get rid of these in this frame
def _install_excepthook():
sys.excepthook = _custom_excepthook
def _install_pytest_assertion_rewrite():
# create minimal config stub so AssertionRewritingHook is happy
pluginmanager = PytestPluginManager()
config = Config(pluginmanager)
config._parser._inidict['python_files'] = ('', '', [''])
config._inicache = {'python_files': None, 'python_functions': None}
config.inicfg = {}
# these modules _have_ to be imported, or AssertionRewritingHook will complain
import py._builtin
import py._path.local
import py._io.saferepr
# call hook registration
_pytest.assertion.install_importhook(config)
# convenience function
def install_hooks():
_install_excepthook()
_install_pytest_assertion_rewrite()
main.py
打电话后hooks.install_hooks()
, main.py
将修改回溯打印。之后导入的每个模块install_hooks()
调用将在导入时重写断言。
from hooks import install_hooks
install_hooks()
import pytest_assert
if __name__ == '__main__':
pytest_assert.test_foo()
pytest_assert.py
def test_foo():
foo = 12
bar = 42
assert foo == bar
输出示例
$ python main.py
Traceback (most recent call last):
File "main.py", line 9, in <module>
pytest_assert.test_foo()
File "/Users/hoefling/projects/private/stackoverflow/so-51839452/pytest_assert.py", line 4, in test_foo
assert foo == bar
AssertionError
------------------------------------
def test_foo():
foo = 12
bar = 42
> assert foo == bar
E AssertionError
pytest_assert.py:4: AssertionError
总结
我会写一个自己的版本AssertionRewritingHook
,没有全部不相关的pytest
东西。这AssertionRewriter
然而看起来几乎可以重复使用;虽然它需要一个Config
例如,它仅用于警告打印,可以留作None
.
一旦你有了这个,编写你自己的函数来正确格式化异常,替换sys.excepthook
你就完成了。