Source code for arsenal.robust
import sys
from time import sleep, time
from threading import Thread
from functools import wraps
from contextlib import contextmanager
# Warning: signal is apparently unavailable on windows.
import signal
[docs]class Timeout(Exception): pass
[docs]@contextmanager
def timelimit(seconds):
"""
A decorator to limit a function to `timeout` seconds, raising `Timeout`.
if it takes longer.
>>> def meaningoflife():
... sleep(.2)
... return 42
>>>
>>> timelimit(.1)(meaningoflife)()
Traceback (most recent call last):
...
Timeout: Call took longer than 0.1 seconds.
>>> timelimit(1)(meaningoflife)()
42
>>> with timelimit(.2):
... sleep(1)
Traceback (most recent call last):
...
Timeout: Call took longer than 0.2 seconds.
>>> with timelimit(.2):
... sleep(.1)
... print('finished')
finished
"""
if seconds is None:
yield
return
def signal_handler(signum, frame):
raise Timeout(f'Call took longer than {seconds} seconds.')
signal.signal(signal.SIGALRM, signal_handler)
signal.setitimer(signal.ITIMER_REAL, seconds)
yield
signal.setitimer(signal.ITIMER_REAL, 0) # disables alarm
timelimit.Timeout = Timeout
#_______________________________________________________________________________
#
[docs]def retry_apply(fn, args, kwargs=None, tries=2, pause=None, suppress=(Exception,),
allow=(NameError, NotImplementedError)):
"""
Attempt to call `fn` up to `tries` times with the `args` as arguments
`suppress`: exceptions to be ignored up to the last retry-attempt.
`allow`: exceptions which will not be suppressed; this helps avoid
stupid mistakes such as NameErrors and NotImplementedErrors.
"""
if kwargs is None:
kwargs = {}
for i in range(tries):
try:
return fn(*args, **kwargs)
except allow: # raise these exceptions
raise
except suppress:
if i == tries - 1: # the last iteration
raise
if pause is not None: sleep(pause)
[docs]def retry(tries=2, pause=None, suppress=(Exception,), allow=(NameError, NotImplementedError)):
def retry1(f):
@wraps(f)
def retry2(*args, **kw):
return retry_apply(f, args, kw, tries=tries, pause=pause, suppress=suppress, allow=allow)
return retry2
return retry1
if __name__ == '__main__':
from arsenal.assertions import assert_throws
def test_retry():
class NotCalledEnough(Exception): pass
class TroublsomeFunction(object):
"Function-like object which must be called >=4 times before succeeding."
def __init__(self):
self.tries = 0
def __call__(self, *args):
self.tries += 1
if self.tries > 4:
return True
else:
raise NotCalledEnough
f = TroublsomeFunction()
assert retry_apply(f, (1,2,3), tries=5)
assert f.tries == 5
with assert_throws(NotCalledEnough):
f = TroublsomeFunction()
print(retry_apply(f, (10,), tries=2))
def create_trouble(tries_needs, attempts):
calls = []
@retry(tries=attempts, pause=0.0)
def troublesome(a, kw=None):
"I'm a troublesome function!"
assert a == 'A' and kw == 'KW'
calls.append(1)
if len(calls) < tries_needs:
raise NotCalledEnough
else:
return 'the secret'
assert troublesome.__doc__ == "I'm a troublesome function!"
assert troublesome.__name__ == 'troublesome'
troublesome('A', kw='KW')
create_trouble(4, 4)
with assert_throws(NotCalledEnough):
create_trouble(4, 2)
@retry(tries=4, pause=0.0)
def broken_function():
raise NotImplementedError
with assert_throws(NotImplementedError):
broken_function()
print('retry tests: pass')
def test_timed():
@timelimit(1.0)
def sleepy_function(x): sleep(x)
with assert_throws(Timeout): # should timeout
sleepy_function(3.0)
sleepy_function(0.2) # should succeed
@timelimit(1)
def raises_errors(): return 1/0
with assert_throws(ZeroDivisionError):
raises_errors()
with timelimit(.2):
sleep(.01)
print('finished')
tic = time()
lim = .2
with assert_throws(Timeout):
with timelimit(lim):
sleep(lim + 1)
toc = time()
took = toc - tic
# make sure we wait at least `lim`.
assert took > lim
# make sure that there isn't too much overhead
assert abs(lim - took) < 0.001, abs(lim - took)
print('decorator tests: pass')
test_retry()
test_timed()
import doctest
doctest.testmod(verbose=True)
print('passed tests..')