Source code for traits_futures.base_future

# (C) Copyright 2018-2020 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

"""
Base class providing common pieces of the Future machinery.
"""

from traits.api import (
    Any,
    Bool,
    Callable,
    Enum,
    HasStrictTraits,
    Property,
    provides,
    Str,
    Tuple,
)

from traits_futures.future_states import (
    CANCELLABLE_STATES,
    CANCELLED,
    CANCELLING,
    COMPLETED,
    DONE_STATES,
    EXECUTING,
    FAILED,
    FutureState,
    WAITING,
)
from traits_futures.i_future import IFuture


# These extra states let us detect the error of calling _task_started
# a second time following cancellation.

#: Extra internal state: WAITING but not yet initialized. Maps to the
#: WAITING public state.
_NOT_INITIALIZED = "not_initialized"

#: Extra internal state: WAITING and initialized. Maps to the WAITING
#: public state.
_INITIALIZED = "initialized"

#: Extra internal state: CANCELLING before STARTED
_CANCELLING_BEFORE_STARTED = "cancelling_before_started"

#: Extra internal state: CANCELLING after STARTED
_CANCELLING_AFTER_STARTED = "cancelling_after_started"


#: Trait type representing the internal state. The internal state maps
#: directly to the user-facing state, but splits some of the user-facing
#: states to provide more information, which can then be used in internal
#: self-consistency checks. In particular, the user-facing CANCELLING state
#: is split into substates _CANCELLING_BEFORE_STARTED and
#: _CANCELLING_AFTER_STARTED, while the user-facing WAITING state is split
#: into _NOT_INITIALIZED and _INITIALIZED states.
_InternalState = Enum(
    _NOT_INITIALIZED,
    _INITIALIZED,
    EXECUTING,
    COMPLETED,
    FAILED,
    _CANCELLING_BEFORE_STARTED,
    _CANCELLING_AFTER_STARTED,
    CANCELLED,
)


def _state_from_internal_state(internal_state):
    """
    Convert an internal state to the corresponding future state.
    """
    if internal_state in (
        _CANCELLING_AFTER_STARTED,
        _CANCELLING_BEFORE_STARTED,
    ):
        return CANCELLING
    elif internal_state in (_NOT_INITIALIZED, _INITIALIZED):
        return WAITING
    else:
        return internal_state


#: Exception used to indicate a bad state transition. This should
#: never happen as a result of user error, only as a result of
#: a coding error in this repository.
class _StateTransitionError(Exception):
    pass


[docs]@provides(IFuture) class BaseFuture(HasStrictTraits): """ Convenience base class for the various flavours of Future. """ #: The state of the background task, to the best of the knowledge of #: this future. One of the six constants ``WAITING``, ``EXECUTING``, #: ``COMPLETED``, ``FAILED``, ``CANCELLING`` or ``CANCELLED``. state = Property(FutureState) #: True if cancellation of the background task can be requested, else #: False. Cancellation of the background task can be requested only if #: the future's ``state`` is either ``WAITING`` or ``EXECUTING``. cancellable = Property(Bool()) #: True when communications from the background task are complete. #: At that point, no further state changes can occur for this future. #: This trait has value True if the ``state`` is one of ``COMPLETED``, #: ``FAILED``, or ``CANCELLED``. It's safe to listen to this trait #: for changes: it will always fire exactly once, and when it fires #: it will be consistent with the ``state``. done = Property(Bool()) @property def result(self): """ Result of the background task. This attribute is only available if the state of the future is ``COMPLETED``. If the future has not reached the ``COMPLETED`` state, any attempt to access this attribute will raise an ``AttributeError``. Returns ------- result : object The result obtained from the background task. Raises ------ AttributeError If the task is still executing, or was cancelled, or raised an exception instead of returning a result. """ if self.state != COMPLETED: raise AttributeError( "No result available. Task has not yet completed, " "or was cancelled, or failed with an exception. " "Task state is {}".format(self.state) ) return self._result @property def exception(self): """ Information about any exception raised by the background task. This attribute is only available if the state of this future is ``FAILED``. If the future has not reached the ``FAILED`` state, any attempt to access this attribute will raise an ``AttributeError.`` Returns ------- exc_info : tuple(str, str, str) Tuple containing exception information in string form: (exception type, exception value, formatted traceback). Raises ------ AttributeError If the task is still executing, or was cancelled, or completed without raising an exception. """ if self.state != FAILED: raise AttributeError( "No exception information available. Task has " "not yet completed, or was cancelled, or completed " "without an exception. " "Task state is {}".format(self.state) ) return self._exception
[docs] def cancel(self): """ Request cancellation of the background task. A task in ``WAITING`` or ``EXECUTING`` state will immediately be moved to ``CANCELLING`` state. If the task is not in ``WAITING`` or ``EXECUTING`` state, this function will raise ``RuntimeError``. Raises ------ RuntimeError If the task has already completed or cancellation has already been requested. """ if not self.cancellable: raise RuntimeError( "Can only cancel a waiting or executing task. " "Task state is {}".format(self.state) ) self._user_cancelled()
# Semi-private methods #################################################### # These methods represent the state transitions in response to external # events. They're used by the FutureWrapper, but are not intended for use # by the users of Traits Futures. def _dispatch_message(self, message): """ Automate dispatch of different types of message. This is a convenience function, and may be safely overridden by subclasses that want to use a different dispatch mechanism. For a message type ``msgtype``, it looks for a method called ``_process_<msgtype>`` and dispatches the message arguments to that method. Subclasses then only need to provide the appropriate ``_process_<msgtype>`` methods. If the future is already in ``CANCELLING`` state, no message is dispatched. Parameters ---------- message : tuple(str, object) Message from the background task, in the form (message_type, message_args). """ if self._state == _CANCELLING_AFTER_STARTED: # Ignore messages that arrive after a cancellation request. return elif self._state == EXECUTING: message_type, message_arg = message method_name = "_process_{}".format(message_type) getattr(self, method_name)(message_arg) else: raise _StateTransitionError( "Unexpected custom message in state {!r}".format(self._state) ) def _task_started(self, none): """ Update state when the background task has started processing. """ if self._state == _INITIALIZED: self._state = EXECUTING elif self._state == _CANCELLING_BEFORE_STARTED: self._state = _CANCELLING_AFTER_STARTED else: raise _StateTransitionError( "Unexpected 'started' message in state {!r}".format( self._state ) ) def _task_returned(self, result): """ Update state when background task reports completing successfully. """ if self._state == EXECUTING: self._cancel = None self._result = result self._state = COMPLETED elif self._state == _CANCELLING_AFTER_STARTED: self._state = CANCELLED else: raise _StateTransitionError( "Unexpected 'returned' message in state {!r}".format( self._state ) ) def _task_raised(self, exception_info): """ Update state when the background task reports completing with an error. """ if self._state == EXECUTING: self._cancel = None self._exception = exception_info self._state = FAILED elif self._state == _CANCELLING_AFTER_STARTED: self._state = CANCELLED else: raise _StateTransitionError( "Unexpected 'raised' message in state {!r}".format(self._state) ) def _user_cancelled(self): """ Update state when the user requests cancellation. A future in ``WAITING`` or ``EXECUTING`` state moves to ``CANCELLING`` state. """ if self._state == _INITIALIZED: self._cancel() self._cancel = None self._state = _CANCELLING_BEFORE_STARTED elif self._state == EXECUTING: self._cancel() self._cancel = None self._state = _CANCELLING_AFTER_STARTED else: raise _StateTransitionError( "Unexpected 'cancelled' message in state {!r}".format( self._state ) ) def _executor_initialized(self, cancel): """ Update state when the executor initializes the future. Parameters ---------- cancel : callable The callback to be called when the user requests cancellation. The callback accepts no arguments, and has no return value. """ if self._state == _NOT_INITIALIZED: self._cancel = cancel self._state = _INITIALIZED else: raise _StateTransitionError( "Unexpected initialization in state {!r}".format(self._state) ) # Private traits ########################################################## #: Callback called (with no arguments) when user requests cancellation. #: This is reset to ``None`` once cancellation is impossible. _cancel = Callable(allow_none=True) #: The internal state of the future. _state = _InternalState #: Result from the background task. _result = Any() #: Exception information from the background task. _exception = Tuple(Str(), Str(), Str()) # Private methods ######################################################### def _get_state(self): return _state_from_internal_state(self._state) def _get_cancellable(self): return _state_from_internal_state(self._state) in CANCELLABLE_STATES def _get_done(self): return _state_from_internal_state(self._state) in DONE_STATES def __state_changed(self, old__state, new__state): old_state = _state_from_internal_state(old__state) new_state = _state_from_internal_state(new__state) if old_state != new_state: self.trait_property_changed("state", old_state, new_state) old_cancellable = old_state in CANCELLABLE_STATES new_cancellable = new_state in CANCELLABLE_STATES if old_cancellable != new_cancellable: self.trait_property_changed( "cancellable", old_cancellable, new_cancellable ) old_done = old_state in DONE_STATES new_done = new_state in DONE_STATES if old_done != new_done: self.trait_property_changed("done", old_done, new_done)