def testDisconnect(self): receiver = Receiver() signal = Signal() signal.connect(receiver.slot) signal.disconnect(receiver.slot) signal() self.assertEqual(receiver.count, 0) self.assertEqual(len(signal), 0)
class InProgress(Signal, Object): """ InProgress objects are returned from functions that require more time to complete (because they are either blocked on some resource, are executing in a thread, or perhaps simply because they yielded control back to the main loop as a form of cooperative time slicing). InProgress subclasses :class:`~kaa.Signal`, which means InProgress objects are themselves signals. Callbacks connected to an InProgress receive a single argument containing the result of the asynchronously executed task. If the asynchronous task raises an exception, the :attr:`~kaa.InProgress.exception` member, which is a separate signal, is emitted instead. """ __kaasignals__ = { 'abort': ''' Emitted when abort() is called. .. describe:: def callback(exc) :param exc: an exception object the InProgress was aborted with. :type exc: InProgressAborted If the task cannot be aborted, the callback can return False, which will cause an exception to be raised by abort(). ''' } def __init__(self, abortable=False, frame=0): """ :param abortable: see the :attr:`~kaa.InProgress.abortable` property. (Default: False) :type abortable: bool """ super(InProgress, self).__init__() self._exception_signal = Signal() self._finished = False self._finished_event = threading.Event() self._exception = None self._unhandled_exception = None # TODO: make progress a property so we can document it. self.progress = None self.abortable = abortable # Stack frame for the caller who is creating us, for debugging. self._stack = traceback.extract_stack()[:frame-2] self._name = 'owner=%s:%d:%s()' % self._stack[-1][:3] def __repr__(self): return '<%s object at 0x%08x, %s>' % (self.__class__.__name__, id(self), self._name) def __inprogress__(self): """ We subclass Signal which implements this method, but as we are already an InProgress, we simply return self. """ return self @property def exception(self): """ A :class:`~kaa.Signal` emitted when the asynchronous task this InProgress represents has raised an exception. Callbacks connected to this signal receive three arguments: exception class, exception instance, traceback. """ return self._exception_signal @property def finished(self): """ True if the InProgress is finished. """ return self._finished @property def result(self): """ The result the InProgress was finished with. If an exception was thrown to the InProgress, accessing this property will raise that exception. """ if not self._finished: raise RuntimeError('operation not finished') if self._exception: self._unhandled_exception = None if self._exception[2]: # We have the traceback, so we can raise using it. exc_type, exc_value, exc_tb_or_stack = self._exception raise exc_type, exc_value, exc_tb_or_stack else: # No traceback, so construct an AsyncException based on the # stack. raise self._exception[1] return self._result @property def failed(self): """ True if an exception was thrown to the InProgress, False if it was finished without error or if it is not yet finished. """ return bool(self._exception) # XXX: is this property sane after all? Maybe there is no such case where # an IP can be just ignored upon abort(). kaa.delay() turned out not to be # an example after all. @property def abortable(self): """ True if the asynchronous task this InProgress represents can be aborted by a call to :meth:`~kaa.InProgress.abort`. Normally :meth:`~kaa.InProgress.abort` will fail if there are no callbacks attached to the :attr:`~kaa.InProgress.signals.abort` signal. This property may be explicitly set to ``True``, in which case :meth:`~kaa.InProgress.abort` will succeed regardless. An InProgress is therefore abortable if the ``abortable`` property has been explicitly set to True, if if there are callbacks connected to the :attr:`~kaa.InProgress.signals.abort` signal. This is useful when constructing an InProgress object that corresponds to an asynchronous task that can be safely aborted with no explicit action. """ return self._abortable or self.signals['abort'].count() > 0 @abortable.setter def abortable(self, abortable): self._abortable = abortable def finish(self, result): """ This method should be called when the owner (creator) of the InProgress is finished successfully (with no exception). Any callbacks connected to the InProgress will then be emitted with the result passed to this method. If *result* is an unfinished InProgress, then instead of finishing, we wait for the result to finish. :param result: the result of the completed asynchronous task. (This can be thought of as the return value of the task if it had been executed synchronously.) :return: This method returns self, which makes it convenient to prime InProgress objects with a finished value. e.g. ``return InProgress().finish(42)`` """ if self._finished: raise RuntimeError('%s already finished' % self) if isinstance(result, InProgress) and result is not self: # we are still not finished, wait for this new InProgress self.waitfor(result) return self # store result self._finished = True self._result = result self._exception = None # Wake any threads waiting on us self._finished_event.set() # emit signal self.emit_when_handled(result) # cleanup self.disconnect_all() self._exception_signal.disconnect_all() self.signals['abort'].disconnect_all() return self def throw(self, type, value, tb, aborted=False): """ This method should be called when the owner (creator) of the InProgress is finished because it raised an exception. Any callbacks connected to the :attr:`~kaa.InProgress.exception` signal will then be emitted with the arguments passed to this method. The parameters correspond to sys.exc_info(). :param type: the class of the exception :param value: the instance of the exception :param tb: the traceback object representing where the exception took place """ # This function must deal with a tricky problem. See: # http://mail.python.org/pipermail/python-dev/2005-September/056091.html # # Ideally, we want to store the traceback object so we can defer the # exception handling until some later time. The problem is that by # storing the traceback, we create some ridiculously deep circular # references. # # The way we deal with this is to pass along the traceback object to # any handler that can handle the exception immediately, and then # discard the traceback. A stringified formatted traceback is attached # to the exception in the formatted_traceback attribute. # # The above URL suggests a possible non-trivial workaround: create a # custom traceback object in C code that preserves the parts of the # stack frames needed for printing tracebacks, but discarding objects # that would create circular references. This might be a TODO. self._finished = True self._exception = type, value, tb self._unhandled_exception = True stack = traceback.extract_tb(tb) # Attach a stringified traceback to the exception object. Right now, # this is the best we can do for asynchronous handlers. trace = ''.join(traceback.format_exception(*self._exception)).strip() value.formatted_traceback = trace # Wake any threads waiting on us. We've initialized _exception with # the traceback object, so any threads that access the result property # between now and the end of this function will have an opportunity to # get the live traceback. self._finished_event.set() if self._exception_signal.count() == 0: # There are no exception handlers, so we know we will end up # queuing the traceback in the exception signal. Set it to None # to prevent that. tb = None if self._exception_signal.emit_when_handled(type, value, tb) == False: # A handler has acknowledged handling this exception by returning # False. So we won't log it. self._unhandled_exception = None if isinstance(value, InProgressAborted): if not aborted: # An InProgress we were waiting on has been aborted, so we # abort too. self.signals['abort'].emit(value) self._unhandled_exception = None if self._unhandled_exception: # This exception was not handled synchronously, so we set up a # weakref object with a finalize callback to a function that # logs the exception. We could do this in __del__, except that # the gc refuses to collect objects with a destructor. The weakref # kludge lets us accomplish the same thing without actually using # __del__. # # If the exception is passed back via result property, then it is # considered handled, and it will not be logged. cb = Callback(InProgress._log_exception, trace, value, self._stack) self._unhandled_exception = _weakref.ref(self, cb) # Remove traceback from stored exception. If any waiting threads # haven't gotten it by now, it's too late. if not isinstance(value, AsyncExceptionBase): value = AsyncException(value, stack) self._exception = value.__class__, value, None # cleanup self.disconnect_all() self._exception_signal.disconnect_all() self.signals['abort'].disconnect_all() # We return False here so that if we've received a thrown exception # from another InProgress we're waiting on, we essentially inherit # the exception from it and indicate to it that we'll handle it # from here on. (Otherwise the linked InProgress would figure # nobody handled it and would dump out an unhandled async exception.) return False @classmethod def _log_exception(cls, weakref, trace, exc, create_stack): """ Callback to log unhandled exceptions. """ if isinstance(exc, (KeyboardInterrupt, SystemExit)): # We have an unhandled asynchronous SystemExit or KeyboardInterrupt # exception. Rather than logging it, we reraise it in the main # loop so that the main loop exception handler can act # appropriately. def reraise(): raise exc return main.signals['step'].connect_once(reraise) log.error('Unhandled %s exception:\n%s', cls.__name__, trace) if log.level <= logging.INFO: # Asynchronous exceptions create a bit of a problem in that while you # know where the exception came from, you don't easily know where it # was going. Here we dump the stack obtained in the constructor, # so it's possible to find out which caller didn't properly catch # the exception. create_tb = ''.join(traceback.format_list(create_stack)) log.info('Create-stack for InProgress from preceding exception:\n%s', create_tb) def abort(self, exc=None): """ Aborts the asynchronous task this InProgress represents. :param exc: optional exception object with which to abort the InProgress; if None is given, a general InProgressAborted exception will be used. :type exc: InProgressAborted Not all such tasks can be aborted. If aborting is not supported, or if the InProgress is already finished, a RuntimeError exception is raised. If a coroutine is aborted, the CoroutineInProgress object returned by the coroutine will be finished with InProgressAborted, while the underlying generator used by the coroutine will have the standard GeneratorExit raised inside it. """ if self.finished: raise RuntimeError('InProgress is already finished.') if exc is None: exc = InProgressAborted('InProgress task aborted by abort()') elif not isinstance(exc, InProgressAborted): raise ValueError('Exception must be instance of InProgressAborted (or subclass thereof)') if not self.abortable or self.signals['abort'].emit(exc) == False: raise RuntimeError('%s cannot be aborted.' % self) self.throw(exc.__class__, exc, None, aborted=True) def timeout(self, timeout, callback=None, abort=False): """ Create a new InProgress object linked to this one that will throw a TimeoutException if this object is not finished by the given timeout. :param callback: called (with no additional arguments) just prior to TimeoutException :return: a new :class:`~kaa.InProgress` object that is subject to the timeout If the original InProgress finishes before the timeout, the new InProgress (returned by this method) is finished with the result of the original. If a timeout does occur, the original InProgress object is not affected: it is not finished with the TimeoutException, nor is it aborted. If you want to abort the original task you must do it explicitly:: @kaa.coroutine() def read_from_socket(sock): try: data = yield sock.read().timeout(3) except TimeoutException, (msg, inprogress): print 'Error:', msg inprogress.abort() """ async = InProgress() def trigger(): self.disconnect(async.finish) self._exception_signal.disconnect(async.throw) if not async._finished: if callback: callback() msg = 'InProgress timed out after %.02f seconds' % timeout async.throw(TimeoutException, TimeoutException(msg, self), None) if abort: self.abort() async.waitfor(self) OneShotTimer(trigger).start(timeout) return async