check50 icon indicating copy to clipboard operation
check50 copied to clipboard

Use pytest's plain asserts

Open Jelleas opened this issue 5 years ago • 2 comments

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?

Jelleas avatar Jul 14 '20 13:07 Jelleas

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.

cmlsharp avatar Jul 14 '20 18:07 cmlsharp

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])

Jelleas avatar Jul 14 '20 21:07 Jelleas