Commit 315ad230 authored by Yakubov, Sergey's avatar Yakubov, Sergey
Browse files

add background worker

parent 00ddc477
Loading
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
[tool.poetry]
name = "nova-mvvm"
version = "0.10.4"
version = "0.11.0"
description = "A Python Package for Model-View-ViewModel pattern"
authors = ["Yakubov, Sergey <yakubovs@ornl.gov>"]
readme = "README.md"
+57 −0
Original line number Diff line number Diff line
@@ -11,6 +11,58 @@ CallbackAfterUpdateType = Union[
]


class Worker:
    """Abstract worker class.

    Provides methods required to run tasks in backend.
    """

    @abstractmethod
    def start(self) -> None:
        """Start running the task in a background thread."""
        raise NotImplementedError("start() must be implemented in a subclass")

    @abstractmethod
    def connect_result(self, callback: Callable[[Any], None]) -> None:
        """
        Register a callback to be called with the result when the task finishes.

        Args:
            callback (Callable[[Any], None]): Function called with the result.
        """
        raise NotImplementedError("connect_result() must be implemented in a subclass")

    @abstractmethod
    def connect_error(self, callback: Callable[[Exception], None]) -> None:
        """
        Register a callback to be called if the task raises an exception.

        Args:
            callback (Callable[[Exception], None]): Function called with the exception.
        """
        raise NotImplementedError("connect_error() must be implemented in a subclass")

    @abstractmethod
    def connect_finished(self, callback: Callable[[], None]) -> None:
        """
        Register a callback to be called when the task has finished (success or failure).

        Args:
            callback (Callable[[], None]): Function called with no arguments.
        """
        raise NotImplementedError("connect_finished() must be implemented in a subclass")

    @abstractmethod
    def connect_progress(self, callback: Any) -> None:
        """
        Register a callback to be called with progress updates (0 to 100).

        Args:
            callback (Callable[[float], None]): Function called with a float progress value.
        """
        raise NotImplementedError("connect_progress() must be implemented in a subclass")


class Communicator(ABC):
    """Abstract communicator class.

@@ -97,3 +149,8 @@ class BindingInterface(ABC):
            ViewModel/Model variable and the GUI framework element.
        """
        raise Exception("Please implement in a concrete class")

    @abstractmethod
    def new_worker(self, task: Callable[..., Any], *args: Any, **kwargs: Any) -> Worker:
        """Creates an instance of a Worker class to be used to run tasks in background."""
        raise Exception("Please implement in a concrete class")
+19 −3
Original line number Diff line number Diff line
"""Binding module for PyQt5 framework."""

import inspect
from typing import Any
from typing import Any, Callable

from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtCore import QObject, QThreadPool, pyqtSignal
from typing_extensions import override

from .._internal.pyqt_communicator import PyQtCommunicator
from ..interface import BindingInterface
from ..interface import BindingInterface, Worker
from .pyqt5_worker import PyQt5Worker


def is_callable(var: Any) -> bool:
@@ -19,9 +21,19 @@ class PyQtObject(QObject):
    signal = pyqtSignal(object)


class ThreadPool(QThreadPool):
    """ThreadPool class."""

    def __init__(self) -> None:
        super().__init__()


class PyQt5Binding(BindingInterface):
    """Binding Interface implementation for PyQt."""

    def __init__(self) -> None:
        self.thread_pool = ThreadPool()

    def new_bind(
        self, linked_object: Any = None, linked_object_arguments: Any = None, callback_after_update: Any = None
    ) -> Any:
@@ -31,3 +43,7 @@ class PyQt5Binding(BindingInterface):
        I update and linked_object to trigger ViewModel/Model update
        """
        return PyQtCommunicator(PyQtObject, linked_object, linked_object_arguments, callback_after_update)

    @override
    def new_worker(self, task: Callable[..., Any], *args: Any, **kwargs: Any) -> Worker:
        return PyQt5Worker(self.thread_pool, task, *args, **kwargs)
+69 −0
Original line number Diff line number Diff line
"""Worker module for PyQt5 framework."""

import sys
import traceback
from typing import Any, Callable

from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from typing_extensions import override

from nova.mvvm.interface import Worker


class WorkerSignals(QObject):
    """Defines the signals available from a running worker thread."""

    finished = pyqtSignal()
    error = pyqtSignal(tuple)
    progress = pyqtSignal(str, int)
    result = pyqtSignal(object)


class PyQt5Worker(QRunnable, Worker):
    """Worker class that executes a function with provided arguments in a separate thread."""

    def __init__(self, thread_pool: QThreadPool, task: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
        super().__init__()
        self.thread_pool = thread_pool
        self.signals = WorkerSignals()
        self.task = task
        self.args = args
        self.kwargs = kwargs

        self.kwargs["progress"] = self._emit_progress

    @pyqtSlot()
    def run(self) -> None:
        try:
            result = self.task(*self.args, **self.kwargs)
        except Exception:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]
            self.signals.error.emit((exctype, value, traceback.format_exc()))
        else:
            self.signals.result.emit(result)
        finally:
            self.signals.finished.emit()

    def _emit_progress(self, message: str, progress: int) -> None:
        self.signals.progress.emit(message, progress)

    @override
    def connect_error(self, callback: Callable[[Any], None]) -> None:
        self.signals.error.connect(callback)

    @override
    def connect_result(self, callback: Callable[[Any], None]) -> None:
        self.signals.result.connect(callback)

    @override
    def connect_finished(self, callback: Callable[[], None]) -> None:
        self.signals.finished.connect(callback)

    @override
    def connect_progress(self, callback: Callable[[str, int], None]) -> None:
        self.signals.progress.connect(callback)

    @override
    def start(self) -> None:
        self.thread_pool.start(self)
+19 −3
Original line number Diff line number Diff line
"""Binding module for PyQt framework."""

from typing import Any
from typing import Any, Callable

from PyQt6.QtCore import QObject, pyqtSignal  # type: ignore
from PyQt6.QtCore import QObject, QThreadPool, pyqtSignal  # type: ignore
from typing_extensions import override

from .._internal.pyqt_communicator import PyQtCommunicator
from ..interface import BindingInterface
from ..interface import BindingInterface, Worker
from .pyqt6_worker import PyQt6Worker


class PyQtObject(QObject):
@@ -14,9 +16,19 @@ class PyQtObject(QObject):
    signal = pyqtSignal(object)


class ThreadPool(QThreadPool):
    """ThreadPool class."""

    def __init__(self) -> None:
        super().__init__()


class PyQt6Binding(BindingInterface):
    """Binding Interface implementation for PyQt."""

    def __init__(self) -> None:
        self.thread_pool = ThreadPool()

    def new_bind(
        self, linked_object: Any = None, linked_object_arguments: Any = None, callback_after_update: Any = None
    ) -> Any:
@@ -26,3 +38,7 @@ class PyQt6Binding(BindingInterface):
        I update and linked_object to trigger ViewModel/Model update
        """
        return PyQtCommunicator(PyQtObject, linked_object, linked_object_arguments, callback_after_update)

    @override
    def new_worker(self, task: Callable[..., Any], *args: Any, **kwargs: Any) -> Worker:
        return PyQt6Worker(self.thread_pool, task, *args, **kwargs)
Loading