Traits Futures: reactive background processing for Traits and TraitsUI

Release v1.1.

The traits_futures package provides a means to fire off a background calculation from a TraitsUI application, and later respond to the result(s) of that calculation, leaving the main UI responsive for user interactions while the background calculation is in progress.

Features

  • Supports simple calls, iterations, and progress-reporting functions. Can easily be extended to support other messaging patterns.

  • Dispatching a background task returns a “future” object, which provides:

    • information about state changes (e.g., background task completion)

    • facilities to (cooperatively) cancel a long-running background task

    • access to result(s) arriving from the background task

  • Future objects are HasTraits instances, suitable for integration into a TraitsUI application.

  • No need to be a threading expert! Incoming results arrive as trait changes in the main thread. This eliminates a large class of potential issues with traditional thread-based solutions (race conditions, deadlocks, and UI updates off the main thread).

  • Cross-platform.

Limitations

Quick start

Here’s a complete example showing a minimal TraitsUI application that fires off a background computation when its “Calculate” button is pressed, and shows the result when it arrives.

import time

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


def slow_square(n):
    """Square the given input, slowly."""
    time.sleep(5.0)
    return n * n


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

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

    #: Input for the calculation.
    input = Int(10)

    #: Copy of the input for the last-run / currently-running calculation.
    input_for_calculation = Int()

    #: Message about state of calculation.
    message = Str("No previous calculation runs")

    #: Button to start the calculation.
    calculate = Button()

    #: Boolean used to decide whether to enable the "calculate" button.
    no_running_future = Property(Bool(), observe="future:done")

    @observe("calculate")
    def _submit_background_call(self, event):
        # Returns immediately.
        input = self.input
        self.input_for_calculation = self.input
        self.message = "Calculating square of {} ...".format(input)
        self.future = submit_call(self.traits_executor, slow_square, input)
        # Keep a record so that we can present messages accurately.
        self.input_for_calculation = input

    @observe("future:done")
    def _report_result(self, event):
        future = event.object
        self.message = "The square of {} is {}.".format(
            self.input_for_calculation, future.result
        )

    def _get_no_running_future(self):
        return self.future is None or self.future.done

    traits_view = View(
        Item("input"),
        UItem("message", style="readonly"),
        UItem("calculate", enabled_when="no_running_future"),
        resizable=True,
    )


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

User Guide

API Documentation

Changelog

Indices and tables