Advanced topics¶
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, with the type of message identified by
a suitable string. For example, the background 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 message types you like. 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. Thesubmit
method of the TraitsExecutor expects an instance ofITaskSpecification
, and interrogates that instance to get the background callable and the corresponding foreground future.
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, a message sent from the background to the foreground has two parts: a message type, and an optional message argument. The message type should be a string, 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"
Note that the message types are all strings. Ideally, those strings should be valid Python identifiers, since (as we’ll see later) the default message dispatch mechanism uses these strings directly in the corresponding message handler names.
The background callable¶
Next, we define the callable that will be run in the background. This callable
must accept 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. Here’s the
fizz_buzz
callable.
import time
def fizz_buzz(send, cancelled):
"""
Count slowly from 1, sending FIZZ / BUZZ messages to the foreground.
Parameters
----------
send : callable(message_type, message_argument) -> None
Callable accepting two arguments: a message type (a string) as the
first argument, and the message argument (if any) as the optional
second argument. The message argument should be pickleable, and
preferably immutable (or at least, not intended to be mutated). It
should return nothing.
cancelled : callable
Callable accepting no arguments and returning a boolean result. It
returns ``True`` if cancellation has been requested, and ``False``
otherwise.
"""
n = 1
while not 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:
send(FIZZ_BUZZ, n)
elif n_is_multiple_of_3:
send(FIZZ, n)
elif n_is_multiple_of_5:
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_message
method. The default
implementation of this method does a couple of things:
it dispatches the argument of each message to a method named
_process_<message_type>
.it suppresses any messages that arrive after cancellation has been requested
The _dispatch_message
method can be safely overridden by subclasses 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 background_task
. Given an instance task
of a task
specification, the TraitsExecutor
calls task.future()
to create the future, and task.background_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):
"""
Return a Future for the background task.
Returns
-------
FizzBuzzFuture
Future object that can be used to monitor the status of the
background task.
"""
return FizzBuzzFuture()
def background_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 fizz_buzz
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¶
Putting everything together, here’s an example GUI that makes use of the new background task type:
# (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!
"""
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.
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(), depends_on="future")
#: Button to cancel, plus its enabled state.
cancel = Button()
can_cancel = Property(Bool(), depends_on="future.cancellable")
@observe("calculate")
def _submit_calculation(self, event):
self.message = "Running"
self.future = submit_fizz_buzz(self.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__":
FizzBuzzUI().configure_traits()