diff options
Diffstat (limited to 'timeout_decorator/timeout_decorator.py')
-rw-r--r-- | timeout_decorator/timeout_decorator.py | 155 |
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 |