aboutsummaryrefslogtreecommitdiff
path: root/timeout_decorator/timeout_decorator.py
diff options
context:
space:
mode:
Diffstat (limited to 'timeout_decorator/timeout_decorator.py')
-rw-r--r--timeout_decorator/timeout_decorator.py175
1 files changed, 175 insertions, 0 deletions
diff --git a/timeout_decorator/timeout_decorator.py b/timeout_decorator/timeout_decorator.py
new file mode 100644
index 0000000..42b6686
--- /dev/null
+++ b/timeout_decorator/timeout_decorator.py
@@ -0,0 +1,175 @@
+"""
+Timeout decorator.
+
+ :copyright: (c) 2012-2013 by PN.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+from __future__ import division
+
+import sys
+import time
+import multiprocessing
+import signal
+from functools import wraps
+
+############################################################
+# Timeout
+############################################################
+
+# http://www.saltycrane.com/blog/2010/04/using-python-timeout-decorator-uploading-s3/
+# Used work of Stephen "Zero" Chappell <Noctis.Skytower@gmail.com>
+# in https://code.google.com/p/verse-quiz/source/browse/trunk/timeout.py
+
+
+class TimeoutError(AssertionError):
+
+ """Thrown when a timeout occurs in the `timeout` context manager."""
+
+ def __init__(self, value="Timed Out"):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
+
+
+def _raise_exception(exception, exception_message):
+ """ This function checks if a exception message is given.
+
+ If there is no exception message, the default behaviour is maintained.
+ If there is an exception message, the message is passed to the exception with the 'value' keyword.
+ """
+ if exception_message is None:
+ raise exception()
+ else:
+ raise exception(exception_message)
+
+
+def timeout(seconds=None, use_signals=True, timeout_exception=TimeoutError, exception_message=None):
+ """Add a timeout parameter to a function and return it.
+
+ :param seconds: optional time limit in seconds or fractions of a second. If None is passed, no timeout is applied.
+ This adds some flexibility to the usage: you can disable timing out depending on the settings.
+ :type seconds: float
+ :param use_signals: flag indicating whether signals should be used for timing function out or the multiprocessing
+ When using multiprocessing, timeout granularity is limited to 10ths of a second.
+ :type use_signals: bool
+
+ :raises: TimeoutError if time limit is reached
+
+ It is illegal to pass anything other than a function as the first
+ parameter. The function is wrapped and returned to the caller.
+ """
+ def decorate(function):
+
+ if use_signals:
+ def handler(signum, frame):
+ _raise_exception(timeout_exception, exception_message)
+
+ @wraps(function)
+ def new_function(*args, **kwargs):
+ new_seconds = kwargs.pop('timeout', seconds)
+ if new_seconds:
+ old = signal.signal(signal.SIGALRM, handler)
+ signal.setitimer(signal.ITIMER_REAL, new_seconds)
+
+ if not seconds:
+ return function(*args, **kwargs)
+
+ try:
+ return function(*args, **kwargs)
+ finally:
+ if new_seconds:
+ signal.setitimer(signal.ITIMER_REAL, 0)
+ signal.signal(signal.SIGALRM, old)
+ return new_function
+ else:
+ @wraps(function)
+ def new_function(*args, **kwargs):
+ timeout_wrapper = _Timeout(function, timeout_exception, exception_message, seconds)
+ return timeout_wrapper(*args, **kwargs)
+ return new_function
+
+ return decorate
+
+
+def _target(queue, function, *args, **kwargs):
+ """Run a function with arguments and return output via a queue.
+
+ This is a helper function for the Process created in _Timeout. It runs
+ the function with positional arguments and keyword arguments and then
+ returns the function's output by way of a queue. If an exception gets
+ raised, it is returned to _Timeout to be raised by the value property.
+ """
+ try:
+ queue.put((True, function(*args, **kwargs)))
+ except:
+ queue.put((False, sys.exc_info()[1]))
+
+
+class _Timeout(object):
+
+ """Wrap a function and add a timeout (limit) attribute to it.
+
+ Instances of this class are automatically generated by the add_timeout
+ function defined above. Wrapping a function allows asynchronous calls
+ to be made and termination of execution after a timeout has passed.
+ """
+
+ def __init__(self, function, timeout_exception, exception_message, limit):
+ """Initialize instance in preparation for being called."""
+ self.__limit = limit
+ self.__function = function
+ self.__timeout_exception = timeout_exception
+ self.__exception_message = exception_message
+ self.__name__ = function.__name__
+ self.__doc__ = function.__doc__
+ self.__timeout = time.time()
+ self.__process = multiprocessing.Process()
+ self.__queue = multiprocessing.Queue()
+
+ def __call__(self, *args, **kwargs):
+ """Execute the embedded function object asynchronously.
+
+ The function given to the constructor is transparently called and
+ requires that "ready" be intermittently polled. If and when it is
+ True, the "value" property may then be checked for returned data.
+ """
+ self.__limit = kwargs.pop('timeout', self.__limit)
+ self.__queue = multiprocessing.Queue(1)
+ args = (self.__queue, self.__function) + args
+ self.__process = multiprocessing.Process(target=_target,
+ args=args,
+ kwargs=kwargs)
+ self.__process.daemon = True
+ self.__process.start()
+ if self.__limit is not None:
+ self.__timeout = self.__limit + time.time()
+ while not self.ready:
+ time.sleep(0.01)
+ return self.value
+
+ def cancel(self):
+ """Terminate any possible execution of the embedded function."""
+ if self.__process.is_alive():
+ self.__process.terminate()
+
+ _raise_exception(self.__timeout_exception, self.__exception_message)
+
+ @property
+ def ready(self):
+ """Read-only property indicating status of "value" property."""
+ if self.__limit and self.__timeout < time.time():
+ self.cancel()
+ return self.__queue.full() and not self.__queue.empty()
+
+ @property
+ def value(self):
+ """Read-only property containing data returned from function."""
+ if self.ready is True:
+ flag, load = self.__queue.get()
+ if flag:
+ return load
+ raise load