Repository navigation
Expand file tree
/
Copy pathtasks.py
More file actions
89 lines (70 loc) · 2.79 KB
/
Copy pathtasks.py
File metadata and controls
89 lines (70 loc) · 2.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
"""Small QThreadPool facade with per-key de-duplication."""
from __future__ import annotations
import logging
from collections.abc import Callable
from typing import Any
from PySide6.QtCore import QObject, QRunnable, QThread, QThreadPool, Signal, Slot
log = logging.getLogger(__name__)
class _TaskSignals(QObject):
done = Signal(str, object)
failed = Signal(str, str)
class _Task(QRunnable):
def __init__(self, key: str, work: Callable[[], Any]) -> None:
super().__init__()
self.key = key
self._work = work
self.signals = _TaskSignals()
self.setAutoDelete(True)
def run(self) -> None:
try:
result = self._work()
except Exception as exc: # boundary: never let a worker die silently
log.exception("task %r failed", self.key)
self.signals.failed.emit(self.key, str(exc))
else:
self.signals.done.emit(self.key, result)
class TaskRunner(QObject):
"""Runs blocking WinAPI work off the GUI thread.
``key`` identifies the logical operation; submitting the same key while
it is still running is ignored, which prevents retry storms and
double-clicks from spawning duplicate threads.
"""
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._pool = QThreadPool(self)
self._pool.setMaxThreadCount(max(2, min(8, QThread.idealThreadCount())))
self._callbacks: dict[str, tuple[Callable | None, Callable | None]] = {}
self._tasks: dict[str, _Task] = {}
def submit(
self,
key: str,
work: Callable[[], Any],
on_done: Callable[[Any], None] | None = None,
on_failed: Callable[[str], None] | None = None,
) -> bool:
if key in self._callbacks:
log.debug("task %r already running, skipped", key)
return False
task = _Task(key, work)
task.signals.done.connect(self._on_done)
task.signals.failed.connect(self._on_failed)
self._callbacks[key] = (on_done, on_failed)
self._tasks[key] = task
self._pool.start(task)
return True
def is_running(self, key: str) -> bool:
return key in self._callbacks
def wait(self, msec: int = 3000) -> bool:
return self._pool.waitForDone(msec)
@Slot(str, object)
def _on_done(self, key: str, result: object) -> None:
callbacks = self._callbacks.pop(key, None)
self._tasks.pop(key, None)
if callbacks and callbacks[0] is not None:
callbacks[0](result)
@Slot(str, str)
def _on_failed(self, key: str, message: str) -> None:
callbacks = self._callbacks.pop(key, None)
self._tasks.pop(key, None)
if callbacks and callbacks[1] is not None:
callbacks[1](message)