[pytest-dev] Custom reporting for asserts without comparison operators?

Shawn Brown 03sjbrown at gmail.com
Wed Mar 21 22:39:09 EDT 2018


Ah. It's good to see that this has been thought about before.

My motivation for asking this question was to perform my due diligence and
make sure I wasn't missing something before moving ahead. My immediate need
is handled by using assert_myfunc() to raise its own error internally--same
as Floris' example. Though, it's not ideal.

I know my examples have been vague as I've stripped the specifics of my
project to focus the question specifically on pytest's behavior and I
greatly appreciate everyone who is giving some thought to this.

As Ronny mentioned, I'm sure it's possible to address this without
user-facing AST manipulation. But I'm not familiar enough with the code
base to see where I can best hack on the representations. However, I do
have a working AST-based demonstration (below). This uses a fragile
monkey-patch that is just asking for trouble so please take this for the
experimental hack it is...


FILE "conftest.py":

    import ast
    import _pytest

    def my_ast_prerewrite_hook(ast_assert):
        """Modifies AST of certain asserts before pytest-rewriting."""
        # Demo AST-tree manipulation (actual implemenation
        # would need to be more careful than this).
        if (isinstance(ast_assert.test, ast.Call)
                and isinstance(ast_assert.test.func, ast.Name)
                and ast_assert.test.func.id == 'myfunc'):

            ast_assert.test.func = ast.Name('assert_myfunc', ast.Load())

        return ast_assert

    # UNDESIRABLE MONKEY PATCHING!!!
    class ModifiedRewriter(_pytest.assertion.rewrite.AssertionRewriter):
        def visit_Assert(self, assert_):
            assert_ = my_ast_prerewrite_hook(assert_)  # <- PRE-REWRITE HOOK
            return super(ModifiedRewriter, self).visit_Assert(assert_)

    def rewrite_asserts(mod, module_path=None, config=None):
        ModifiedRewriter(module_path, config).run(mod)

    _pytest.assertion.rewrite.rewrite_asserts = rewrite_asserts


FILE "test_ast_hook_approach.py":

    import pytest

    # Test helpers.
    def myfunc(x):
        return x == 42

    def assert_myfunc(x):
        __tracebackhide__ = True
        if not myfunc(x):
            msg = 'custom report\nmulti-line output\nmyfunc({0}) failed'
            raise AssertionError(msg.format(x))
        return True

    # Test cases.
    def test_1passing():
        assert myfunc(42)

    def test_2passing():
        assert myfunc(41) is False

    def test_3passing():
        with pytest.raises(AssertionError) as excinfo:
            assert myfunc(41)
        assert 'custom report' in str(excinfo.value)

    def test_4failing():
        assert myfunc(41)


Running the above test gives 3 passing cases and 1 failing case (which uses
the custom report). Also, test_2passing() checks for "is False" instead of
just "== False" which I think would be wonderful to support as it removes
all caveats for the user (so users get a real False when they expect False,
instead of a Falsey alternative). Also, if I were going to use AST
manipulation like this, I would probably reference assert_myfunc() by
attaching it as a private attribute to myfunc() itself -- and then
reference it with ast.Attribute() node instead of an ast.Name(). But again,
solving this without AST manipulation could be better in many ways.

--Shawn


On Mon, Mar 19, 2018 at 1:59 PM, Ronny Pfannschmidt <
ich at ronnypfannschmidt.de> wrote:

> hi everyone,
>
> this is just about single value assertion helpers
>
> i logged an feature request about that a few year back
> see https://github.com/pytest-dev/pytest/issues/95 -
>
> so basically this use-case was known since 2011 ^^ and doesn't require
> ast rewriting lice macros,
> just proper engineering of the representation and handling of single
> values in the assertion rewriter.
>
> -- Ronny
>
>
> Am 19.03.2018 um 15:13 schrieb holger krekel:
> > On Mon, Mar 19, 2018 at 15:03 +0100, Floris Bruynooghe wrote:
> >> On Sun, Mar 18 2018, Shawn Brown wrote:
> >>> Unfortunately, this does not solve my usecase. I'm trying to handle
> cases
> >>> where the following statement would pass:
> >>>
> >>>     assert myfunc(failing_input) == False
> >>>
> >>> But where this next statement would fail using my custom report:
> >>>
> >>>     assert myfunc(failing_input)
> >>>
> >>> Calling myfunc() needs to return True or False (or at least Truthy or
> >>> Falsy)--this is locked-in behavior.
> >> I'm not sure if this is compatible with Python's semantics really.  If I
> >> understand correctly you're asking for a full-on macro implementation on
> >> Python or something.  Which in theory you could do with an AST
> >> NodeVisitor, but really Python isn't made for this -- sounds like you'd
> >> enjoy lisp! ;-)
> >>
> >> The best thing I can suggest is to make use of the::
> >>
> >>    assert myfunc(failing_input), repr(myfunc(failing_input()))
> > i wonder if one could try to rewrite the ast for "assert myfunc(x)" to
> > "assert __pytest_funcrepr_helper(myfunc(x), 'myfunc(x)')" with
> something like:
> >
> >     class __pytest_funcrepr_helper:
> >         def __init__(self, val, source):
> >             self.val = val
> >             self.source = source
> >         def __bool__(self):
> >             return bool(self.val)
> >         def __repr__(self):
> >             return "{!r} returned non-true {!r}".format(self.source,
> self.val)
> >
> > but maybe i am not grasping all details involved. It's been a while since
> > i looked into ast-rewriting ...
> >
> > holger
> >
> >
> >> functionality to also get a custom error message.  Here your myfunc()
> >> whould have to return some object which both implements __bool__ as well
> >> as __repr__ I guess.
> >>
> >> Maybe there's a feature request in here for something like this::
> >>
> >>    class Foo:
> >>        def __bool__(self):
> >>            return False
> >>
> >>        def __repr__(self):
> >>            return 'multiline\nstring'
> >>
> >>    assert Foo()
> >>
> >> To actually show the repr in the error message, which it currently
> >> doesn't.  I'd like to know what other people think of such a feature
> >> though, and haven't thought through all the implications yet.  But I'm
> >> curious, would something like that solve your case?
> >>
> >> Cheers,
> >> Floris
> >> _______________________________________________
> >> pytest-dev mailing list
> >> pytest-dev at python.org
> >> https://mail.python.org/mailman/listinfo/pytest-dev
> > _______________________________________________
> > pytest-dev mailing list
> > pytest-dev at python.org
> > https://mail.python.org/mailman/listinfo/pytest-dev
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/pytest-dev/attachments/20180321/9f261c00/attachment-0001.html>


More information about the pytest-dev mailing list