diff --git a/releasenotes/notes/tryagain-reraise-exception-chain-91c5c2f030803e8b.yaml b/releasenotes/notes/tryagain-reraise-exception-chain-91c5c2f030803e8b.yaml new file mode 100644 index 00000000..e1c03ed9 --- /dev/null +++ b/releasenotes/notes/tryagain-reraise-exception-chain-91c5c2f030803e8b.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + When ``reraise=True`` surfaces the exception that triggered a ``TryAgain``, + that exception's own ``__cause__`` and ``__context__`` are now kept, so the + traceback still shows what led to it. Previously they were overwritten as + if it had been raised with ``from None``. diff --git a/tenacity/__init__.py b/tenacity/__init__.py index 89eeca6e..72a97018 100644 --- a/tenacity/__init__.py +++ b/tenacity/__init__.py @@ -212,7 +212,10 @@ def reraise(self) -> t.NoReturn: if isinstance(exc, TryAgain): cause = exc.__cause__ or exc.__context__ if cause is not None: - raise cause.with_traceback(cause.__traceback__) from None + # No "from None": that would overwrite the exception's own + # __cause__ and suppress its __context__, hiding the + # chain that led to it. + raise cause.with_traceback(cause.__traceback__) raise self.last_attempt.result() raise self diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 21c4e936..1024bd6e 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -365,6 +365,25 @@ async def _test() -> None: await retrying(_test) self.assertEqual(2, retrying.statistics["attempt_number"]) + @asynctest + async def test_reraise_try_again_keeps_exception_chain(self) -> None: + root = OSError("disk unavailable") + + async def _test() -> None: + try: + try: + raise root + except OSError as e: + raise KeyError("config") from e + except KeyError: + raise tenacity.TryAgain # noqa: B904 + + retrying = tasyncio.AsyncRetrying(stop=stop_after_attempt(2), reraise=True) + with pytest.raises(KeyError) as raised: + await retrying(_test) + assert raised.value.__cause__ is root + assert raised.value.__context__ is root + @asynctest async def test_sleeps(self) -> None: start = current_time_ms() diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index 577720ec..2bfa8e60 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -975,6 +975,42 @@ def _r() -> None: self.assertRaises(UnderlyingError, r, _r) self.assertEqual(5, r.statistics["attempt_number"]) + def test_retry_try_again_reraise_keeps_exception_chain(self) -> None: + # Unwrapping TryAgain must not rewrite the chain of the exception it + # surfaces: its own cause and context still have to be reported. + root = OSError("disk unavailable") + + def _r() -> None: + try: + try: + raise root + except OSError as e: + raise KeyError("config") from e + except KeyError: + raise tenacity.TryAgain # noqa: B904 + + r = Retrying(stop=tenacity.stop_after_attempt(2), reraise=True) + with self.assertRaises(KeyError) as raised: + r(_r) + self.assertIs(raised.exception.__cause__, root) + self.assertIs(raised.exception.__context__, root) + + def test_retry_try_again_reraise_keeps_implicit_context(self) -> None: + def _r() -> None: + try: + try: + raise OSError("disk unavailable") + except OSError: + raise KeyError("config") # noqa: B904 + except KeyError: + raise tenacity.TryAgain # noqa: B904 + + r = Retrying(stop=tenacity.stop_after_attempt(2), reraise=True) + with self.assertRaises(KeyError) as raised: + r(_r) + self.assertIsInstance(raised.exception.__context__, OSError) + self.assertFalse(raised.exception.__suppress_context__) + def test_retry_try_again_forever_reraise(self) -> None: def _r() -> None: raise tenacity.TryAgain