pytest
pytest copied to clipboard
Show nicer tracebacks for BaseExceptionGroup from fixture setup/teardown
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.