Skip to content

hookwrappers can't modify the in-flight exception without skipping later hookwrapper teardowns #244

Closed
@oremanj

Description

@oremanj

Suppose you want to write a pytest plugin that transforms some exceptions in a test into different exceptions. As I understand it, this can be done with a hookwrapper, something like this:

@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call():
    outcome = yield
    if isinstance(outcome.excinfo[1], ValueError):
        raise KeyError("example") from outcome.excinfo[1]

And this works great, as long as you only have one such wrapper. But if you have two, then raising an exception in the "inner" one will skip the teardown portion of the "outer" one. A complete example:

$ head *.py
==> conftest.py <==
pytest_plugins = ["plugin1", "plugin2"]

==> plugin1.py <==
import pytest

@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_pyfunc_call():
    print("before 1")
    yield
    print("after 1")

==> plugin2.py <==
import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call():
    print("before 2")
    result = yield
    print("after 2")
    if isinstance(result.excinfo[1], ValueError):
        raise KeyError("added in 2") from result.excinfo[1]

==> test.py <==
def test_foo():
    raise ValueError("in test")

$ pytest test.py
[... snip some frames from pytest/pluggy internals ...]
    def test_foo():
>       raise ValueError("in test")
E       ValueError: in test

t.py:2: ValueError

The above exception was the direct cause of the following exception:

    @pytest.hookimpl(hookwrapper=True)
    def pytest_pyfunc_call():
        print("before 2")
        result = yield
        print("after 2")
        if isinstance(result.excinfo[1], ValueError):
>           raise KeyError("added in 2") from result.excinfo[1]
E           KeyError: 'added in 2'

plugin2.py:9: KeyError
---- Captured stdout call ----
before 1
before 2
after 2
==== 1 failed in 0.08 seconds ====

Notably missing from the "Captured stdout call": after 1.

I encountered this when trying to use pytest_runtest_call as the wrapper; _pytest.logging uses a pytest_runtest_call wrapper to add the captured logging to the test report, so logging would mysteriously fail to be captured.

A workaround is possible by assigning to result._excinfo instead of re-raising, but it would be nice to have a supported mechanism for this so as not to need to reach into internals.

This issue was seen with pytest 4.3.1 and pluggy 0.9.0 on Linux. I'm not immediately able to test with a newer version, but the relevant code (pluggy.callers._multicall) doesn't seem to have changed recently.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions