Skip to content

Show nicer tracebacks for BaseExceptionGroup from fixture setup/teardown #12255

@nicoddemus

Description

@nicoddemus

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 #12250.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: enhancementnew feature or API change, should be merged into features branch

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions