我可以修补Python的断言以获取py.test提供的输出吗? [英] Can I patch Python's assert to get the output that py.test provides?

查看:104
本文介绍了我可以修补Python的断言以获取py.test提供的输出吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Pytest的断言失败输出比Python中的默认输出更具信息性和有用性.我想在正常运行Python程序时利用此功能,而不仅仅是在执行测试时使用.

Pytest's output for failed asserts is much more informative and useful than the default in Python. I would like to leverage this when normally running my Python program, not just when executing tests. Is there a way to, from within my script, overwrite Python's assert behavior to use pytest to print the stacktrace instead while still running my program as python script/pytest_assert.py?

def test_foo():
  foo = 12
  bar = 42
  assert foo == bar

if __name__ == '__main__':
  test_foo()

$ python script/pytest_assert.py

Traceback (most recent call last):
  File "script/pytest_assert.py", line 8, in <module>
    test_foo()
  File "script/pytest_assert.py", line 4, in test_foo
    assert foo == bar
AssertionError

$ pytest script/pytest_assert.py

======================== test session starts ========================
platform linux -- Python 3.5.3, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: /usr/local/google/home/danijar, inifile:
collected 1 item                                                    

script/pytest_assert.py F                                     [100%]

============================= FAILURES ==============================
_____________________________ test_foo ______________________________

    def test_foo():
      foo = 12
      bar = 42
>     assert foo == bar
E     assert 12 == 42

script/pytest_assert.py:4: AssertionError
===================== 1 failed in 0.02 seconds =====================

所需结果

$ python script/pytest_assert.py

Traceback (most recent call last):
  File "script/pytest_assert.py", line 8, in <module>
    test_foo()

    def test_foo():
      foo = 12
      bar = 42
>     assert foo == bar
E     assert 12 == 42

script/pytest_assert.py:4: AssertionError

进度更新

我最接近的是这个,但它仅适用于该函数中的断言并向跟踪发送垃圾邮件:

Progress update

The closest I've got is this but it only works for asserts within that one function and spams the trace:

import ast
import inspect

from _pytest import assertion


def test_foo():
  foo = []
  foo.append(13)
  foo = foo[-1]
  bar = 42
  assert foo == bar, 'message'


if __name__ == '__main__':
  tree = ast.parse(inspect.getsource(test_foo))
  assertion.rewrite.rewrite_asserts(tree)
  code = compile(tree, '<name>', 'exec')
  ns = {}
  exec(code, ns)
  ns[test_foo.__name__]()

$ python script/pytest_assert.py
Traceback (most recent call last):
  File "script/pytest_assert.py", line 21, in <module>
    ns[test_foo.__name__]()
  File "<name>", line 6, in test_foo
AssertionError: message
assert 13 == 42

推荐答案

免责声明

尽管肯定有一种方法可以重用pytest代码以所需格式打印回溯,但是您需要使用的东西不是公共API的一部分,因此生成的解决方案将太脆弱,需要调用非相关的pytest代码(出于初始化目的),可能会在软件包更新时中断.最好的选择是重写pytest代码作为示例.

Disclaimer

Although there is surely a way to reuse pytest code to print the traceback in the desired format, stuff you need to use is not part of public API, so the resulting solution will be too fragile, require invocation of non-related pytest code (for initialization purposes) and likely break on package updates. Best bet would be rewriting crucial parts, using pytest code as an example.

基本上,下面的概念验证代码可以完成三件事:

Basically, the proof-of-concept code below does three things:

  1. 替换默认的 sys.excepthook 使用自定义选项:这是更改默认回溯格式的必要条件.示例:

  1. Replace the default sys.excepthook with the custom one: this is necessary to alter the default traceback formatting. Example:

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() .

  • Instead of hello world, the formatted exception info will be printed. We use ExceptionInfo.getrepr() for that.

    要访问断言中的其他信息,pytest重写assert语句(在> PEP中指定的自定义导入挂钩302 .挂钩是最有问题的部分,因为它与 Config 对象,我也注意到某些模块导入会引起问题(我想它不会因pytest而失败,仅是因为注册钩子时模块已经被导入;将尝试编写一个在a上重现该问题的测试pytest运行并创建新的问题).因此,我建议编写一个自定义导入挂钩,以调用 AssertionRewriter .这个AST树沃克类是断言重写中必不可少的部分,而AssertionRewritingHook并不是那么重要.

    To access the additional info in asserts, pytest rewrites the assert statements (you can get some rough info about how they look like after rewrite in this old article). To achieve that, pytest registers a custom import hook as specified in PEP 302. The hook is the most problematic part as it is tightly coupled to Config object, also I noticed some module imports to cause problems (I guess it doesn't fail with pytest only because the modules are already imported when the hook is registered; will try to write a test that reproduces the issue on a pytest run and create a new issue). I would thus suggest to write a custom import hook that invokes the AssertionRewriter. This AST tree walker class is the essential part in assertion rewriting, while the AssertionRewritingHook is not that important.

    代码

    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()调用之后导入的每个模块都将在导入时重写断言.

    main.py

    After calling hooks.install_hooks(), main.py will have modified traceback printing. Every module imported after install_hooks() call will have asserts rewritten on import.

    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.

    Summarizing

    I would go with writing an own version of AssertionRewritingHook, without the whole non-related pytest stuff. The AssertionRewriter however looks pretty much reusable; although it requires a Config instance, it is only used for warning printing and can be left to None.

    一旦有了这些,就编写自己的函数以正确格式化异常,替换sys.excepthook即可.

    Once you have that, write your own function that formats the exception properly, replace sys.excepthook and you're done.

    这篇关于我可以修补Python的断言以获取py.test提供的输出吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆