#!/usr/bin/env python3 """ Miscellaneous Utility functions for the GUI. Includes LongRunningTask object """ from __future__ import annotations import logging import sys import typing as T from threading import Event, Thread from queue import Queue from .config import get_config if T.TYPE_CHECKING: from collections.abc import Callable from types import TracebackType from lib.multithreading import _ErrorType logger = logging.getLogger(__name__) # pylint: disable=invalid-name class LongRunningTask(Thread): """ Runs long running tasks in a background thread to prevent the GUI from becoming unresponsive. This is sub-classed from :class:`Threading.Thread` so check documentation there for base parameters. Additional parameters listed below. Parameters ---------- widget: tkinter object, optional The widget that this :class:`LongRunningTask` is associated with. Used for setting the busy cursor in the correct location. Default: ``None``. """ _target: Callable _args: tuple _kwargs: dict[str, T.Any] _name: str def __init__(self, target: Callable | None = None, name: str | None = None, args: tuple = (), kwargs: dict[str, T.Any] | None = None, *, daemon: bool = True, widget=None): logger.debug("Initializing %s: (target: %s, name: %s, args: %s, kwargs: %s, " "daemon: %s)", self.__class__.__name__, target, name, args, kwargs, daemon) super().__init__(target=target, name=name, args=args, kwargs=kwargs, daemon=daemon) self.err: _ErrorType = None self._widget = widget self._config = get_config() self._config.set_cursor_busy(widget=self._widget) self._complete = Event() self._queue: Queue = Queue() logger.debug("Initialized %s", self.__class__.__name__,) @property def complete(self) -> Event: """ :class:`threading.Event`: Event is set if the thread has completed its task, otherwise it is unset. """ return self._complete def run(self) -> None: """ Commence the given task in a background thread. """ try: if self._target is not None: retval = self._target(*self._args, **self._kwargs) self._queue.put(retval) except Exception: # pylint: disable=broad-except self.err = T.cast(tuple[type[BaseException], BaseException, "TracebackType"], sys.exc_info()) assert self.err is not None logger.debug("Error in thread (%s): %s", self._name, self.err[1].with_traceback(self.err[2])) finally: self._complete.set() # Avoid a ref-cycle if the thread is running a function with # an argument that has a member that points to the thread. del self._target, self._args, self._kwargs def get_result(self) -> T.Any: """ Return the result from the given task. Returns ------- varies: The result of the thread will depend on the given task. If a call is made to :func:`get_result` prior to the thread completing its task then ``None`` will be returned """ if not self._complete.is_set(): logger.warning("Aborting attempt to retrieve result from a LongRunningTask that is " "still running") return None if self.err: logger.debug("Error caught in thread") self._config.set_cursor_default(widget=self._widget) raise self.err[1].with_traceback(self.err[2]) logger.debug("Getting result from thread") retval = self._queue.get() logger.debug("Got result from thread") self._config.set_cursor_default(widget=self._widget) return retval