before_sleep_log: Add an exc_info option to include exception tracebacks (#227)
* before_sleep_log: More specific tests to show how log messages look
* before_sleep_log: Add an exc_info option to include exception tracebacks
* fixup! before_sleep_log: Add an exc_info option to include exception tracebacks
Fix pep8 failures.
* fixup! before_sleep_log: Add an exc_info option to include exception tracebacks
* Add release note.
diff --git a/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml b/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml
new file mode 100644
index 0000000..b8d16ee
--- /dev/null
+++ b/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Add an ``exc_info`` option to the ``before_sleep_log()`` strategy.
\ No newline at end of file
diff --git a/tenacity/before_sleep.py b/tenacity/before_sleep.py
index b5fd601..c8b3d33 100644
--- a/tenacity/before_sleep.py
+++ b/tenacity/before_sleep.py
@@ -15,23 +15,32 @@
# limitations under the License.
from tenacity import _utils
+from tenacity.compat import get_exc_info_from_future
def before_sleep_nothing(retry_state):
"""Before call strategy that does nothing."""
-def before_sleep_log(logger, log_level):
+def before_sleep_log(logger, log_level, exc_info=False):
"""Before call strategy that logs to some logger the attempt."""
def log_it(retry_state):
if retry_state.outcome.failed:
- verb, value = 'raised', retry_state.outcome.exception()
+ ex = retry_state.outcome.exception()
+ verb, value = 'raised', '%s: %s' % (type(ex).__name__, ex)
+
+ if exc_info:
+ local_exc_info = get_exc_info_from_future(retry_state.outcome)
+ else:
+ local_exc_info = False
else:
verb, value = 'returned', retry_state.outcome.result()
+ local_exc_info = False # exc_info does not apply when no exception
logger.log(log_level,
"Retrying %s in %s seconds as it %s %s.",
_utils.get_callback_name(retry_state.fn),
getattr(retry_state.next_action, 'sleep'),
- verb, value)
+ verb, value,
+ exc_info=local_exc_info)
return log_it
diff --git a/tenacity/compat.py b/tenacity/compat.py
index 026c248..6bd815e 100644
--- a/tenacity/compat.py
+++ b/tenacity/compat.py
@@ -299,3 +299,24 @@
'retry_error_callback', fn, stacklevel=4)
return fn(retry_state.outcome)
return wrapped_retry_error_callback
+
+
+def get_exc_info_from_future(future):
+ """
+ Get an exc_info value from a Future.
+
+ Given a a Future instance, retrieve an exc_info value suitable for passing
+ in as the exc_info parameter to logging.Logger.log() and related methods.
+
+ On Python 2, this will be a (type, value, traceback) triple.
+ On Python 3, this will be an exception instance (with embedded traceback).
+
+ If there was no exception, None is returned on both versions of Python.
+ """
+ if six.PY3:
+ return future.exception()
+ else:
+ ex, tb = future.exception_info()
+ if ex is None:
+ return None
+ return type(ex), ex, tb
diff --git a/tenacity/tests/test_tenacity.py b/tenacity/tests/test_tenacity.py
index 490d03a..4fbed6e 100644
--- a/tenacity/tests/test_tenacity.py
+++ b/tenacity/tests/test_tenacity.py
@@ -14,6 +14,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
+import os
+import re
import sys
import time
import typing
@@ -1157,12 +1159,42 @@
finally:
logger.removeHandler(handler)
- etalon_re = r'Retrying .* in 0\.01 seconds as it raised .*\.'
+ etalon_re = (r"^Retrying .* in 0\.01 seconds as it raised "
+ r"(IO|OS)Error: Hi there, I'm an IOError\.$")
self.assertEqual(len(handler.records), 2)
- self.assertRegexpMatches(handler.records[0].getMessage(), etalon_re)
- self.assertRegexpMatches(handler.records[1].getMessage(), etalon_re)
+ fmt = logging.Formatter().format
+ self.assertRegexpMatches(fmt(handler.records[0]), etalon_re)
+ self.assertRegexpMatches(fmt(handler.records[1]), etalon_re)
- def test_before_sleep_log_returns(self):
+ def test_before_sleep_log_raises_with_exc_info(self):
+ thing = NoIOErrorAfterCount(2)
+ logger = logging.getLogger(self.id())
+ logger.propagate = False
+ logger.setLevel(logging.INFO)
+ handler = CapturingHandler()
+ logger.addHandler(handler)
+ try:
+ _before_sleep = tenacity.before_sleep_log(logger,
+ logging.INFO,
+ exc_info=True)
+ retrying = Retrying(wait=tenacity.wait_fixed(0.01),
+ stop=tenacity.stop_after_attempt(3),
+ before_sleep=_before_sleep)
+ retrying.call(thing.go)
+ finally:
+ logger.removeHandler(handler)
+
+ etalon_re = re.compile(r"^Retrying .* in 0\.01 seconds as it raised "
+ r"(IO|OS)Error: Hi there, I'm an IOError\.{0}"
+ r"Traceback \(most recent call last\):{0}"
+ r".*$".format(os.linesep),
+ flags=re.MULTILINE)
+ self.assertEqual(len(handler.records), 2)
+ fmt = logging.Formatter().format
+ self.assertRegexpMatches(fmt(handler.records[0]), etalon_re)
+ self.assertRegexpMatches(fmt(handler.records[1]), etalon_re)
+
+ def test_before_sleep_log_returns(self, exc_info=False):
thing = NoneReturnUntilAfterCount(2)
logger = logging.getLogger(self.id())
logger.propagate = False
@@ -1170,7 +1202,9 @@
handler = CapturingHandler()
logger.addHandler(handler)
try:
- _before_sleep = tenacity.before_sleep_log(logger, logging.INFO)
+ _before_sleep = tenacity.before_sleep_log(logger,
+ logging.INFO,
+ exc_info=exc_info)
_retry = tenacity.retry_if_result(lambda result: result is None)
retrying = Retrying(wait=tenacity.wait_fixed(0.01),
stop=tenacity.stop_after_attempt(3),
@@ -1179,10 +1213,14 @@
finally:
logger.removeHandler(handler)
+ etalon_re = r'^Retrying .* in 0\.01 seconds as it returned None\.$'
self.assertEqual(len(handler.records), 2)
- etalon_re = r'Retrying .* in 0\.01 seconds as it returned None'
- self.assertRegexpMatches(handler.records[0].getMessage(), etalon_re)
- self.assertRegexpMatches(handler.records[1].getMessage(), etalon_re)
+ fmt = logging.Formatter().format
+ self.assertRegexpMatches(fmt(handler.records[0]), etalon_re)
+ self.assertRegexpMatches(fmt(handler.records[1]), etalon_re)
+
+ def test_before_sleep_log_returns_with_exc_info(self):
+ self.test_before_sleep_log_returns(exc_info=True)
class TestReraiseExceptions(unittest.TestCase):