Advanced topics

Note

Support for writing custom tasks is provisional. The API is subject to change in future releases. Feedback on the feature is welcome.

Creating your own background task type

Traits Futures comes with three basic background task types: background calls, background iterations and background progress calls, created via the submit_call, submit_iteration and submit_progress functions, respectively. In each case, communication from the background task to the corresponding foreground IFuture instance is implemented by sending custom task-type-specific messages of the form (message_type, message_value), where message_type is a suitable string describing the type of the message. For example, the progress task sends messages of type "progress" to report progress, while the background iteration task sends messages of type "generated".

If none of the standard task types meets your needs, it’s possible to write your own background task type, that sends whatever messages you like. Two base classes, BaseFuture and BaseTask, are made available to make this easier. This section describes how to do this in detail.

To create your own task type, you’ll need three ingredients:

  • A factory for the background callable.

  • A suitable future type, implementing the IFuture interface.

  • A task specification class, implementing the ITaskSpecification interface. The submit method of the TraitsExecutor expects an instance of ITaskSpecification, and interrogates that instance to get the background callable and the corresponding foreground future.

You may optionally also want to create a convenience function analogous to the existing submit_call, submit_iteration and submit_progress functions.

Below we give a worked example that demonstrates how to create each of these ingredients for a simple case.

Worked example: Fizz buzz!

In this section we’ll create an example new background task type, based on the well-known Fizz buzz game. We’ll create a background task that counts slowly from 1, sending three different types of messages to the foreground: it sends “Fizz” messages on multiples of 3, “Buzz” messages on multiples of 5, and “Fizz Buzz” messages on multiples of 15. Each message is accompanied by the corresponding number.

Message types

In general, each message sent from the background task to the future can be any Python object, and the future can interpret the sent object in any way that it likes. However, the BaseFuture and BaseTask convenience base classes that we’ll use below provide helper functions to handle and dispatch messages of the form (message_type, message_args). Here the message type should be a string that’s valid as a Python identifier, while the message argument can be any Python object (though it should usually be pickleable and immutable).

We first define named constants representing our three message types. This isn’t strictly necessary, but it makes the code cleaner.

FIZZ = "fizz"
BUZZ = "buzz"
FIZZ_BUZZ = "fizz_buzz"

The background callable

Next, we define the callable that will be run in the background. The callable itself expects two arguments (which will be passed by position): send and cancelled. The send object is a callable which will be used to send messages to the foreground. The cancelled object is a zero-argument callable which can be used to check for cancellation requests.

However, instead of implementing this callable directly, we inherit from the BaseTask abstract base class. This requires us to implement a (parameterless) run method for the body of the task. The run method has access to methods send and cancelled to send messages to the associated BaseFuture instance and to check whether the user has requested cancellation.

Here’s the fizz_buzz callable.

import time

from traits_futures.api import BaseTask


class FizzBuzzTask(BaseTask):
    """
    Background task for Fizz Buzz

    Counts slowly from 1, sending FIZZ / BUZZ messages to the foreground.

    Parameters
    ----------
    send
        Callable accepting the message to be sent, and returning nothing. The
        message argument should be pickleable, and preferably immutable (or at
        least, not intended to be mutated).
    cancelled
        Callable accepting no arguments and returning a boolean result. It
        returns ``True`` if cancellation has been requested, and ``False``
        otherwise.
    """
    def run(self):
        n = 1
        while not self.cancelled():

            n_is_multiple_of_3 = n % 3 == 0
            n_is_multiple_of_5 = n % 5 == 0

            if n_is_multiple_of_3 and n_is_multiple_of_5:
                self.send(FIZZ_BUZZ, n)
            elif n_is_multiple_of_3:
                self.send(FIZZ, n)
            elif n_is_multiple_of_5:
                self.send(BUZZ, n)

            time.sleep(1.0)
            n += 1

In this example, we don’t return anything from the fizz_buzz function, but in general any object returned by the background callable will be made available under the result property of the corresponding future. Similarly, any exception raised during execution will be made available under the exception property of the corresponding future.

The foreground Future

Now we define a dedicated future class FizzBuzzFuture for this background task type. The most convenient way to do this is to inherit from the BaseFuture class, which is a HasStrictTraits subclass that provides the IFuture interface. Messages coming into the BaseFuture instance from the background task are processed by the dispatch method. The default implementation of this method expects incoming messages to have the form (message_type, message_arg), and it converts each such message to a call to a method named _process_<message_type>, passing message_arg as an argument.

The dispatch method can be safely overridden by subclasses if messages do not have the form (message_type, message_arg), or if some other dispatch mechanism is wanted. For this example, we use the default dispatch mechanism, so all we need to do is to define methods _process_fizz, _process_buzz and _process_fizz_buzz to handle messages of types FIZZ, BUZZ and FIZZ_BUZZ respectively. We choose to process each message by firing a corresponding event on the future.

from traits.api import Event, Int
from traits_futures.api import BaseFuture


class FizzBuzzFuture(BaseFuture):
    """
    Object representing the front-end handle to a running fizz_buzz call.
    """

    #: Event fired whenever we get a FIZZ message. The payload is the
    #: corresponding integer.
    fizz = Event(Int)

    #: Event fired whenever we get a BUZZ message. The payload is the
    #: corresponding integer.
    buzz = Event(Int)

    #: Event fired whenever a FIZZ_BUZZ arrives from the background.
    #: The payload is the corresponding integer.
    fizz_buzz = Event(Int)

    # Private methods #########################################################

    def _process_fizz(self, n):
        self.fizz = n

    def _process_buzz(self, n):
        self.buzz = n

    def _process_fizz_buzz(self, n):
        self.fizz_buzz = n

Putting it all together: the task specification

The last piece we need is a task specification, which is the object that can be submitted to the TraitsExecutor. This object needs to have two attributes: future and task. Given an instance task of a task specification, the TraitsExecutor calls task.future(cancel) to create the future, and task.task() to create the background callable. For the background task, we want to return (but not call!) the fizz_buzz function that we defined above. For the future, we create and return a new FizzBuzzFuture instance. So our task specification looks like this:

from traits_futures.api import ITaskSpecification


@ITaskSpecification.register
class BackgroundFizzBuzz:
    """
    Task specification for Fizz Buzz background tasks.
    """

    def future(self, cancel):
        """
        Return a Future for the background task.

        Parameters
        ----------
        cancel
            Zero-argument callable, returning no useful result. The returned
            future's ``cancel`` method should call this to request cancellation
            of the associated background task.

        Returns
        -------
        FizzBuzzFuture
            Future object that can be used to monitor the status of the
            background task.
        """
        return FizzBuzzFuture(_cancel=cancel)

    def task(self):
        """
        Return a background callable for this task specification.

        Returns
        -------
        collections.abc.Callable
            Callable accepting arguments ``send`` and ``cancelled``. The
            callable can use ``send`` to send messages and ``cancelled`` to
            check whether cancellation has been requested.
        """
        return FizzBuzzTask()

Submitting the new task

With all of the above in place, a Fizz buzz background task can be submitted to a TraitsExecutor executor by passing an instance of BackgroundFizzBuzz to executor.submit. For convenience, we can encapsulate that operation in a function:

def submit_fizz_buzz(executor):
    """
    Convenience function to submit a Fizz buzz task to an executor.

    Parameters
    ----------
    executor : TraitsExecutor
        The executor to submit the task to.

    Returns
    -------
    future : FizzBuzzFuture
        The future for the background task, allowing monitoring and
        cancellation of the background task.
    """
    task = BackgroundFizzBuzz()
    future = executor.submit(task)
    return future

An example GUI

Here’s the complete script obtained from putting together the above snippets:

"""
Example of a user-created background task type.
"""

# This Python file provides supporting code for the "advanced" section of the
# user guide. Because we're using pieces of the file for documentation
# snippets, the overall structure of this file is a little odd, and doesn't
# follow standard style guidelines (for example, placing imports at the top
# of the file). The various '# -- start' and '# -- end' markers are there
# to allow the documentation build to extract the appropriate pieces.


# -- start message types
FIZZ = "fizz"
BUZZ = "buzz"
FIZZ_BUZZ = "fizz_buzz"
# -- end message types


# -- start fizz_buzz --
import time

from traits_futures.api import BaseTask


class FizzBuzzTask(BaseTask):
    """
    Background task for Fizz Buzz

    Counts slowly from 1, sending FIZZ / BUZZ messages to the foreground.

    Parameters
    ----------
    send
        Callable accepting the message to be sent, and returning nothing. The
        message argument should be pickleable, and preferably immutable (or at
        least, not intended to be mutated).
    cancelled
        Callable accepting no arguments and returning a boolean result. It
        returns ``True`` if cancellation has been requested, and ``False``
        otherwise.
    """
    def run(self):
        n = 1
        while not self.cancelled():

            n_is_multiple_of_3 = n % 3 == 0
            n_is_multiple_of_5 = n % 5 == 0

            if n_is_multiple_of_3 and n_is_multiple_of_5:
                self.send(FIZZ_BUZZ, n)
            elif n_is_multiple_of_3:
                self.send(FIZZ, n)
            elif n_is_multiple_of_5:
                self.send(BUZZ, n)

            time.sleep(1.0)
            n += 1
# -- end fizz_buzz --


# -- start FizzBuzzFuture --
from traits.api import Event, Int
from traits_futures.api import BaseFuture


class FizzBuzzFuture(BaseFuture):
    """
    Object representing the front-end handle to a running fizz_buzz call.
    """

    #: Event fired whenever we get a FIZZ message. The payload is the
    #: corresponding integer.
    fizz = Event(Int)

    #: Event fired whenever we get a BUZZ message. The payload is the
    #: corresponding integer.
    buzz = Event(Int)

    #: Event fired whenever a FIZZ_BUZZ arrives from the background.
    #: The payload is the corresponding integer.
    fizz_buzz = Event(Int)

    # Private methods #########################################################

    def _process_fizz(self, n):
        self.fizz = n

    def _process_buzz(self, n):
        self.buzz = n

    def _process_fizz_buzz(self, n):
        self.fizz_buzz = n
# -- end FizzBuzzFuture --


# -- start BackgroundFizzBuzz --
from traits_futures.api import ITaskSpecification


@ITaskSpecification.register
class BackgroundFizzBuzz:
    """
    Task specification for Fizz Buzz background tasks.
    """

    def future(self, cancel):
        """
        Return a Future for the background task.

        Parameters
        ----------
        cancel
            Zero-argument callable, returning no useful result. The returned
            future's ``cancel`` method should call this to request cancellation
            of the associated background task.

        Returns
        -------
        FizzBuzzFuture
            Future object that can be used to monitor the status of the
            background task.
        """
        return FizzBuzzFuture(_cancel=cancel)

    def task(self):
        """
        Return a background callable for this task specification.

        Returns
        -------
        collections.abc.Callable
            Callable accepting arguments ``send`` and ``cancelled``. The
            callable can use ``send`` to send messages and ``cancelled`` to
            check whether cancellation has been requested.
        """
        return FizzBuzzTask()
# -- end BackgroundFizzBuzz


# -- start submit_fizz_buzz
def submit_fizz_buzz(executor):
    """
    Convenience function to submit a Fizz buzz task to an executor.

    Parameters
    ----------
    executor : TraitsExecutor
        The executor to submit the task to.

    Returns
    -------
    future : FizzBuzzFuture
        The future for the background task, allowing monitoring and
        cancellation of the background task.
    """
    task = BackgroundFizzBuzz()
    future = executor.submit(task)
    return future
# -- end submit_fizz_buzz

And here’s an example GUI that makes use of the new background task type:

"""
Example of a custom background job type.
"""

from traits.api import (
    Bool,
    Button,
    HasStrictTraits,
    Instance,
    observe,
    Property,
    Str,
)
from traits_futures.api import TraitsExecutor
from traitsui.api import HGroup, UItem, View

from fizz_buzz_task import FizzBuzzFuture, submit_fizz_buzz


class FizzBuzzUI(HasStrictTraits):
    #: The executor to submit tasks to.
    traits_executor = Instance(TraitsExecutor)

    #: The future object returned on task submission.
    future = Instance(FizzBuzzFuture)

    #: Status message showing current state or the last-received result.
    message = Str("Ready")

    #: Button to calculate, plus its enabled state.
    calculate = Button()
    can_calculate = Property(Bool(), observe="future")

    #: Button to cancel, plus its enabled state.
    cancel = Button()
    can_cancel = Property(Bool(), observe="future.cancellable")

    @observe("calculate")
    def _submit_calculation(self, event):
        self.message = "Running"
        self.future = submit_fizz_buzz(self.traits_executor)

    @observe("cancel")
    def _cancel_running_task(self, event):
        self.message = "Cancelling"
        self.future.cancel()

    @observe("future:fizz")
    def _report_fizz(self, event):
        self.message = "Fizz {}".format(event.new)

    @observe("future:buzz")
    def _report_buzz(self, event):
        self.message = "Buzz {}".format(event.new)

    @observe("future:fizz_buzz")
    def _report_fizz_buzz(self, event):
        self.message = "FIZZ BUZZ! {}".format(event.new)

    @observe("future:done")
    def _reset_future(self, event):
        self.message = "Ready"
        self.future = None

    def _get_can_calculate(self):
        return self.future is None

    def _get_can_cancel(self):
        return self.future is not None and self.future.cancellable

    traits_view = View(
        UItem("message", style="readonly"),
        HGroup(
            UItem("calculate", enabled_when="can_calculate"),
            UItem("cancel", enabled_when="can_cancel"),
        ),
        resizable=True,
    )


if __name__ == "__main__":
    traits_executor = TraitsExecutor()
    try:
        FizzBuzzUI(traits_executor=traits_executor).configure_traits()
    finally:
        traits_executor.shutdown()