pytest icon indicating copy to clipboard operation
pytest copied to clipboard

Show nicer tracebacks for BaseExceptionGroup from fixture setup/teardown

Open nicoddemus opened this issue 1 year ago • 0 comments

Currently if an exception is raised during the setup or teardown phase of a fixture, pytest handles that and shows a nice traceback:

import pytest

@pytest.fixture
def my_setup() -> None:
    raise ValueError("e1")    

def test(my_setup) -> None:
    pass
λ pytest bar.py --no-header
======================== test session starts ========================
collected 1 item

bar.py E                                                       [100%]

============================== ERRORS ===============================
______________________ ERROR at setup of test _______________________

    @pytest.fixture
    def my_setup() -> None:
>       raise ValueError("e1")
E       ValueError: e1

bar.py:6: ValueError
====================== short test summary info ======================
ERROR bar.py::test - ValueError: e1
========================= 1 error in 0.23s ==========================

However if we raise an ExceptionGroup, there is no special handling and the full traceback is shown:

import pytest


@pytest.fixture
def my_setup() -> None:
    raise ExceptionGroup("some errors", [ValueError("e1"), ValueError("e2")])


def test(my_setup) -> None:
    pass
λ pytest bar.py --no-header
======================== test session starts ========================
collected 1 item

bar.py E                                                       [100%]

============================== ERRORS ===============================
______________________ ERROR at setup of test _______________________
  + Exception Group Traceback (most recent call last):
  |   File "E:\projects\pytest\src\_pytest\runner.py", line 341, in from_call
  |     result: Optional[TResult] = func()
  |                                 ^^^^^^
  |   File "E:\projects\pytest\src\_pytest\runner.py", line 241, in <lambda>
  |     lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_hooks.py", line 513, in __call__
  |     return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec
  |     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall
  |     raise exception.with_traceback(exception.__traceback__)
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall
  |     teardown.throw(exception)  # type: ignore[union-attr]
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\unraisableexception.py", line 85, in pytest_runtest_setup
  |     yield from unraisable_exception_runtest_hook()
  |   File "E:\projects\pytest\src\_pytest\unraisableexception.py", line 65, in unraisable_exception_runtest_hook
  |     yield
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall
  |     teardown.throw(exception)  # type: ignore[union-attr]
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\logging.py", line 844, in pytest_runtest_setup
  |     yield from self._runtest_for(item, "setup")
  |   File "E:\projects\pytest\src\_pytest\logging.py", line 833, in _runtest_for
  |     yield
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall
  |     teardown.throw(exception)  # type: ignore[union-attr]
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\capture.py", line 873, in pytest_runtest_setup
  |     return (yield)
  |             ^^^^^
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall
  |     teardown.throw(exception)  # type: ignore[union-attr]
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\threadexception.py", line 82, in pytest_runtest_setup
  |     yield from thread_exception_runtest_hook()
  |   File "E:\projects\pytest\src\_pytest\threadexception.py", line 63, in thread_exception_runtest_hook
  |     yield
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_callers.py", line 103, in _multicall
  |     res = hook_impl.function(*args)
  |           ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\runner.py", line 159, in pytest_runtest_setup
  |     item.session._setupstate.setup(item)
  |   File "E:\projects\pytest\src\_pytest\runner.py", line 515, in setup
  |     raise exc
  |   File "E:\projects\pytest\src\_pytest\runner.py", line 512, in setup
  |     col.setup()
  |   File "E:\projects\pytest\src\_pytest\python.py", line 1630, in setup
  |     self._request._fillfixtures()
  |   File "E:\projects\pytest\src\_pytest\fixtures.py", line 695, in _fillfixtures
  |     item.funcargs[argname] = self.getfixturevalue(argname)
  |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\fixtures.py", line 552, in getfixturevalue
  |     fixturedef = self._get_active_fixturedef(argname)
  |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\fixtures.py", line 581, in _get_active_fixturedef
  |     self._compute_fixture_value(fixturedef)
  |   File "E:\projects\pytest\src\_pytest\fixtures.py", line 656, in _compute_fixture_value
  |     fixturedef.execute(request=subrequest)
  |   File "E:\projects\pytest\src\_pytest\fixtures.py", line 1086, in execute
  |     result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_hooks.py", line 513, in __call__
  |     return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec
  |     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall
  |     raise exception.with_traceback(exception.__traceback__)
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall
  |     teardown.throw(exception)  # type: ignore[union-attr]
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\setuponly.py", line 36, in pytest_fixture_setup
  |     return (yield)
  |             ^^^^^
  |   File "E:\projects\pytest\.env312\Lib\site-packages\pluggy\_callers.py", line 103, in _multicall
  |     res = hook_impl.function(*args)
  |           ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\fixtures.py", line 1135, in pytest_fixture_setup
  |     result = call_fixture_func(fixturefunc, request, kwargs)
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\projects\pytest\src\_pytest\fixtures.py", line 903, in call_fixture_func
  |     fixture_result = fixturefunc(**kwargs)
  |                      ^^^^^^^^^^^^^^^^^^^^^
  |   File "e:\projects\pytest\.tmp\bar.py", line 7, in my_setup
  |     raise ExceptionGroup("some errors", [ValueError("e1"), ValueError("e2")])
  | ExceptionGroup: some errors (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | ValueError: e1
    +---------------- 2 ----------------
    | ValueError: e2
    +------------------------------------
====================== short test summary info ======================
ERROR bar.py::test - ExceptionGroup: some errors (2 sub-exceptions)
========================= 1 error in 0.05s ==========================

I think pytest should be able to also handle ExceptionGroup and show a nicer traceback.

Noticed this while working on https://github.com/pytest-dev/pytest/pull/12250.

nicoddemus avatar Apr 26 '24 23:04 nicoddemus