Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-tenacity for openSUSE:Factory
checked in at 2024-09-09 14:44:34
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-tenacity (Old)
and /work/SRC/openSUSE:Factory/.python-tenacity.new.10096 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-tenacity"
Mon Sep 9 14:44:34 2024 rev:25 rq:1199461 version:9.0.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-tenacity/python-tenacity.changes
2024-09-05 15:48:25.967173540 +0200
+++
/work/SRC/openSUSE:Factory/.python-tenacity.new.10096/python-tenacity.changes
2024-09-09 14:45:27.721239409 +0200
@@ -1,0 +2,11 @@
+Sun Sep 8 13:40:43 UTC 2024 - Dirk Müller <[email protected]>
+
+- update to 9.0.0:
+ * Respects `min` argument for `wait_random_exponential`
+ * Bump major version to warn API breakage on statistics
+ attribute
+- update to 8.5.0:
+ * fix: Restore contents of retry attribute for wrapped
+ functions
+
+-------------------------------------------------------------------
Old:
----
tenacity-8.4.2.tar.gz
New:
----
tenacity-9.0.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-tenacity.spec ++++++
--- /var/tmp/diff_new_pack.lncpun/_old 2024-09-09 14:45:28.361266034 +0200
+++ /var/tmp/diff_new_pack.lncpun/_new 2024-09-09 14:45:28.361266034 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-tenacity
-Version: 8.4.2
+Version: 9.0.0
Release: 0
Summary: Python module for retrying code until it succeeeds
License: Apache-2.0
++++++ tenacity-8.4.2.tar.gz -> tenacity-9.0.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/.github/workflows/ci.yaml
new/tenacity-9.0.0/.github/workflows/ci.yaml
--- old/tenacity-8.4.2/.github/workflows/ci.yaml 2024-06-24
16:59:58.000000000 +0200
+++ new/tenacity-9.0.0/.github/workflows/ci.yaml 2024-07-29
14:12:16.000000000 +0200
@@ -34,7 +34,7 @@
tox: mypy
steps:
- name: Checkout ðï¸
- uses: actions/[email protected]
+ uses: actions/[email protected]
with:
fetch-depth: 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/.github/workflows/deploy.yaml
new/tenacity-9.0.0/.github/workflows/deploy.yaml
--- old/tenacity-8.4.2/.github/workflows/deploy.yaml 2024-06-24
16:59:58.000000000 +0200
+++ new/tenacity-9.0.0/.github/workflows/deploy.yaml 2024-07-29
14:12:16.000000000 +0200
@@ -11,7 +11,7 @@
runs-on: ubuntu-latest
steps:
- name: Checkout ðï¸
- uses: actions/[email protected]
+ uses: actions/[email protected]
with:
fetch-depth: 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/PKG-INFO new/tenacity-9.0.0/PKG-INFO
--- old/tenacity-8.4.2/PKG-INFO 2024-06-24 17:00:08.379039000 +0200
+++ new/tenacity-9.0.0/PKG-INFO 2024-07-29 14:12:25.123712800 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: tenacity
-Version: 8.4.2
+Version: 9.0.0
Summary: Retry code until it succeeds
Home-page: https://github.com/jd/tenacity
Author: Julien Danjou
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/README.rst
new/tenacity-9.0.0/README.rst
--- old/tenacity-8.4.2/README.rst 2024-06-24 16:59:58.000000000 +0200
+++ new/tenacity-9.0.0/README.rst 2024-07-29 14:12:16.000000000 +0200
@@ -124,8 +124,8 @@
print("Stopping after 10 seconds")
raise Exception
-If you're on a tight deadline, and exceeding your delay time isn't ok,
-then you can give up on retries one attempt before you would exceed the delay.
+If you're on a tight deadline, and exceeding your delay time isn't ok,
+then you can give up on retries one attempt before you would exceed the delay.
.. testcode::
@@ -362,7 +362,7 @@
~~~~~~~~~~
You can access the statistics about the retry made over a function by using the
-`retry` attribute attached to the function and its `statistics` attribute:
+`statistics` attribute attached to the function:
.. testcode::
@@ -375,7 +375,7 @@
except Exception:
pass
- print(raise_my_exception.retry.statistics)
+ print(raise_my_exception.statistics)
.. testoutput::
:hide:
@@ -495,7 +495,7 @@
except Exception:
pass
- print(raise_my_exception.retry.statistics)
+ print(raise_my_exception.statistics)
.. testoutput::
:hide:
@@ -514,6 +514,32 @@
retryer = Retrying(stop=stop_after_attempt(max_attempts), reraise=True)
retryer(never_good_enough, 'I really do try')
+You may also want to change the behaviour of a decorated function temporarily,
+like in tests to avoid unnecessary wait times. You can modify/patch the `retry`
+attribute attached to the function. Bear in mind this is a write-only
attribute,
+statistics should be read from the function `statistics` attribute.
+
+.. testcode::
+
+ @retry(stop=stop_after_attempt(3), wait=wait_fixed(3))
+ def raise_my_exception():
+ raise MyException("Fail")
+
+ from unittest import mock
+
+ with mock.patch.object(raise_my_exception.retry, "wait", wait_fixed(0)):
+ try:
+ raise_my_exception()
+ except Exception:
+ pass
+
+ print(raise_my_exception.statistics)
+
+.. testoutput::
+ :hide:
+
+ ...
+
Retrying code block
~~~~~~~~~~~~~~~~~~~
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/doc/source/index.rst
new/tenacity-9.0.0/doc/source/index.rst
--- old/tenacity-8.4.2/doc/source/index.rst 2024-06-24 16:59:58.000000000
+0200
+++ new/tenacity-9.0.0/doc/source/index.rst 2024-07-29 14:12:16.000000000
+0200
@@ -124,8 +124,8 @@
print("Stopping after 10 seconds")
raise Exception
-If you're on a tight deadline, and exceeding your delay time isn't ok,
-then you can give up on retries one attempt before you would exceed the delay.
+If you're on a tight deadline, and exceeding your delay time isn't ok,
+then you can give up on retries one attempt before you would exceed the delay.
.. testcode::
@@ -362,7 +362,7 @@
~~~~~~~~~~
You can access the statistics about the retry made over a function by using the
-`retry` attribute attached to the function and its `statistics` attribute:
+`statistics` attribute attached to the function:
.. testcode::
@@ -375,7 +375,7 @@
except Exception:
pass
- print(raise_my_exception.retry.statistics)
+ print(raise_my_exception.statistics)
.. testoutput::
:hide:
@@ -495,7 +495,7 @@
except Exception:
pass
- print(raise_my_exception.retry.statistics)
+ print(raise_my_exception.statistics)
.. testoutput::
:hide:
@@ -514,6 +514,32 @@
retryer = Retrying(stop=stop_after_attempt(max_attempts), reraise=True)
retryer(never_good_enough, 'I really do try')
+You may also want to change the behaviour of a decorated function temporarily,
+like in tests to avoid unnecessary wait times. You can modify/patch the `retry`
+attribute attached to the function. Bear in mind this is a write-only
attribute,
+statistics should be read from the function `statistics` attribute.
+
+.. testcode::
+
+ @retry(stop=stop_after_attempt(3), wait=wait_fixed(3))
+ def raise_my_exception():
+ raise MyException("Fail")
+
+ from unittest import mock
+
+ with mock.patch.object(raise_my_exception.retry, "wait", wait_fixed(0)):
+ try:
+ raise_my_exception()
+ except Exception:
+ pass
+
+ print(raise_my_exception.statistics)
+
+.. testoutput::
+ :hide:
+
+ ...
+
Retrying code block
~~~~~~~~~~~~~~~~~~~
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/tenacity-8.4.2/releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml
new/tenacity-9.0.0/releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml
---
old/tenacity-8.4.2/releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml
1970-01-01 01:00:00.000000000 +0100
+++
new/tenacity-9.0.0/releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml
2024-07-29 14:12:16.000000000 +0200
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Restore the value of the `retry` attribute for wrapped functions. Also,
+ clarify that those attributes are write-only and statistics should be
+ read from the function attribute directly.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/tenacity-8.4.2/releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml
new/tenacity-9.0.0/releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml
---
old/tenacity-8.4.2/releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml
1970-01-01 01:00:00.000000000 +0100
+++
new/tenacity-9.0.0/releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml
2024-07-29 14:12:16.000000000 +0200
@@ -0,0 +1,4 @@
+---
+fixes:
+ - |
+ Respects `min` arg for `wait_random_exponential`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/tenacity/__init__.py
new/tenacity-9.0.0/tenacity/__init__.py
--- old/tenacity-8.4.2/tenacity/__init__.py 2024-06-24 16:59:58.000000000
+0200
+++ new/tenacity-9.0.0/tenacity/__init__.py 2024-07-29 14:12:16.000000000
+0200
@@ -339,7 +339,7 @@
return self.copy(*args, **kwargs).wraps(f)
# Preserve attributes
- wrapped_f.retry = wrapped_f # type: ignore[attr-defined]
+ wrapped_f.retry = self # type: ignore[attr-defined]
wrapped_f.retry_with = retry_with # type: ignore[attr-defined]
wrapped_f.statistics = {} # type: ignore[attr-defined]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/tenacity/asyncio/__init__.py
new/tenacity-9.0.0/tenacity/asyncio/__init__.py
--- old/tenacity-8.4.2/tenacity/asyncio/__init__.py 2024-06-24
16:59:58.000000000 +0200
+++ new/tenacity-9.0.0/tenacity/asyncio/__init__.py 2024-07-29
14:12:16.000000000 +0200
@@ -189,7 +189,7 @@
return await copy(fn, *args, **kwargs)
# Preserve attributes
- async_wrapped.retry = async_wrapped # type: ignore[attr-defined]
+ async_wrapped.retry = self # type: ignore[attr-defined]
async_wrapped.retry_with = wrapped.retry_with # type:
ignore[attr-defined]
async_wrapped.statistics = {} # type: ignore[attr-defined]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/tenacity/wait.py
new/tenacity-9.0.0/tenacity/wait.py
--- old/tenacity-8.4.2/tenacity/wait.py 2024-06-24 16:59:58.000000000 +0200
+++ new/tenacity-9.0.0/tenacity/wait.py 2024-07-29 14:12:16.000000000 +0200
@@ -197,7 +197,7 @@
def __call__(self, retry_state: "RetryCallState") -> float:
high = super().__call__(retry_state=retry_state)
- return random.uniform(0, high)
+ return random.uniform(self.min, high)
class wait_exponential_jitter(wait_base):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/tenacity.egg-info/PKG-INFO
new/tenacity-9.0.0/tenacity.egg-info/PKG-INFO
--- old/tenacity-8.4.2/tenacity.egg-info/PKG-INFO 2024-06-24
17:00:08.000000000 +0200
+++ new/tenacity-9.0.0/tenacity.egg-info/PKG-INFO 2024-07-29
14:12:25.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: tenacity
-Version: 8.4.2
+Version: 9.0.0
Summary: Retry code until it succeeds
Home-page: https://github.com/jd/tenacity
Author: Julien Danjou
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/tenacity.egg-info/SOURCES.txt
new/tenacity-9.0.0/tenacity.egg-info/SOURCES.txt
--- old/tenacity-8.4.2/tenacity.egg-info/SOURCES.txt 2024-06-24
17:00:08.000000000 +0200
+++ new/tenacity-9.0.0/tenacity.egg-info/SOURCES.txt 2024-07-29
14:12:25.000000000 +0200
@@ -38,6 +38,7 @@
releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml
releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml
releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml
+releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml
releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml
releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml
releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml
@@ -52,6 +53,7 @@
releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml
releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml
releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml
+releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml
releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml
tenacity/__init__.py
tenacity/_utils.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/tests/test_asyncio.py
new/tenacity-9.0.0/tests/test_asyncio.py
--- old/tenacity-8.4.2/tests/test_asyncio.py 2024-06-24 16:59:58.000000000
+0200
+++ new/tenacity-9.0.0/tests/test_asyncio.py 2024-07-29 14:12:16.000000000
+0200
@@ -17,6 +17,7 @@
import inspect
import unittest
from functools import wraps
+from unittest import mock
try:
import trio
@@ -59,7 +60,7 @@
@retry(stop=stop_after_attempt(2))
async def _retryable_coroutine_with_2_attempts(thing):
await asyncio.sleep(0.00001)
- thing.go()
+ return thing.go()
class TestAsyncio(unittest.TestCase):
@@ -394,6 +395,64 @@
await _async_function(thing)
+class TestDecoratorWrapper(unittest.TestCase):
+ @asynctest
+ async def test_retry_function_attributes(self):
+ """Test that the wrapped function attributes are exposed as intended.
+
+ - statistics contains the value for the latest function run
+ - retry object can be modified to change its behaviour (useful to
patch in tests)
+ - retry object statistics do not contain valid information
+ """
+
+ self.assertTrue(
+ await _retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(1))
+ )
+
+ expected_stats = {
+ "attempt_number": 2,
+ "delay_since_first_attempt": mock.ANY,
+ "idle_for": mock.ANY,
+ "start_time": mock.ANY,
+ }
+ self.assertEqual(
+ _retryable_coroutine_with_2_attempts.statistics, # type:
ignore[attr-defined]
+ expected_stats,
+ )
+ self.assertEqual(
+ _retryable_coroutine_with_2_attempts.retry.statistics, # type:
ignore[attr-defined]
+ {},
+ )
+
+ with mock.patch.object(
+ _retryable_coroutine_with_2_attempts.retry, # type:
ignore[attr-defined]
+ "stop",
+ tenacity.stop_after_attempt(1),
+ ):
+ try:
+ self.assertTrue(
+ await
_retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(2))
+ )
+ except RetryError as exc:
+ expected_stats = {
+ "attempt_number": 1,
+ "delay_since_first_attempt": mock.ANY,
+ "idle_for": mock.ANY,
+ "start_time": mock.ANY,
+ }
+ self.assertEqual(
+ _retryable_coroutine_with_2_attempts.statistics, # type:
ignore[attr-defined]
+ expected_stats,
+ )
+ self.assertEqual(exc.last_attempt.attempt_number, 1)
+ self.assertEqual(
+ _retryable_coroutine_with_2_attempts.retry.statistics, #
type: ignore[attr-defined]
+ {},
+ )
+ else:
+ self.fail("RetryError should have been raised after 1 attempt")
+
+
# make sure mypy accepts passing an async sleep function
# https://github.com/jd/tenacity/issues/399
async def my_async_sleep(x: float) -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tenacity-8.4.2/tests/test_tenacity.py
new/tenacity-9.0.0/tests/test_tenacity.py
--- old/tenacity-8.4.2/tests/test_tenacity.py 2024-06-24 16:59:58.000000000
+0200
+++ new/tenacity-9.0.0/tests/test_tenacity.py 2024-07-29 14:12:16.000000000
+0200
@@ -25,6 +25,7 @@
from contextlib import contextmanager
from copy import copy
from fractions import Fraction
+from unittest import mock
import pytest
@@ -471,9 +472,17 @@
self._assert_inclusive_range(fn(make_retry_state(8, 0)), 0, 60.0)
self._assert_inclusive_range(fn(make_retry_state(9, 0)), 0, 60.0)
- fn = tenacity.wait_random_exponential(10, 5)
+ # max wait
+ max_wait = 5
+ fn = tenacity.wait_random_exponential(10, max_wait)
for _ in range(1000):
- self._assert_inclusive_range(fn(make_retry_state(1, 0)), 0.00,
5.00)
+ self._assert_inclusive_range(fn(make_retry_state(1, 0)), 0.00,
max_wait)
+
+ # min wait
+ min_wait = 5
+ fn = tenacity.wait_random_exponential(min=min_wait)
+ for _ in range(1000):
+ self._assert_inclusive_range(fn(make_retry_state(1, 0)), min_wait,
5)
# Default arguments exist
fn = tenacity.wait_random_exponential()
@@ -1073,7 +1082,7 @@
_retryable_test_with_unless_exception_type_name(NameErrorUntilCount(5))
)
except NameError as e:
- s =
_retryable_test_with_unless_exception_type_name.retry.statistics
+ s = _retryable_test_with_unless_exception_type_name.statistics
self.assertTrue(s["attempt_number"] == 6)
print(e)
else:
@@ -1088,7 +1097,7 @@
)
)
except NameError as e:
- s =
_retryable_test_with_unless_exception_type_no_input.retry.statistics
+ s = _retryable_test_with_unless_exception_type_no_input.statistics
self.assertTrue(s["attempt_number"] == 6)
print(e)
else:
@@ -1111,7 +1120,7 @@
_retryable_test_if_exception_message_message(NoCustomErrorAfterCount(3))
)
except CustomError:
-
print(_retryable_test_if_exception_message_message.retry.statistics)
+ print(_retryable_test_if_exception_message_message.statistics)
self.fail("CustomError should've been retried from errormessage")
def test_retry_if_not_exception_message(self):
@@ -1122,7 +1131,7 @@
)
)
except CustomError:
- s =
_retryable_test_if_not_exception_message_message.retry.statistics
+ s = _retryable_test_if_not_exception_message_message.statistics
self.assertTrue(s["attempt_number"] == 1)
def test_retry_if_not_exception_message_delay(self):
@@ -1131,7 +1140,7 @@
_retryable_test_not_exception_message_delay(NameErrorUntilCount(3))
)
except NameError:
- s = _retryable_test_not_exception_message_delay.retry.statistics
+ s = _retryable_test_not_exception_message_delay.statistics
print(s["attempt_number"])
self.assertTrue(s["attempt_number"] == 4)
@@ -1151,7 +1160,7 @@
)
)
except CustomError:
- s =
_retryable_test_if_not_exception_message_message.retry.statistics
+ s = _retryable_test_if_not_exception_message_message.statistics
self.assertTrue(s["attempt_number"] == 1)
def test_retry_if_exception_cause_type(self):
@@ -1209,6 +1218,43 @@
h = retrying.wraps(Hello())
self.assertEqual(h(), "Hello")
+ def test_retry_function_attributes(self):
+ """Test that the wrapped function attributes are exposed as intended.
+
+ - statistics contains the value for the latest function run
+ - retry object can be modified to change its behaviour (useful to
patch in tests)
+ - retry object statistics do not contain valid information
+ """
+
+
self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2)))
+
+ expected_stats = {
+ "attempt_number": 3,
+ "delay_since_first_attempt": mock.ANY,
+ "idle_for": mock.ANY,
+ "start_time": mock.ANY,
+ }
+ self.assertEqual(_retryable_test_with_stop.statistics, expected_stats)
+ self.assertEqual(_retryable_test_with_stop.retry.statistics, {})
+
+ with mock.patch.object(
+ _retryable_test_with_stop.retry, "stop",
tenacity.stop_after_attempt(1)
+ ):
+ try:
+
self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2)))
+ except RetryError as exc:
+ expected_stats = {
+ "attempt_number": 1,
+ "delay_since_first_attempt": mock.ANY,
+ "idle_for": mock.ANY,
+ "start_time": mock.ANY,
+ }
+ self.assertEqual(_retryable_test_with_stop.statistics,
expected_stats)
+ self.assertEqual(exc.last_attempt.attempt_number, 1)
+ self.assertEqual(_retryable_test_with_stop.retry.statistics,
{})
+ else:
+ self.fail("RetryError should have been raised after 1 attempt")
+
class TestRetryWith:
def test_redefine_wait(self):
@@ -1479,21 +1525,21 @@
def _foobar():
return 42
- self.assertEqual({}, _foobar.retry.statistics)
+ self.assertEqual({}, _foobar.statistics)
_foobar()
- self.assertEqual(1, _foobar.retry.statistics["attempt_number"])
+ self.assertEqual(1, _foobar.statistics["attempt_number"])
def test_stats_failing(self):
@retry(stop=tenacity.stop_after_attempt(2))
def _foobar():
raise ValueError(42)
- self.assertEqual({}, _foobar.retry.statistics)
+ self.assertEqual({}, _foobar.statistics)
try:
_foobar()
except Exception: # noqa: B902
pass
- self.assertEqual(2, _foobar.retry.statistics["attempt_number"])
+ self.assertEqual(2, _foobar.statistics["attempt_number"])
class TestRetryErrorCallback(unittest.TestCase):