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
?
Example program
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 =====================
Desired result
$ 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
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-relatedpytest
code (for initialization purposes) and likely break on package updates. Best bet would be rewriting crucial parts, usingpytest
code as an example.Notes
Basically, the proof-of-concept code below does three things:
Replace the default
sys.excepthook
with the custom one: this is necessary to alter the default traceback formatting. Example:will output:
Instead of
hello world
, the formatted exception info will be printed. We useExceptionInfo.getrepr()
for that.To access the additional info in asserts,
pytest
rewrites theassert
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 toConfig
object, also I noticed some module imports to cause problems (I guess it doesn't fail withpytest
only because the modules are already imported when the hook is registered; will try to write a test that reproduces the issue on apytest
run and create a new issue). I would thus suggest to write a custom import hook that invokes theAssertionRewriter
. This AST tree walker class is the essential part in assertion rewriting, while theAssertionRewritingHook
is not that important.Code
hooks.py
main.py
After calling
hooks.install_hooks()
,main.py
will have modified traceback printing. Every module imported afterinstall_hooks()
call will have asserts rewritten on import.pytest_assert.py
Example output
Summarizing
I would go with writing an own version of
AssertionRewritingHook
, without the whole non-relatedpytest
stuff. TheAssertionRewriter
however looks pretty much reusable; although it requires aConfig
instance, it is only used for warning printing and can be left toNone
.Once you have that, write your own function that formats the exception properly, replace
sys.excepthook
and you're done.