diff --git a/changelog/504.bugfix.rst b/changelog/504.bugfix.rst new file mode 100644 index 00000000..17a08ff4 --- /dev/null +++ b/changelog/504.bugfix.rst @@ -0,0 +1 @@ +Fix a regression in pluggy 1.1.0 where using :func:`result.get_result() ` on the same failed :class:`~pluggy.Result` causes the exception's traceback to get longer and longer. diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index d01f925c..f4a2aced 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -136,7 +136,7 @@ def _multicall( _raise_wrapfail(teardown, "has second yield") # type: ignore[arg-type] if exception is not None: - raise exception.with_traceback(exception.__traceback__) + raise exception else: return result diff --git a/src/pluggy/_result.py b/src/pluggy/_result.py index f9a081c4..aa3912c0 100644 --- a/src/pluggy/_result.py +++ b/src/pluggy/_result.py @@ -28,7 +28,7 @@ class Result(Generic[ResultType]): """An object used to inspect and set the result in a :ref:`hook wrapper `.""" - __slots__ = ("_result", "_exception") + __slots__ = ("_result", "_exception", "_traceback") def __init__( self, @@ -38,6 +38,8 @@ def __init__( """:meta private:""" self._result = result self._exception = exception + # Exception __traceback__ is mutable, this keeps the original. + self._traceback = exception.__traceback__ if exception is not None else None @property def excinfo(self) -> _ExcInfo | None: @@ -46,7 +48,7 @@ def excinfo(self) -> _ExcInfo | None: if exc is None: return None else: - return (type(exc), exc, exc.__traceback__) + return (type(exc), exc, self._traceback) @property def exception(self) -> BaseException | None: @@ -75,6 +77,7 @@ def force_result(self, result: ResultType) -> None: """ self._result = result self._exception = None + self._traceback = None def force_exception(self, exception: BaseException) -> None: """Force the result to fail with ``exception``. @@ -85,6 +88,7 @@ def force_exception(self, exception: BaseException) -> None: """ self._result = None self._exception = exception + self._traceback = exception.__traceback__ if exception is not None else None def get_result(self) -> ResultType: """Get the result(s) for this hook call. @@ -94,10 +98,11 @@ def get_result(self) -> ResultType: """ __tracebackhide__ = True exc = self._exception + tb = self._traceback if exc is None: return cast(ResultType, self._result) else: - raise exc.with_traceback(exc.__traceback__) + raise exc.with_traceback(tb) # Historical name (pluggy<=1.2), kept for backward compatibility. diff --git a/testing/test_result.py b/testing/test_result.py new file mode 100644 index 00000000..c4a33920 --- /dev/null +++ b/testing/test_result.py @@ -0,0 +1,27 @@ +import traceback + +from pluggy import Result + + +def test_exceptions_traceback_doesnt_get_longer_and_longer() -> None: + def bad() -> None: + 1 / 0 + + result = Result.from_call(bad) + + try: + result.get_result() + except Exception as exc: + tb1 = traceback.extract_tb(exc.__traceback__) + + try: + result.get_result() + except Exception as exc: + tb2 = traceback.extract_tb(exc.__traceback__) + + try: + result.get_result() + except Exception as exc: + tb3 = traceback.extract_tb(exc.__traceback__) + + assert len(tb1) == len(tb2) == len(tb3)