From 39d59ec580b57a2df7b52e261fd0787bbaf6bb24 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 17 Oct 2026 12:48:04 -0300 Subject: [PATCH] Do not chain the original mock error to wrapped assertion failures Since 747a1e6 the wrapped assertions re-raise their AssertionError inside the except block, so the error raised by the mock module became the context of the new one. pytest then showed the failure two or three times, including the mock frames the wrappers are meant to hide, making the output longer than with mock_traceback_monkeypatch=false. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.rst | 5 +++++ src/pytest_mock/plugin.py | 8 ++++++-- tests/test_pytest_mock.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7b3fe1e..5973d65 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Releases ======== +UNRELEASED +---------- + +* `#641 `_: Failures of mock assertions such as ``assert_called_with`` are again reported only once, instead of two or three times with the original error from the ``mock`` module chained before them. + 3.16.1 ------ diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 2bd0b8d..ece8610 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -565,7 +565,9 @@ def assert_wrapper( msg += "\n\npytest introspection follows:\n" + introspection e = AssertionError(msg) e._mock_introspection_applied = True # type:ignore[attr-defined] - raise e # noqa:TRY201 + # Suppress the context, otherwise pytest shows the original error + # (and the mock frames) chained before this one. + raise e from None def _unpack_expected_call(expect_call: Any) -> tuple[Any, Any] | None: @@ -651,7 +653,9 @@ def assert_has_calls_wrapper( msg += "\n\npytest introspection follows:\n" + introspection e = AssertionError(msg) e._mock_introspection_applied = True # type:ignore[attr-defined] - raise e # noqa:TRY201 + # Suppress the context, otherwise pytest shows the original error + # (and the mock frames) chained before this one. + raise e from None def wrap_assert_not_called(*args: Any, **kwargs: Any) -> None: diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index 984e816..89dc4cc 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -1106,6 +1106,35 @@ def test_foo(mocker): result = testdir.runpytest() assert "pytest introspection follows:" not in result.stdout.str() + @pytest.mark.parametrize( + "assertion", + [ + "stub.assert_called_with(2)", + # assert_called_once_with calls assert_called_with internally, + # so both wrappers are involved. + "stub.assert_called_once_with(2)", + "stub.assert_not_called()", + "stub.assert_has_calls([mocker.call(2)])", + ], + ) + def test_no_chained_tracebacks(self, testdir: Any, assertion: str) -> None: + """ + The wrapped assertions report the failure only once, without the + original exception from the mock module chained to it. + """ + testdir.makepyfile( + f""" + def test_foo(mocker): + stub = mocker.stub() + stub(1) + {assertion} + """ + ) + result = testdir.runpytest() + result.assert_outcomes(failed=1) + assert "During handling of the above exception" not in result.stdout.str() + assert "mock.py" not in result.stdout.str() + class TestAssertHasCalls: """``assert_has_calls``, including the shapes accepted as expected calls."""