diff --git a/changelog/12175.bugfix.rst b/changelog/12175.bugfix.rst new file mode 100644 index 00000000000..140e602d3c4 --- /dev/null +++ b/changelog/12175.bugfix.rst @@ -0,0 +1 @@ +Fixed :meth:`ExceptionInfo.exconly(tryshort=True) ` not stripping the ``AssertionError`` prefix when the :class:`ExceptionInfo` was created via :meth:`ExceptionInfo.for_later` (e.g. when using :func:`pytest.raises`). diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 4cf99a77340..e7ddde37f31 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -545,6 +545,20 @@ def from_exception( exc_info = (type(exception), exception, exception.__traceback__) return cls.from_exc_info(exc_info, exprinfo) + @classmethod + def _compute_striptext( + cls, + exc_info: tuple[type[E], E, TracebackType], + ) -> str: + """Determine if AssertionError prefix should be stripped from output.""" + if isinstance(exc_info[1], AssertionError): + exprinfo = getattr(exc_info[1], "msg", None) + if exprinfo is None: + exprinfo = saferepr(exc_info[1]) + if exprinfo and exprinfo.startswith(cls._assert_start_repr): + return "AssertionError: " + return "" + @classmethod def from_exc_info( cls, @@ -553,12 +567,8 @@ def from_exc_info( ) -> ExceptionInfo[E]: """Like :func:`from_exception`, but using old-style exc_info tuple.""" _striptext = "" - if exprinfo is None and isinstance(exc_info[1], AssertionError): - exprinfo = getattr(exc_info[1], "msg", None) - if exprinfo is None: - exprinfo = saferepr(exc_info[1]) - if exprinfo and exprinfo.startswith(cls._assert_start_repr): - _striptext = "AssertionError: " + if exprinfo is None: + _striptext = cls._compute_striptext(exc_info) return cls(exc_info, _striptext, _ispytest=True) @@ -591,6 +601,7 @@ def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None: """Fill an unfilled ExceptionInfo created with ``for_later()``.""" assert self._excinfo is None, "ExceptionInfo was already filled" self._excinfo = exc_info + self._striptext = self._compute_striptext(exc_info) @property def type(self) -> type[E]: diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 70499fec893..1c3453231b9 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -365,6 +365,51 @@ def test_excinfo_for_later() -> None: assert "for raises" in str(e) +def test_excinfo_for_later_strips_assertion(pytester: Pytester) -> None: + """ExceptionInfo created via for_later() should strip AssertionError prefix + with exconly(tryshort=True) for rewritten assertions (#12175).""" + pytester.makepyfile( + """ + import pytest + + def test_tryshort(): + with pytest.raises(AssertionError) as exc_info: + assert 1 == 2 + # tryshort should strip 'AssertionError: ' from rewritten assertions + result = exc_info.exconly(tryshort=True) + assert not result.startswith("AssertionError"), ( + f"Expected stripped prefix, got: {result!r}" + ) + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +def test_excinfo_for_later_no_strip_non_assertion() -> None: + """fill_unfilled() should not strip prefix for non-AssertionError exceptions.""" + excinfo: ExceptionInfo[ValueError] = ExceptionInfo.for_later() + try: + raise ValueError("test error") + except ValueError: + excinfo.fill_unfilled(sys.exc_info()) # type: ignore[arg-type] + assert excinfo.exconly(tryshort=True).startswith("ValueError") + + +def test_excinfo_for_later_strips_manual_assertion() -> None: + """fill_unfilled() handles AssertionError with explicit .msg attribute.""" + excinfo: ExceptionInfo[AssertionError] = ExceptionInfo.for_later() + try: + err = AssertionError("manual error") + err.msg = "assert something" # type: ignore[attr-defined] + raise err + except AssertionError: + excinfo.fill_unfilled(sys.exc_info()) # type: ignore[arg-type] + # Manual .msg that doesn't match _assert_start_repr should not strip + result = excinfo.exconly(tryshort=True) + assert result.startswith("AssertionError") + + def test_excinfo_errisinstance(): excinfo = pytest.raises(ValueError, h) assert excinfo.errisinstance(ValueError)