diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2b9b4f92..9b9c5d76 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,7 +27,9 @@ jobs: - python: "3.11" tox: py311 - python: "3.12" - tox: py312,py312-trio + tox: py312 + - python: "3.13" + tox: py313,py313-trio - python: "3.12" tox: pep8 - python: "3.11" diff --git a/setup.cfg b/setup.cfg index ccb824a2..176e0e13 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,7 @@ classifier = [options] install_requires = + exceptiongroup; python_version < "3.11" python_requires = >=3.8 packages = find: diff --git a/tenacity/retry.py b/tenacity/retry.py index 9211631b..d5f82cd9 100644 --- a/tenacity/retry.py +++ b/tenacity/retry.py @@ -16,11 +16,15 @@ import abc import re +import sys import typing if typing.TYPE_CHECKING: from tenacity import RetryCallState +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + class retry_base(abc.ABC): """Abstract base class for retry strategies.""" @@ -79,6 +83,9 @@ def __call__(self, retry_state: "RetryCallState") -> bool: exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") + if isinstance(exception, BaseExceptionGroup): + # look for any exceptions not matching the predicate + return exception.split(self.predicate)[1] is None return self.predicate(exception) else: return False diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index b76fec2c..db2fd877 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -32,6 +32,9 @@ import tenacity from tenacity import RetryCallState, RetryError, Retrying, retry +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + _unset = object() @@ -733,6 +736,24 @@ def go(self): return True +class NoExceptionGroupAfterCount: + def __init__(self, count: int, exceptions: typing.Tuple[Exception]): + self.counter = 0 + self.count = count + self.exceptions = exceptions + + def go(self): + """Raise an ExceptionGroup until after count threshold has been crossed. + + Then return True. + """ + if self.counter < self.count: + self.counter += 1 + raise ExceptionGroup("tenacity test group", self.exceptions) + + return True + + class NoNameErrorAfterCount: """Holds counter state for invoking a method several times in a row.""" @@ -1014,6 +1035,7 @@ def _retryable_test_with_exception_type_custom(thing): retry=tenacity.retry_if_exception_type(CustomError), ) def _retryable_test_with_exception_type_custom_attempt_limit(thing): + # this is not used?? return thing.go() @@ -1064,6 +1086,28 @@ def test_retry_if_exception_of_type(self): self.assertTrue(isinstance(n, NameError)) print(n) + def test_retry_if_exception_of_type_exceptiongroup(self): + self.assertTrue( + _retryable_test_with_exception_type_io( + NoExceptionGroupAfterCount(5, exceptions=(IOError(),)) + ) + ) + with pytest.raises(ExceptionGroup): + self.assertTrue( + _retryable_test_with_exception_type_io( + NoExceptionGroupAfterCount(5, exceptions=(IOError(), ValueError())) + ) + ) + # not supported + with pytest.raises(ExceptionGroup): + e = IOError() + e.__cause__ = NameError() + self.assertTrue( + _retryable_test_with_exception_cause_type( + NoExceptionGroupAfterCount(5, exceptions=(e,)) + ) + ) + def test_retry_except_exception_of_type(self): self.assertTrue( _retryable_test_if_not_exception_type_io(NoNameErrorAfterCount(5)) diff --git a/tox.ini b/tox.ini index 14f8ae00..42f1b7d6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py3{8,9,10,11,12,12-trio}, pep8, pypy3 +envlist = py3{8,9,10,11,12,13,13-trio}, pep8, pypy3 skip_missing_interpreters = True [testenv]