Use pytest's plain asserts
pytest has built-in support for rather detailed assert messages. Allowing the testwriter to just use plain asserts:
assert 1 == 2
instead of
if 1 == 2:
raise check50.Failure("1 does not equal 2")
To do this pytest rewrites part of the code and as it turns out that functionality is reasonably separate from pytests' core. With inspiration from their own test suite:
echo "assert 1 == 2" > bar.py
import ast
from _pytest.assertion.rewrite import rewrite_asserts
def rewrite(src: str) -> ast.Module:
tree = ast.parse(src)
rewrite_asserts(tree, src.encode())
return tree
src = open("bar.py").read()
mod = rewrite(src)
code = compile(mod, "<test>", "exec")
namespace = {}
exec(code, namespace)
Just a thought, but perhaps it's worth exploring?
How do they generate the error messages?
We can always add "AssertionError" to the list of exceptions we catch in the check decorator and do a manual conversion fo check50.Failure. This wouldn't require rewriting the ast. The question is what the error message should be. I mean, you can include error messages in asserts like assert 1 == 2, "one does not equal two", but what if they don't include a human readable error message? Obviously this is technically possible now since someone could write raise check50.Failure(""), but this seems easier to do with the assert syntax.
I guess I'm not necessarily opposed to this, but I'm also not sure it adds much.
The default assert is rather useless. It provides nothing to produce a check50.Failure from:
try:
assert 1 == 2
except AssertionError as e:
print(e.args) # prints ()
pytest however puts the actual assertion into e.args, but also allows you to intercept this via _pytest.assertion.util._reprcompare (the implementation behind https://docs.pytest.org/en/latest/assert.html#defining-your-own-explanation-for-failed-assertions)
Allowing you to do the following:
import ast
from _pytest.assertion.rewrite import rewrite_asserts
from _pytest.assertion import util
def custom_assert_handler(left, right, op):
print(f"left: {left}, right: {right}, op:{op}")
util._reprcompare = custom_assert_handler
def rewrite(src: str) -> ast.Module:
tree = ast.parse(src)
rewrite_asserts(tree, src.encode())
return tree
src = open("bar.py").read()
mod = rewrite(src)
code = compile(mod, "<test>", "exec")
namespace = {}
exec(code, namespace)
Through this we could quite reasonably intercept different operators and types to provide student-friendly output, without putting all the burden on the checkwriter. Some low hanging fruit perhaps:
assert 1 == 2 => check50.Mismatch(1, 2)
assert 1 in [1,2,3] => check50.Missing(1, [1,2,3])