Skip to content

Commit 671a7b1

Browse files
authored
Merge pull request #6 from ddmee/log_errors
Add log_errors parameter to the poll() function.
2 parents 96d31ad + 05c46c1 commit 671a7b1

File tree

3 files changed

+112
-7
lines changed

3 files changed

+112
-7
lines changed

README.md

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,22 @@
66
polling2
77
=============
88

9+
_Never write another polling function again!_
10+
911
Polling2 is a powerful python utility used to wait for a function to return a certain expected condition.
12+
1013
Some possible uses cases include:
1114

1215
- Wait for API response to return with code 200
1316
- Wait for a file to exist (or not exist)
1417
- Wait for a thread lock on a resource to expire
1518

19+
Polling2 is handy for getting rid of all that duplicated polling-code. Often, applications require retrying until the correct response is returned. Why re-implement the ability to poll again and again? Use Polling2!
20+
1621
Polling2 is a fork of the original [polling](https://github.com/justiniso/polling). It was forked when the original maintainer failed to respond to issues or PRs.
1722

23+
Polling2 is ++under active development++. Would you like to see a particular feature? Ask and thou shall recieve.
24+
1825
# Installation
1926

2027
```
@@ -23,7 +30,7 @@ pip install polling2
2330

2431
# Development installation
2532

26-
```
33+
```shell
2734
# install lib, but use system links from the repo into sitepackages.
2835
python setup.py develop
2936
# install test dependenices.
@@ -139,12 +146,50 @@ This will log the string representation of response object to python's logging m
139146
A message like this will be sent to the log for each return value. You can change the level by providing
140147
a different value to the log parameter.
141148

142-
```
149+
```text
143150
poll() calls check_success(<Response [200]>)
144151
```
145152

153+
There is also an option to log the exceptions that are caught by ignore_exceptions. Note, the full-exception traceback
154+
will not be printed in the logs. Instead, the error and it's message (using %r formatting) will appear. In the following
155+
code snippet, the ValueError raised by the function `raises_error()` will be sent to the logger at the 'warning' level.
156+
157+
```python
158+
import polling2
159+
import logging
160+
import mock
161+
162+
# basicConfig should sent warning level messages to the stdout.
163+
logging.basicConfig()
164+
165+
# Create a function that raises a ValueError, then a RuntimeError.
166+
raises_error = mock.Mock(side_effect=[ValueError('a message'), RuntimeError])
167+
168+
try:
169+
polling2.poll(
170+
target=raises_error,
171+
step=0.1,
172+
max_tries=3,
173+
ignore_exceptions=(ValueError), # Only ignore the ValueError.
174+
log_error=logging.WARNING # Ignored errors should be passed to the logger at warning level.
175+
)
176+
except RuntimeError as _e:
177+
print "Un-ignored %r" % _e``
178+
```
179+
180+
# Future extensions
181+
182+
- Add poll_killer(). Specify a hard timeout so that if the function being polled blocks and doesn't return, poll_killer() will raise a timeout.
183+
- Add an option to do via multiprocessing.
184+
- Add an option to do via threading - probably the default option.
185+
- Add poll_chain(). Have reason to poll a bunch of functions in a row? poll_chain() allows you to chain a bunch of polling functions together.
186+
- Allow step to be specificed as 0, so that we can poll continously. (Perhaps it's best to write a poll_continous() method.)
187+
146188
# Release notes
147189

190+
## 0.4.3
191+
- Add log_error parameter to the poll signature. Enables logging of ignored exceptions.
192+
148193
## 0.4.2
149194
- Add log_value() decorator and log parameter to poll signature. Enables logging of return_values.
150195

@@ -170,4 +215,4 @@ poll() calls check_success(<Response [200]>)
170215

171216
# Contributors
172217
- Justin Iso (original creator)
173-
- Donal Mee
218+
- Donal Mee

polling2.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
"""Polling2 module containing all exceptions and helpers used for the polling function"""
1+
"""Polling2 module containing all exceptions and helpers used for the polling function
22
3-
__version__ = '0.4.2'
3+
Never write another polling function again.
4+
5+
"""
6+
7+
__version__ = '0.4.3'
48

59
import logging
610
import time
@@ -61,7 +65,7 @@ def wrap_check_success(return_val):
6165

6266
def poll(target, step, args=(), kwargs=None, timeout=None, max_tries=None, check_success=is_truthy,
6367
step_function=step_constant, ignore_exceptions=(), poll_forever=False, collect_values=None,
64-
log=logging.NOTSET):
68+
log=logging.NOTSET, log_error=logging.NOTSET):
6569
"""Poll by calling a target function until a certain condition is met. You must specify at least a target
6670
function to be called and the step -- base wait time between each function call.
6771
@@ -112,6 +116,13 @@ def poll(target, step, args=(), kwargs=None, timeout=None, max_tries=None, check
112116
set to a log level greater than NOTSET, then the return values passed to check_success will be logged.
113117
This is done by using the decorator log_value.
114118
119+
:type log_error: int or str, one of logging._levelNames
120+
:opt param log_level: If ignore_exception has been set, you might want to log the exceptions that are
121+
ignored. If the log_error level is greater than NOTSET, then any caught exceptions will be logged at that
122+
level. Note: the logger.exception() function is not used. That would print the stacktrace in the logs. Because
123+
you are ignoring these exceptions, it seems unlikely that'd you'd want a full stack trace for each exception.
124+
However, if you do what this, you can retrieve the exceptions using the collect_values parameter.
125+
115126
:return: Polling will return first value from the target function that meets the condions of the check_success
116127
callback. By default, this will be the first value that is not None, 0, False, '', or an empty collection.
117128
"""
@@ -143,6 +154,10 @@ def poll(target, step, args=(), kwargs=None, timeout=None, max_tries=None, check
143154
last_item = val
144155
except ignore_exceptions as e:
145156
last_item = e
157+
158+
if log_error: # NOTSET is 0, so it'll evaluate to False.
159+
LOGGER.log(log_error, "poll() ignored exception %r", e)
160+
146161
else:
147162
# Condition passes, this is the only "successful" exit from the polling function
148163
if check_success(val):

tests/test_polling2.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import time
44
import unittest
55

6-
from mock import patch
6+
from mock import patch, Mock
77
import pytest
88

99
import polling2
@@ -95,6 +95,20 @@ def test_max_call_no_sleep(self):
9595
polling2.poll(lambda: False, step=sleep, max_tries=tries)
9696
assert time.time() - start_time < tries * sleep, 'Poll function slept before MaxCallException'
9797

98+
def test_ignore_specified_exceptions(self):
99+
"""
100+
Test that ignore_exceptions tuple will ignore exceptions specified.
101+
Should throw any errors not in the tuple.
102+
"""
103+
# raises_errors is a function that returns 3 different things, each time it is called.
104+
# First it raises a ValueError, then EOFError, then a TypeError.
105+
raises_errors = Mock(return_value=True, side_effect=[ValueError, EOFError, RuntimeError])
106+
with pytest.raises(RuntimeError):
107+
# We are ignoring the exceptions other than a TypeError.
108+
polling2.poll(target=raises_errors, step=0.1, max_tries=3,
109+
ignore_exceptions=(ValueError, EOFError))
110+
assert raises_errors.call_count == 3
111+
98112

99113
@pytest.mark.skipif(is_py_34(), reason="pytest logcap fixture isn't available on 3.4")
100114
class TestPollLogging(object):
@@ -128,3 +142,34 @@ def test_default_is_not_log(self, caplog):
128142
with caplog.at_level(logging.DEBUG):
129143
polling2.poll(target=lambda: True, step=0.1, max_tries=1)
130144
assert len(caplog.records) == 0, "Should not be any log records"
145+
146+
def test_log_error_default_is_not_log(self, caplog):
147+
"""
148+
Shouldn't log anything unless explicitly asked to do so.
149+
"""
150+
raises_errors = Mock(side_effect=ValueError('msg is this'))
151+
with caplog.at_level(logging.DEBUG), pytest.raises(polling2.MaxCallException):
152+
polling2.poll(target=raises_errors, ignore_exceptions=(ValueError),
153+
step=0.1, max_tries=2)
154+
assert len(caplog.records) == 0, "Wrong number of log records."
155+
# Test that logging.NOTSET does not print log records either.
156+
polling2.poll(target=raises_errors, ignore_exceptions=(ValueError),
157+
step=0.1, max_tries=2, log_error=logging.NOTSET)
158+
assert len(caplog.records) == 0, "Wrong number of log records."
159+
160+
def test_log_error_set_at_debug_level(self, caplog):
161+
"""
162+
Test that when the log_error parameter is set to debug level, the ignored
163+
errors are sent to the logger.
164+
"""
165+
raises_errors = Mock(side_effect=[ValueError('msg this'), RuntimeError('this msg')])
166+
with caplog.at_level(logging.DEBUG), pytest.raises(polling2.MaxCallException):
167+
polling2.poll(target=raises_errors, ignore_exceptions=(ValueError, RuntimeError),
168+
step=0.1, max_tries=2, log_error=logging.DEBUG)
169+
assert len(caplog.records) == 2, "Wrong number of log records."
170+
assert caplog.records[0].message == "poll() ignored exception ValueError('msg this',)"
171+
assert caplog.records[1].message == "poll() ignored exception RuntimeError('this msg',)"
172+
173+
174+
175+

0 commit comments

Comments
 (0)