#! /usr/bin/env python """Provide way to add timeout specifications to arbitrary functions. There are many ways to add a timeout to a function, but no solution is both cross-platform and capable of terminating the procedure. This module use the multiprocessing module to solve both of those problems.""" ################################################################################ __author__ = 'Stephen "Zero" Chappell <Noctis.Skytower@gmail.com>' __date__ = '11 February 2010' __version__ = '$Revision: 3 $' ################################################################################ import inspect import sys import time import multiprocessing ################################################################################ def add_timeout(function, limit=60): """Add a timeout parameter to a function and return it. It is illegal to pass anything other than a function as the first parameter. If the limit is not given, it gets a default value equal to one minute. The function is wrapped and returned to the caller.""" assert inspect.isfunction(function) if limit <= 0: raise ValueError() return _Timeout(function, limit) class NotReadyError(Exception): pass ################################################################################ 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.__timeout = time.clock() 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.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.clock() def cancel(self): """Terminate any possible execution of the embedded function.""" if self.__process.is_alive(): self.__process.terminate() @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.clock(): 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 raise NotReadyError() def __get_limit(self): return self.__limit def __set_limit(self, value): if value <= 0: raise ValueError() self.__limit = value limit = property(__get_limit, __set_limit, doc="Property for controlling the value of the timeout.")