thread_worker.pyΒΆ

 1#!/usr/bin/env python
 2
 3"""Example: run a slow computation on a worker thread and show its progress and result.
 4
 5Press :kbd:`Enter` on the button to start the computation and :kbd:`q` to exit.
 6The interface keeps answering keys while the worker runs.
 7"""
 8
 9from __future__ import annotations
10
11import contextlib
12import threading
13import time
14
15import urwid
16
17STEPS = 20
18
19
20def compute(loop: urwid.MainLoop, progress: urwid.ProgressBar, status: urwid.Text) -> None:
21    """Sum the squares of 1 to :data:`STEPS` slowly, reporting each step to the loop.
22
23    Runs on the worker thread: widgets are changed only through :meth:`urwid.MainLoop.call_from_thread`.
24
25    :param loop: main loop that shows the progress
26    :param progress: bar showing the steps done
27    :param status: text showing the result
28    """
29    total = 0
30    # RuntimeError: the loop stopped, so nothing shows the result any more
31    with contextlib.suppress(RuntimeError):
32        for step in range(1, STEPS + 1):
33            time.sleep(0.2)
34            total += step * step
35            loop.call_from_thread(progress.set_completion, step)
36        loop.call_from_thread(status.set_text, f"Done: the sum of squares is {total}.")
37
38
39def main() -> None:
40    """Run the example."""
41    status = urwid.Text("Not started.")
42    progress = urwid.ProgressBar("pg normal", "pg complete", done=STEPS)
43    worker: threading.Thread | None = None
44
45    def start(_button: urwid.Button) -> None:
46        """Start the worker, unless it is still running."""
47        nonlocal worker
48        if worker is not None and worker.is_alive():
49            return
50        status.set_text("Working...")
51        progress.set_completion(0)
52        # A daemon thread, so that q does not wait for the computation to finish.
53        worker = threading.Thread(target=compute, args=(loop, progress, status), daemon=True)
54        worker.start()
55
56    def handle_key(key: str | tuple[str, int, int, int]) -> None:
57        """Exit on :kbd:`q`.
58
59        :param key: unhandled key or mouse event
60        :raises urwid.ExitMainLoop: when the user presses :kbd:`q`
61        """
62        if key in {"q", "Q"}:
63            raise urwid.ExitMainLoop
64
65    body = urwid.Pile([urwid.Button("Compute", on_press=start), progress, status])
66    loop = urwid.MainLoop(
67        urwid.Filler(body, valign=urwid.TOP),
68        palette=[("pg normal", "white", "dark gray"), ("pg complete", "white", "dark blue")],
69        unhandled_input=handle_key,
70    )
71    loop.run()
72
73
74if __name__ == "__main__":
75    main()