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.py155
1 files changed, 131 insertions, 24 deletions
diff --git a/timeout_decorator/timeout_decorator.py b/timeout_decorator/timeout_decorator.py
index f4bb024..014abc0 100644
--- a/timeout_decorator/timeout_decorator.py
+++ b/timeout_decorator/timeout_decorator.py
@@ -1,7 +1,6 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
"""
+Timeout decorator.
+
:copyright: (c) 2012-2013 by PN.
:license: MIT, see LICENSE for more details.
"""
@@ -10,6 +9,9 @@ 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
@@ -17,10 +19,15 @@ from functools import wraps
# Timeout
############################################################
-#http://www.saltycrane.com/blog/2010/04/using-python-timeout-decorator-uploading-s3/
+# 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
@@ -28,25 +35,125 @@ class TimeoutError(AssertionError):
return repr(self.value)
-def timeout(seconds=None):
- def decorate(f):
- def handler(signum, frame):
- raise TimeoutError()
+def timeout(seconds=None, use_signals=True):
+ """Add a timeout parameter to a function and return it.
+
+ :param seconds: optional time limit in seconds. 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: int
+ :param use_signals: flag indicating whether signals should be used for timing function out or the multiprocessing
+ :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 not seconds:
+ return function
+
+ if use_signals:
+ def handler(signum, frame):
+ raise TimeoutError()
+
+ @wraps(function)
+ def new_function(*args, **kwargs):
+ new_seconds = kwargs.pop('timeout', seconds)
+ if new_seconds:
+ old = signal.signal(signal.SIGALRM, handler)
+ signal.alarm(new_seconds)
+ try:
+ return function(*args, **kwargs)
+ finally:
+ if new_seconds:
+ signal.alarm(0)
+ signal.signal(signal.SIGALRM, old)
+ return new_function
+ else:
+ return _Timeout(function, seconds)
- @wraps(f)
- def new_f(*args, **kwargs):
- old = signal.signal(signal.SIGALRM, handler)
-
- new_seconds = kwargs['timeout'] if 'timeout' in kwargs else seconds
- if new_seconds is None:
- raise ValueError("You must provide a timeout value")
-
- signal.alarm(new_seconds)
- try:
- result = f(*args, **kwargs)
- finally:
- signal.alarm(0)
- signal.signal(signal.SIGALRM, old)
- return result
- return new_f
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:
+
+ """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, limit):
+ """Initialize instance in preparation for being called."""
+ self.__limit = limit
+ self.__function = function
+ 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.cancel()
+ 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()
+ 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 TimeoutError()
+
+ @property
+ def ready(self):
+ """Read-only property indicating status of "value" property."""
+ if self.__queue.full():
+ return True
+ elif not self.__queue.empty():
+ return True
+ elif self.__timeout < time.time():
+ self.cancel()
+ else:
+ return False
+
+ @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