Files
cmshoppe/app/workers.py
T
chengma 479ab5c41d feat: 完成PySide6 worker基座
- 新增 BaseWorker 通用 signals、取消标记和统一 run 入口

- 新增 run_worker QThread 包装与生命周期清理

- 新增 worker 单元测试覆盖 signal、取消、异常和类型校验

- 更新任务看板、API 合约、架构、当前状态和进度记录
2026-06-27 10:05:29 +08:00

128 lines
3.4 KiB
Python

"""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