73 lines
2.0 KiB
Python
73 lines
2.0 KiB
Python
"""Run long operations off the GUI thread, with progress messages and cancel."""
|
|
import traceback
|
|
|
|
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal
|
|
|
|
|
|
class Cancelled(Exception):
|
|
pass
|
|
|
|
|
|
class _Signals(QObject):
|
|
done = Signal(object)
|
|
failed = Signal(object, str)
|
|
progress = Signal(str)
|
|
|
|
|
|
class Job(QRunnable):
|
|
"""fn(job) -> result. Inside fn call job.report(msg) and job.check() (raises Cancelled)."""
|
|
|
|
def __init__(self, fn):
|
|
super().__init__()
|
|
self.fn = fn
|
|
self.signals = _Signals()
|
|
self.cancelled = False
|
|
|
|
def report(self, msg):
|
|
self.check()
|
|
self.signals.progress.emit(str(msg))
|
|
|
|
def check(self):
|
|
if self.cancelled:
|
|
raise Cancelled()
|
|
|
|
def cancel(self):
|
|
self.cancelled = True
|
|
|
|
def run(self):
|
|
try:
|
|
r = self.fn(self)
|
|
except Cancelled:
|
|
self.signals.failed.emit(Cancelled(), "cancelled")
|
|
except Exception as e: # noqa: BLE001 - shown to the user
|
|
self.signals.failed.emit(e, traceback.format_exc())
|
|
else:
|
|
self.signals.done.emit(r)
|
|
|
|
|
|
class Runner:
|
|
"""Starts jobs on a thread pool (or inline when `sync`, used by the GUI tests)."""
|
|
|
|
def __init__(self, sync=False):
|
|
self.sync = sync
|
|
self.pool = QThreadPool.globalInstance()
|
|
self.active = set() # keep Python references until the queued result signal is delivered
|
|
|
|
def start(self, fn, on_done=None, on_failed=None, on_progress=None):
|
|
job = Job(fn)
|
|
job.setAutoDelete(False)
|
|
self.active.add(job)
|
|
job.signals.done.connect(lambda *_: self.active.discard(job))
|
|
job.signals.failed.connect(lambda *_: self.active.discard(job))
|
|
if on_done:
|
|
job.signals.done.connect(on_done)
|
|
if on_failed:
|
|
job.signals.failed.connect(on_failed)
|
|
if on_progress:
|
|
job.signals.progress.connect(on_progress)
|
|
if self.sync:
|
|
job.run()
|
|
else:
|
|
self.pool.start(job)
|
|
return job
|