Many attempts have been made in the past to add timeout functionality in Python such that when a specified time limit expired, waiting code could move on. Unfortunately, pre
The principal problem with your code is the overuse of the double underscore namespace conflict prevention in a class that isn't intended to be subclassed at all.
In general, self.__foo
is a code smell that should be accompanied by a comment along the lines of # This is a mixin and we don't want arbitrary subclasses to have a namespace conflict
.
Further the client API of this method would look like this:
def mymethod(): pass
mymethod = add_timeout(mymethod, 15)
# start the processing
timeout_obj = mymethod()
try:
# access the property, which is really a function call
ret = timeout_obj.value
except TimeoutError:
# handle a timeout here
ret = None
This is not very pythonic at all and a better client api would be:
@timeout(15)
def mymethod(): pass
try:
my_method()
except TimeoutError:
pass
You are using @property in your class for something that is a state mutating accessor, this is not a good idea. For instance, what would happen when .value is accessed twice? It looks like it would fail because queue.get() would return trash because the queue is already empty.
Remove @property entirely. Don't use it in this context, it's not suitable for your use-case. Make call block when called and return the value or raise the exception itself. If you really must have value accessed later, make it a method like .get() or .value().
This code for the _target should be rewritten a little:
def _target(queue, function, *args, **kwargs):
try:
queue.put((True, function(*args, **kwargs)))
except:
queue.put((False, exc_info())) # get *all* the exec info, don't do exc_info[1]
# then later:
raise exc_info[0], exc_info[1], exc_info[2]
That way the stack trace will be preserved correctly and visible to the programmer.
I think you've made a reasonable first crack at writing a useful library, I like the usage of the processing module to achieve the goals.
This question was asked over 9 years ago, and Python has changed a decent amount since then as has my repertoire of experience. After reviewing other APIs in the standard library and wanting to partially replicate one in particular, the follow module was written to serve a similar purpose as the one posted in the question.
asynchronous.py
#! /usr/bin/env python3
import _thread
import abc as _abc
import collections as _collections
import enum as _enum
import math as _math
import multiprocessing as _multiprocessing
import operator as _operator
import queue as _queue
import signal as _signal
import sys as _sys
import time as _time
__all__ = (
'Executor',
'get_timeout',
'set_timeout',
'submit',
'map_',
'shutdown'
)
class _Base(metaclass=_abc.ABCMeta):
__slots__ = (
'__timeout',
)
@_abc.abstractmethod
def __init__(self, timeout):
self.timeout = _math.inf if timeout is None else timeout
def get_timeout(self):
return self.__timeout
def set_timeout(self, value):
if not isinstance(value, (float, int)):
raise TypeError('value must be of type float or int')
if value <= 0:
raise ValueError('value must be greater than zero')
self.__timeout = value
timeout = property(get_timeout, set_timeout)
def _run_and_catch(fn, args, kwargs):
# noinspection PyPep8,PyBroadException
try:
return False, fn(*args, **kwargs)
except:
return True, _sys.exc_info()[1]
def _run(fn, args, kwargs, queue):
queue.put_nowait(_run_and_catch(fn, args, kwargs))
class _State(_enum.IntEnum):
PENDING = _enum.auto()
RUNNING = _enum.auto()
CANCELLED = _enum.auto()
FINISHED = _enum.auto()
ERROR = _enum.auto()
def _run_and_catch_loop(iterable, *args, **kwargs):
exception = None
for fn in iterable:
error, value = _run_and_catch(fn, args, kwargs)
if error:
exception = value
if exception:
raise exception
class _Future(_Base):
__slots__ = (
'__queue',
'__process',
'__start_time',
'__callbacks',
'__result',
'__mutex'
)
def __init__(self, timeout, fn, args, kwargs):
super().__init__(timeout)
self.__queue = _multiprocessing.Queue(1)
self.__process = _multiprocessing.Process(
target=_run,
args=(fn, args, kwargs, self.__queue),
daemon=True
)
self.__start_time = _math.inf
self.__callbacks = _collections.deque()
self.__result = True, TimeoutError()
self.__mutex = _thread.allocate_lock()
@property
def __state(self):
pid, exitcode = self.__process.pid, self.__process.exitcode
return (_State.PENDING if pid is None else
_State.RUNNING if exitcode is None else
_State.CANCELLED if exitcode == -_signal.SIGTERM else
_State.FINISHED if exitcode == 0 else
_State.ERROR)
def __repr__(self):
root = f'{type(self).__name__} at {id(self)} state={self.__state.name}'
if self.__state < _State.CANCELLED:
return f'<{root}>'
error, value = self.__result
suffix = f'{"raised" if error else "returned"} {type(value).__name__}'
return f'<{root} {suffix}>'
def __consume_callbacks(self):
while self.__callbacks:
yield self.__callbacks.popleft()
def __invoke_callbacks(self):
self.__process.join()
_run_and_catch_loop(self.__consume_callbacks(), self)
def cancel(self):
self.__process.terminate()
self.__invoke_callbacks()
def __auto_cancel(self):
elapsed_time = _time.perf_counter() - self.__start_time
if elapsed_time > self.timeout:
self.cancel()
return elapsed_time
def cancelled(self):
self.__auto_cancel()
return self.__state is _State.CANCELLED
def running(self):
self.__auto_cancel()
return self.__state is _State.RUNNING
def done(self):
self.__auto_cancel()
return self.__state > _State.RUNNING
def __handle_result(self, error, value):
self.__result = error, value
self.__invoke_callbacks()
def __ensure_termination(self):
with self.__mutex:
elapsed_time = self.__auto_cancel()
if not self.__queue.empty():
self.__handle_result(*self.__queue.get_nowait())
elif self.__state < _State.CANCELLED:
remaining_time = self.timeout - elapsed_time
if remaining_time == _math.inf:
remaining_time = None
try:
result = self.__queue.get(True, remaining_time)
except _queue.Empty:
self.cancel()
else:
self.__handle_result(*result)
def result(self):
self.__ensure_termination()
error, value = self.__result
if error:
raise value
return value
def exception(self):
self.__ensure_termination()
error, value = self.__result
if error:
return value
def add_done_callback(self, fn):
if self.done():
fn(self)
else:
self.__callbacks.append(fn)
def _set_running_or_notify_cancel(self):
if self.__state is _State.PENDING:
self.__process.start()
self.__start_time = _time.perf_counter()
else:
self.cancel()
class Executor(_Base):
__slots__ = (
'__futures',
)
def __init__(self, timeout=None):
super().__init__(timeout)
self.__futures = set()
def submit(self, fn, *args, **kwargs):
future = _Future(self.timeout, fn, args, kwargs)
self.__futures.add(future)
future.add_done_callback(self.__futures.remove)
# noinspection PyProtectedMember
future._set_running_or_notify_cancel()
return future
@staticmethod
def __cancel_futures(iterable):
_run_and_catch_loop(map(_operator.attrgetter('cancel'), iterable))
def map(self, fn, *iterables):
futures = tuple(self.submit(fn, *args) for args in zip(*iterables))
def result_iterator():
future_iterator = iter(futures)
try:
for future in future_iterator:
yield future.result()
finally:
self.__cancel_futures(future_iterator)
return result_iterator()
def shutdown(self):
self.__cancel_futures(frozenset(self.__futures))
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.shutdown()
return False
_executor = Executor()
get_timeout = _executor.get_timeout
set_timeout = _executor.set_timeout
submit = _executor.submit
map_ = _executor.map
shutdown = _executor.shutdown
del _executor
Since testing is crucial for ensuring that one's code is working correctly, a unit test was created for the linked answer. It is included down below and can be run along the side of the asynchronous
module it is testing.
test_asynchronous.py
#! /usr/bin/env python3
import _thread
import atexit
import contextlib
import functools
import inspect
import itertools
import io
import math
import operator
import os
import queue
import sys
import time
import unittest
import asynchronous
# noinspection PyUnresolvedReferences
class TestConstructor:
def instantiate(self, *args):
parameters = len(inspect.signature(self.CLASS).parameters)
return self.CLASS(*args[:parameters])
def test_valid_timeout(self):
instance = self.instantiate(None, print, (), {})
self.assertEqual(instance.get_timeout(), math.inf)
instance = self.instantiate(1, print, (), {})
self.assertEqual(instance.get_timeout(), 1)
float_timeout = (math.e ** (1j * math.pi) + 1).imag
self.assertIsInstance(float_timeout, float)
instance = self.instantiate(float_timeout, print, (), {})
self.assertEqual(instance.get_timeout(), float_timeout)
def test_error_timeout(self):
self.assertRaises(TypeError, self.instantiate, '60', print, (), {})
self.assertRaises(ValueError, self.instantiate, 0, print, (), {})
self.assertRaises(ValueError, self.instantiate, -1, print, (), {})
# noinspection PyUnresolvedReferences
class TestTimeout(TestConstructor):
def test_valid_property(self):
instance = self.instantiate(None, None, None, None)
instance.timeout = 1
self.assertIsInstance(instance.timeout, int)
instance.timeout = 1 / 2
self.assertIsInstance(instance.timeout, float)
kilo_bit = int.from_bytes(os.urandom(1 << 7), 'big')
instance.timeout = kilo_bit
self.assertEqual(instance.timeout, kilo_bit)
def test_error_property(self):
instance = self.instantiate(None, None, None, None)
for exception, value in (
(TypeError, 'inf'),
(TypeError, complex(123456789, 0)),
(ValueError, 0),
(ValueError, 0.0),
(ValueError, -1),
(ValueError, -math.pi)
):
with self.assertRaises(exception):
instance.timeout = value
self.assertEqual(instance.timeout, math.inf)
class Timer:
__timers = {}
@classmethod
def start_timer(cls):
ident, now = _thread.get_ident(), time.perf_counter()
if now is not cls.__timers.setdefault(ident, now):
raise KeyError(ident)
@classmethod
def stop_timer(cls, expected_time, error=None):
if error is None:
error = 1 / 4 # the default is a quarter second
used = time.perf_counter() - cls.__timers.pop(_thread.get_ident())
diff = used - expected_time
return -error <= diff <= +error
# noinspection PyUnresolvedReferences
class TestTimer(Timer):
def stop_timer(self, expected_time, error=None):
self.assertTrue(super().stop_timer(expected_time, error))
def delay_run(delay, fn, *args, sync=True, **kwargs):
def wrapper():
time.sleep(delay)
return fn(*args, **kwargs)
if sync:
return wrapper()
_thread.start_new_thread(wrapper, ())
# noinspection PyUnresolvedReferences
class TestModuleOrInstance(TestTimer):
@property
def moi(self):
return self.MODULE_OR_INSTANCE
def test_valid_timeout(self):
self.moi.set_timeout(math.inf)
self.assertEqual(self.moi.get_timeout(), math.inf)
self.moi.set_timeout(60)
self.assertEqual(self.moi.get_timeout(), 60)
self.moi.set_timeout(0.05)
self.assertEqual(self.moi.get_timeout(), 0.05)
def test_error_timeout(self):
self.moi.set_timeout(math.inf)
self.assertRaises(TypeError, self.moi.set_timeout, None)
self.assertEqual(self.moi.get_timeout(), math.inf)
self.assertRaises(ValueError, self.moi.set_timeout, 0)
self.assertEqual(self.moi.get_timeout(), math.inf)
self.assertRaises(ValueError, self.moi.set_timeout, -1)
self.assertEqual(self.moi.get_timeout(), math.inf)
def run_submit_check(self):
self.start_timer()
future = self.moi.submit(delay_run, 0.5, operator.add, 1, 2)
self.assertRegex(repr(future), r'^<_Future at \d+ state=RUNNING>$')
self.assertEqual(future.result(), 3)
self.stop_timer(0.5)
self.assertRegex(
repr(future),
r'^<_Future at \d+ state=FINISHED returned int>$'
)
def test_submit_one_second_timeout(self):
self.moi.set_timeout(1)
self.run_submit_check()
def test_submit_no_timeout(self):
self.moi.set_timeout(math.inf)
self.run_submit_check()
def test_submit_short_timeout(self):
self.moi.set_timeout(0.5)
self.start_timer()
future = self.moi.submit(delay_run, 1, operator.add, 1, 2)
self.assertRegex(repr(future), r'^<_Future at \d+ state=RUNNING>$')
self.assertIsInstance(future.exception(), TimeoutError)
self.stop_timer(0.5)
self.assertRegex(
repr(future),
r'^<_Future at \d+ state=CANCELLED raised TimeoutError>$'
)
def run_map(self, *args):
return getattr(self.moi, self.NAME_OF_MAP)(delay_run, *args)
def test_valid_map(self):
self.moi.set_timeout(1.5)
for result in self.run_map(
[1, 1, 1, 1],
[operator.add] * 4,
[0, 1, 2, 3],
[3, 2, 1, 0]
):
self.assertEqual(result, 3)
def test_error_map(self):
self.moi.set_timeout(1.5)
success = 0
with self.assertRaises(TimeoutError):
for result in self.run_map(
[1, 1, 2, 1],
[operator.add] * 4,
[0, 1, 2, 3],
[3, 2, 1, 0]
):
self.assertEqual(result, 3)
success += 1
self.assertEqual(success, 2)
def run_shutdown_check(self, running, future):
self.assertRaises(TimeoutError, future.result)
running.remove(future)
def run_submit_loop(self, executor):
running = set()
done_callback = functools.partial(self.run_shutdown_check, running)
for _ in range(10):
future = executor.submit(delay_run, 2, operator.add, 10, 20)
running.add(future)
future.add_done_callback(done_callback)
time.sleep(0.5)
return running
def test_valid_shutdown(self):
self.moi.set_timeout(1.5)
running = self.run_submit_loop(self.moi)
self.moi.shutdown()
self.assertFalse(running)
def test_error_shutdown(self):
self.moi.set_timeout(1.5)
running = self.run_submit_loop(self.moi)
running.pop()
self.assertRaises(KeyError, self.moi.shutdown)
self.assertFalse(running)
class TestExecutorAPI(TestTimeout, TestModuleOrInstance, unittest.TestCase):
CLASS = asynchronous.Executor
MODULE_OR_INSTANCE = CLASS()
NAME_OF_MAP = 'map'
def test_valid_context_manager(self):
with self.instantiate(1.5) as executor:
running = self.run_submit_loop(executor)
self.assertFalse(running)
def test_error_context_manager(self):
error = Exception()
with self.assertRaises(Exception) as cm:
with self.instantiate(1.5) as executor:
running = self.run_submit_loop(executor)
raise error
self.assertIs(cm.exception, error)
self.assertFalse(running)
with self.assertRaises(KeyError):
with self.instantiate(1.5) as executor:
running = self.run_submit_loop(executor)
running.pop()
self.assertFalse(running)
class TestModuleAPI(TestModuleOrInstance, unittest.TestCase):
MODULE_OR_INSTANCE = asynchronous
NAME_OF_MAP = 'map_'
def verify_error():
sys.stderr.seek(0, io.SEEK_SET)
for line in sys.stderr:
if line == 'queue.Full\n':
break
else:
sys.stderr.seek(0, io.SEEK_SET)
sys.__stderr__.write(sys.stderr.read())
sys.__stderr__.flush()
def cause_error(obj):
sys.stderr = io.StringIO()
atexit.register(verify_error)
inspect.currentframe().f_back.f_back.f_locals['queue'].put_nowait(obj)
def return_(obj):
return obj
# noinspection PyUnusedLocal
def throw(exception, *args):
raise exception
class Silencer:
def __init__(self, silenced):
self.__silenced = silenced
self.__ident = _thread.get_ident()
@property
def silenced(self):
return self.__silenced
def __getattr__(self, name):
return (getattr(self.__silenced, name)
if _thread.get_ident() == self.__ident else
self)
def __call__(self, *args, **kwargs):
return self
@contextlib.contextmanager
def silence_other_threads():
sys.stdout, sys.stderr = Silencer(sys.stdout), Silencer(sys.stderr)
try:
yield
finally:
sys.stdout, sys.stderr = sys.stdout.silenced, sys.stderr.silenced
class TestFutureAPI(TestTimer, TestTimeout, unittest.TestCase):
CLASS = asynchronous._Future
def test_valid_representation(self):
future = self.instantiate(None, time.sleep, (0.1,), {})
self.assertRegex(repr(future), r'^<_Future at \d+ state=PENDING>$')
future._set_running_or_notify_cancel()
self.assertRegex(repr(future), r'^<_Future at \d+ state=RUNNING>$')
future._set_running_or_notify_cancel()
self.assertRegex(
repr(future),
r'^<_Future at \d+ state=CANCELLED raised TimeoutError>$'
)
future = self.instantiate(None, time.sleep, (0.1,), {})
future._set_running_or_notify_cancel()
time.sleep(0.5)
self.assertRegex(
repr(future),
r'^<_Future at \d+ state=FINISHED raised TimeoutError>$'
)
self.assertIsNone(future.exception())
self.assertRegex(
repr(future),
r'^<_Future at \d+ state=FINISHED returned NoneType>$'
)
def test_error_representation(self):
future = self.instantiate(0.5, cause_error, (None,), {})
future._set_running_or_notify_cancel()
self.assertRaises(TypeError, future.result)
self.assertIsInstance(future.exception(), TimeoutError)
self.assertRegex(
repr(future),
r'^<_Future at \d+ state=ERROR raised TimeoutError>$'
)
future = self.instantiate(0.5, cause_error, ((False, 'okay'),), {})
future._set_running_or_notify_cancel()
self.assertEqual(future.result(), 'okay')
self.assertRegex(
repr(future),
r'^<_Future at \d+ state=ERROR returned str>$'
)
def test_cancel(self):
future = self.instantiate(None, time.sleep, (0.1,), {})
self.assertRaises(AttributeError, future.cancel)
future._set_running_or_notify_cancel()
future.cancel()
self.assertTrue(future.cancelled())
future = self.instantiate(None, time.sleep, (0.1,), {})
checker = set()
future.add_done_callback(checker.add)
future._set_running_or_notify_cancel()
future.cancel()
future.cancel()
self.assertIs(checker.pop(), future)
self.assertFalse(checker)
def test_cancelled(self):
future = self.instantiate(None, time.sleep, (0.1,), {})
self.assertFalse(future.cancelled())
future._set_running_or_notify_cancel()
self.assertFalse(future.cancelled())
self.assertIsNone(future.result())
self.assertFalse(future.cancelled())
future = self.instantiate(None, time.sleep, (0.1,), {})
future._set_running_or_notify_cancel()
future.cancel()
self.assertTrue(future.cancelled())
future = self.instantiate(0.1, time.sleep, (1,), {})
future._set_running_or_notify_cancel()
time.sleep(0.5)
self.assertTrue(future.cancelled())
def test_running(self):
future = self.instantiate(None, time.sleep, (0.1,), {})
self.assertFalse(future.running())
future._set_running_or_notify_cancel()
self.assertTrue(future.running())
self.assertIsNone(future.result())
self.assertFalse(future.running())
future = self.instantiate(None, time.sleep, (0.1,), {})
future._set_running_or_notify_cancel()
future.cancel()
self.assertFalse(future.running())
future = self.instantiate(0.1, time.sleep, (1,), {})
future._set_running_or_notify_cancel()
time.sleep(0.5)
self.assertFalse(future.running())
def test_done(self):
future = self.instantiate(None, time.sleep, (0.1,), {})
self.assertFalse(future.done())
future._set_running_or_notify_cancel()
self.assertFalse(future.done())
self.assertIsNone(future.result())
self.assertTrue(future.done())
future = self.instantiate(None, time.sleep, (None,), {})
future._set_running_or_notify_cancel()
self.assertIsInstance(future.exception(), TypeError)
self.assertTrue(future.done())
def test_result_immediate(self):
data = os.urandom(1 << 20)
future = self.instantiate(None, return_, (data,), {})
future._set_running_or_notify_cancel()
self.assertEqual(future.result(), data)
test_exception = Exception('test')
future = self.instantiate(None, throw, (test_exception,), {})
future._set_running_or_notify_cancel()
with self.assertRaises(Exception) as cm:
future.result()
self.assertIsInstance(cm.exception, type(test_exception))
self.assertEqual(cm.exception.args, test_exception.args)
def test_result_delay(self):
future = self.instantiate(None, delay_run, (0, operator.add, 1, 2), {})
self.start_timer()
future._set_running_or_notify_cancel()
self.assertEqual(future.result(), 3)
self.stop_timer(0.1)
future = self.instantiate(None, delay_run, (1, operator.add, 2, 3), {})
self.start_timer()
future._set_running_or_notify_cancel()
self.assertEqual(future.result(), 5)
self.stop_timer(1)
future = self.instantiate(0.5, delay_run, (0, operator.add, 1, 2), {})
self.start_timer()
future._set_running_or_notify_cancel()
self.assertEqual(future.result(), 3)
self.stop_timer(0.1)
future = self.instantiate(0.5, delay_run, (1, operator.add, 2, 3), {})
self.start_timer()
future._set_running_or_notify_cancel()
self.assertRaises(TimeoutError, future.result)
self.stop_timer(0.5)
def test_result_before_running(self):
future = self.instantiate(0.1, delay_run, (0, operator.add, 1, 2), {})
delay_run(0.5, future._set_running_or_notify_cancel, sync=False)
self.start_timer()
self.assertEqual(future.result(), 3)
self.stop_timer(0.5)
def run_time_check(self, test):
self.start_timer()
test()
self.stop_timer(0.5)
def run_waiter_check(self, threads, *tests):
future = self.instantiate(1, delay_run, (0.5, operator.add, 1, 2), {})
future._set_running_or_notify_cancel()
# noinspection PyUnresolvedReferences
result = queue.SimpleQueue()
with silence_other_threads():
for test in itertools.islice(itertools.cycle(tests), threads):
args = self.run_time_check, (lambda: test(future),), {}, result
_thread.start_new_thread(asynchronous._run, args)
for _ in range(threads):
error, value = result.get(True, 1.5)
self.assertFalse(error)
def test_result_with_waiters(self):
self.run_waiter_check(
10,
lambda future: self.assertEqual(future.result(), 3)
)
def test_exception_immediate(self):
data = os.urandom(1 << 20)
future = self.instantiate(None, return_, (data,), {})
future._set_running_or_notify_cancel()
self.assertIsNone(future.exception())
test_exception = Exception('test')
future = self.instantiate(None, throw, (test_exception,), {})
future._set_running_or_notify_cancel()
self.assertIsInstance(future.exception(), type(test_exception))
self.assertEqual(future.exception().args, test_exception.args)
def test_exception_delay(self):
future = self.instantiate(None, delay_run, (0, operator.add, 1, 2), {})
self.start_timer()
future._set_running_or_notify_cancel()
self.assertIsNone(future.exception())
self.stop_timer(0.1)
future = self.instantiate(None, delay_run, (1, operator.add, 2, 3), {})
self.start_timer()
future._set_running_or_notify_cancel()
self.assertIsNone(future.exception())
self.stop_timer(1)
future = self.instantiate(0.5, delay_run, (0, operator.add, 1, 2), {})
self.start_timer()
future._set_running_or_notify_cancel()
self.assertIsNone(future.exception())
self.stop_timer(0.1)
future = self.instantiate(0.5, delay_run, (1, operator.add, 2, 3), {})
self.start_timer()
future._set_running_or_notify_cancel()
self.assertIsInstance(future.exception(), TimeoutError)
self.assertFalse(future.exception().args)
self.stop_timer(0.5)
def test_exception_before_running(self):
future = self.instantiate(0.1, delay_run, (0, operator.add, 1, 2), {})
delay_run(0.5, future._set_running_or_notify_cancel, sync=False)
self.start_timer()
self.assertIsNone(future.exception())
self.stop_timer(0.5)
def test_exception_with_waiters(self):
self.run_waiter_check(
10,
lambda future: self.assertIsNone(future.exception())
)
def test_result_and_exception_waiters(self):
self.run_waiter_check(
10,
lambda future: self.assertEqual(future.result(), 3),
lambda future: self.assertIsNone(future.exception())
)
self.run_waiter_check(
10,
lambda future: self.assertIsNone(future.exception()),
lambda future: self.assertEqual(future.result(), 3)
)
def test_valid_add_done_callback(self):
future = self.instantiate(None, time.sleep, (0,), {})
requires_callback = {future}
future.add_done_callback(requires_callback.remove)
self.assertIn(future, requires_callback)
future._set_running_or_notify_cancel()
self.assertIsNone(future.exception())
self.assertFalse(requires_callback)
requires_callback.add(future)
future.add_done_callback(requires_callback.remove)
self.assertFalse(requires_callback)
def test_error_add_done_callback(self):
future = self.instantiate(None, time.sleep, (0,), {})
requires_callback = [{future} for _ in range(10)]
callbacks = [s.remove for s in requires_callback]
error = Exception()
callbacks.insert(5, functools.partial(throw, error))
for fn in callbacks:
future.add_done_callback(fn)
future._set_running_or_notify_cancel()
with self.assertRaises(Exception) as cm:
future.exception()
self.assertIs(cm.exception, error)
self.assertFalse(any(requires_callback))
def test_set_running_or_notify_cancel(self):
future = self.instantiate(None, time.sleep, (0.1,), {})
self.assertFalse(future.running() or future.done())
future._set_running_or_notify_cancel()
self.assertTrue(future.running())
future._set_running_or_notify_cancel()
self.assertTrue(future.cancelled())
def test_not_empty_queue(self):
data = os.urandom(1 << 20)
future = self.instantiate(None, return_, (data,), {})
future._set_running_or_notify_cancel()
# noinspection PyUnresolvedReferences
result = queue.SimpleQueue()
with silence_other_threads():
for _ in range(2):
delay_run(
0.1,
asynchronous._run,
lambda: self.assertEqual(future.result(), data),
(),
{},
result,
sync=False
)
for _ in range(2):
error, value = result.get(True, 0.2)
self.assertFalse(error)
if __name__ == '__main__':
unittest.main()
This is how to get the decorator syntax Jerub mentioned
def timeout(limit=None):
if limit is None:
limit = DEFAULT_TIMEOUT
if limit <= 0:
raise TimeoutError() # why not ValueError here?
def wrap(function):
return _Timeout(function,limit)
return wrap
@timeout(15)
def mymethod(): pass
The Pebble library was designed to offer cross-platform implementation capable of dealing with problematic logic which could crash, segfault or run indefinitely.
from pebble import concurrent
@concurrent.process(timeout=10)
def function(foo, bar=0):
return foo + bar
future = function(1, bar=2)
try:
result = future.result() # blocks until results are ready
except Exception as error:
print("Function raised %s" % error)
print(error.traceback) # traceback of the function
except TimeoutError as error:
print("Function took longer than %d seconds" % error.args[1])
The decorator works as well with static and class methods. I would not recommend to decorate methods nevertheless, as it is a quite error prone practice.