From a47db0d60916fda04f7c2912a47722f046c239f3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jan 2020 10:05:37 +1000 Subject: [PATCH] add helper for background execution --- qt/aqt/main.py | 2 ++ qt/aqt/taskman.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 qt/aqt/taskman.py diff --git a/qt/aqt/main.py b/qt/aqt/main.py index fdfa21672..69bb47da0 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -34,6 +34,7 @@ from aqt import gui_hooks from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * from aqt.qt import sip +from aqt.taskman import TaskManager from aqt.utils import ( askUser, checkInvalidFilename, @@ -67,6 +68,7 @@ class AnkiQt(QMainWindow): self.state = "startup" self.opts = opts self.col: Optional[_Collection] = None + self.taskman = TaskManager() aqt.mw = self self.app = app self.pm = profileManager diff --git a/qt/aqt/taskman.py b/qt/aqt/taskman.py new file mode 100644 index 000000000..34325de51 --- /dev/null +++ b/qt/aqt/taskman.py @@ -0,0 +1,77 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +""" +Helper for running tasks on background threads. +""" + +from concurrent.futures import Future +from concurrent.futures.thread import ThreadPoolExecutor +from dataclasses import dataclass +from threading import Lock +from typing import Any, Callable, Dict, List, Optional + +from PyQt5.QtCore import QObject, pyqtSignal + + +@dataclass +class PendingDoneCallback: + callback: Callable[[Any], None] + future: Future + + +class TaskManager(QObject): + _results_available = pyqtSignal() + + def __init__(self): + QObject.__init__(self) + + self._executor = ThreadPoolExecutor() + + self._pending_callbacks: List[PendingDoneCallback] = [] + + self._results_lock = Lock() + self._results_available.connect(self._drain_results) # type: ignore + + def run( + self, + task: Callable, + on_done: Optional[Callable], + args: Optional[Dict[str, Any]] = None, + ) -> Future: + """Run task on a background thread, calling on_done on the main thread if provided. + + Args if provided will be passed on as keyword arguments to the task callable.""" + if args is None: + args = {} + + fut = self._executor.submit(task, **args) + + if on_done is not None: + + def done_closure(completed_future: Future) -> None: + self._handle_done_callback(completed_future, on_done) + + fut.add_done_callback(done_closure) + + return fut + + def _handle_done_callback(self, future: Future, callback: Callable) -> None: + """When future completes, schedule its callback to run on the main thread.""" + # add result to the queue + with self._results_lock: + self._pending_callbacks.append( + PendingDoneCallback(callback=callback, future=future) + ) + + # and tell the main thread to flush the queue + self._results_available.emit() # type: ignore + + def _drain_results(self): + """Fires pending callbacks in the main thread.""" + with self._results_lock: + results = self._pending_callbacks + self._pending_callbacks = [] + + for result in results: + result.callback(result.future)