Skip to content
Open
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
18 changes: 18 additions & 0 deletions tenacity/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ def _check(self, e: BaseException) -> bool:
return isinstance(e, self.exception_types)


def _signals_control_flow(e: BaseException) -> bool:
"""Whether ``e`` interrupts execution rather than reporting a failure.

``asyncio.CancelledError``, ``KeyboardInterrupt``, ``SystemExit`` and
``GeneratorExit`` derive from ``BaseException`` but not ``Exception``,
precisely so that blanket handlers leave them alone. Retrying one swallows
the cancellation, which breaks ``asyncio.wait_for`` and Ctrl-C.

``retry_if_exception_type`` defaults to ``Exception`` and so already skips
them; it stays the explicit opt-in for retrying one on purpose.
"""
return not isinstance(e, Exception)


class retry_if_not_exception_type(retry_if_exception):
"""Retries except an exception has been raised of one or more types."""

Expand All @@ -126,6 +140,8 @@ def __init__(
super().__init__(self._check)

def _check(self, e: BaseException) -> bool:
if _signals_control_flow(e):
return False
return not isinstance(e, self.exception_types)


Expand All @@ -141,6 +157,8 @@ def __init__(
super().__init__(self._check)

def _check(self, e: BaseException) -> bool:
if _signals_control_flow(e):
return False
return not isinstance(e, self.exception_types)

def __call__(self, retry_state: "RetryCallState") -> bool:
Expand Down
51 changes: 51 additions & 0 deletions tests/test_tenacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import contextlib
import datetime
import logging
Expand Down Expand Up @@ -728,6 +729,56 @@ def r(fut: tenacity.Future) -> bool:
self.assertTrue(r(tenacity.Future.construct(1, 2, False)))
self.assertFalse(r(tenacity.Future.construct(1, 1, False)))

def _exception_retry_state(self, exc: BaseException) -> "tenacity.RetryCallState":
return make_retry_state(
1, 1.0, last_result=tenacity.Future.construct(1, exc, True)
)

def test_retry_if_not_exception_type_skips_base_exceptions(self) -> None:
"""A BaseException that is not an Exception is control flow, not a failure.

Retrying asyncio.CancelledError swallows a cancellation, which breaks
asyncio.wait_for.
"""
retry = tenacity.retry_if_not_exception_type(IOError)

for exc in (
asyncio.CancelledError(),
KeyboardInterrupt(),
SystemExit(),
GeneratorExit(),
):
self.assertFalse(retry(self._exception_retry_state(exc)), repr(exc))

def test_retry_unless_exception_type_skips_base_exceptions(self) -> None:
retry = tenacity.retry_unless_exception_type(NameError)

for exc in (
asyncio.CancelledError(),
KeyboardInterrupt(),
SystemExit(),
GeneratorExit(),
):
self.assertFalse(retry(self._exception_retry_state(exc)), repr(exc))

def test_retry_if_not_exception_type_still_retries_exceptions(self) -> None:
retry = tenacity.retry_if_not_exception_type(IOError)

self.assertTrue(retry(self._exception_retry_state(ValueError())))
self.assertFalse(retry(self._exception_retry_state(OSError())))

def test_retry_unless_exception_type_still_retries_exceptions(self) -> None:
retry = tenacity.retry_unless_exception_type(NameError)

self.assertTrue(retry(self._exception_retry_state(ValueError())))
self.assertFalse(retry(self._exception_retry_state(NameError())))

def test_retry_if_exception_type_remains_the_opt_in(self) -> None:
"""Retrying a cancellation on purpose is still possible, just explicit."""
retry = tenacity.retry_if_exception_type(asyncio.CancelledError)

self.assertTrue(retry(self._exception_retry_state(asyncio.CancelledError())))

def test_retry_any(self) -> None:
retry = tenacity.retry_any(
tenacity.retry_if_result(lambda x: x == 1),
Expand Down
Loading