Files

128 lines
3.4 KiB
Python
Raw Permalink Normal View History

2026-06-27 10:05:29 +08:00
"""PySide6 worker base classes and thread helpers."""
from __future__ import annotations
from typing import Any, Dict, Optional
try:
from PySide6.QtCore import QObject, QThread, Signal, Slot
QT_IMPORT_ERROR = None
except ModuleNotFoundError as exc:
QObject = object
QThread = None
QT_IMPORT_ERROR = exc
def Signal(*args, **kwargs): # type: ignore
return None
def Slot(*args, **kwargs): # type: ignore
def decorator(func):
return func
return decorator
class WorkerError(RuntimeError):
"""Raised when a worker cannot be started."""
def _require_qt() -> None:
if QT_IMPORT_ERROR is not None:
raise WorkerError("PySide6 未安装,无法启动后台任务")
def _summary_dict(value: Any) -> Dict[str, Any]:
if value is None:
return {}
if isinstance(value, dict):
return dict(value)
return {"result": value}
if QT_IMPORT_ERROR is None:
class BaseWorker(QObject):
"""Base QObject worker for long-running GUI tasks."""
progress = Signal(dict)
row_updated = Signal(int, dict)
log = Signal(str)
failed = Signal(int, str)
finished = Signal(dict)
cancelled = Signal(dict)
def __init__(self, parent: Optional[QObject] = None):
super().__init__(parent)
self._cancel_requested = False
def cancel(self) -> None:
self._cancel_requested = True
def is_cancelled(self) -> bool:
return self._cancel_requested
def should_cancel(self) -> bool:
return self.is_cancelled()
@Slot()
def run(self) -> None:
if self.is_cancelled():
self.cancelled.emit({"cancelled": True})
return
try:
summary = self.execute()
except Exception as exc: # pragma: no cover - covered through signal behavior
error = str(exc) or exc.__class__.__name__
self.failed.emit(-1, error)
self.finished.emit({"ok": False, "error": error})
return
payload = _summary_dict(summary)
if self.is_cancelled():
payload["cancelled"] = True
self.cancelled.emit(payload)
else:
self.finished.emit(payload)
def execute(self) -> Optional[Dict[str, Any]]:
raise NotImplementedError
else:
class BaseWorker(QObject): # type: ignore
def __init__(self, *args, **kwargs):
_require_qt()
def run_worker(
worker: BaseWorker,
thread_name: Optional[str] = None,
start: bool = True,
) -> QThread:
"""Move a worker to a QThread, wire lifecycle signals, and start it."""
_require_qt()
if not isinstance(worker, BaseWorker):
raise TypeError("worker 必须是 BaseWorker 实例")
thread = QThread()
if thread_name:
thread.setObjectName(thread_name)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(thread.quit)
worker.cancelled.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
worker.cancelled.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
# Keep the Python wrapper alive until Qt owns the lifecycle.
thread._cmshopee_worker = worker # type: ignore[attr-defined]
if start:
thread.start()
return thread