Pytest
is a very complete test framework for Python. I like how you can write a basic unittest.TestCase
and the py.test
test runner command will inject all its magic at runtime, without you having to directly import
anything: awesome separation of concerns.
This modularity comes at a cost though: py.test
actually preprocess tests before running them, by parsing their AST tree and replacing assert
calls by custom exceptions "manually "raised. The final compiled .pyc
binaries are then cached in a __pycache__/
directory.
My curiosity got aroused by this blog post from 2011 : wouldn't that be nice to peek into this process and check what the modified code look like exactly ?
I considered 2 solutions:
- either decompile the cached
.pyc
files, but I couldn't feed them touncompyle2
norpycdc
without raising bytecode format errors. - take a glance at Pytest code base and find a way to invoke its custom AST-parsing method, then "AST-unparse" the resulting AST tree instead of compiling it down to bytecode.
This second solution revealed to be very easy to implement. I hesitated for a moment between two good-looking AST-unparser, namely astor
and astunparse
, and ended up with the following code:
from _pytest.assertion.rewrite import rewrite_asserts
import ast, astunparse, sys
with open(sys.argv[1], 'r') as open_file:
ast_tree = ast.parse(open_file.read())
rewrite_asserts(ast_tree)
print astunparse.unparse(ast_tree)
And that's it ! To test it, just write a dummy stupid_test.py file with:
def dummy_test():
assert False
And then python pytest_rewrite.py stupid_test.py
:
import __builtin__ as @py_builtins
import _pytest.assertion.rewrite as @pytest_ar
def dummy_test():
if (not False):
@py_format1 = (('' + 'assert %(py0)s') % {'py0': (@pytest_ar._saferepr(False) if (('False' in @py_builtins.locals()) or @pytest_ar._should_repr_global_name(False)) else 'False')})
raise AssertionError(@pytest_ar._format_explanation(@py_format1))
Now, I want to conclude on a more nuanced tone: not everything is perfect in the Pytest world, and I have a few pain points to mention:
- Pytest terminal reports are rendered character by character, making it impossible to write log messages to stdout without messing everything
- Pytest wraps every module/class/object in your tests into custom wrappers and use generic callback hooks on at least 3 invocation levels : I had to scratch my head for some time to debug minor errors stacktraces and hack around it
- Pytest code base is very dense and not always following PEP code standards, making it very difficult to understand and contribute
- finally, I have a last minor complaint: when using
pytest-xdist
to parallelize tests, you cannot write to stdout. I guess it'd be difficult to collect the standard outputs of every process spawned bypytest-xdist
, but it's still a PITA.