Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
fixes:
- |
When ``reraise=True`` and ``TryAgain`` is raised from within an ``except``
block, the underlying exception that triggered the retry is now reraised
once attempts are exhausted, instead of the opaque ``TryAgain`` sentinel.
A bare ``TryAgain`` raised without an active exception keeps reraising
``TryAgain`` as before.
8 changes: 8 additions & 0 deletions tenacity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ def __init__(self, last_attempt: "Future") -> None:

def reraise(self) -> t.NoReturn:
if self.last_attempt.failed:
exc = self.last_attempt.exception()
# When the user explicitly raises TryAgain (typically from within
# an "except" block), surface the underlying exception that caused
# the retry rather than the opaque TryAgain sentinel.
if isinstance(exc, TryAgain):
cause = exc.__cause__ or exc.__context__
if cause is not None:
raise cause.with_traceback(cause.__traceback__) from None
raise self.last_attempt.result()
raise self

Expand Down
23 changes: 23 additions & 0 deletions tests/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,29 @@ class CustomError(Exception):
else:
raise Exception

@asynctest
async def test_reraise_try_again_with_cause(self) -> None:
# When TryAgain is raised from within an "except" block, reraise=True
# should surface the underlying exception rather than TryAgain itself.
class UnderlyingError(Exception):
pass

async def _test() -> None:
try:
raise UnderlyingError("boom")
except UnderlyingError:
# Implicit chaining via __context__ is exactly what we test.
raise tenacity.TryAgain # noqa: B904

retrying = tasyncio.AsyncRetrying(
stop=stop_after_attempt(2),
retry=tenacity.retry_never,
reraise=True,
)
with pytest.raises(UnderlyingError):
await retrying(_test)
Comment thread
jd marked this conversation as resolved.
self.assertEqual(2, retrying.statistics["attempt_number"])

@asynctest
async def test_sleeps(self) -> None:
start = current_time_ms()
Expand Down
37 changes: 37 additions & 0 deletions tests/test_tenacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,43 @@ def _r() -> None:
self.assertRaises(tenacity.RetryError, r, _r)
self.assertEqual(5, r.statistics["attempt_number"])

def test_retry_try_again_with_cause_reraise(self) -> None:
# When TryAgain is raised from within an "except" block, reraise=True
# should surface the underlying exception rather than TryAgain itself.
class UnderlyingError(Exception):
pass

def _r() -> None:
try:
raise UnderlyingError("boom")
except UnderlyingError:
# Implicit chaining via __context__ is exactly what we test.
raise tenacity.TryAgain # noqa: B904

r = Retrying(
stop=tenacity.stop_after_attempt(5),
retry=tenacity.retry_never,
reraise=True,
)
self.assertRaises(UnderlyingError, r, _r)
self.assertEqual(5, r.statistics["attempt_number"])

def test_retry_try_again_from_cause_reraise(self) -> None:
# An explicit "raise TryAgain from exc" should also be unwrapped.
class UnderlyingError(Exception):
pass

def _r() -> None:
raise tenacity.TryAgain from UnderlyingError("boom")

r = Retrying(
stop=tenacity.stop_after_attempt(5),
retry=tenacity.retry_never,
reraise=True,
)
self.assertRaises(UnderlyingError, r, _r)
Comment thread
jd marked this conversation as resolved.
self.assertEqual(5, r.statistics["attempt_number"])

def test_retry_try_again_forever_reraise(self) -> None:
def _r() -> None:
raise tenacity.TryAgain
Expand Down