2019-04-19 16:07:01 +10:00
|
|
|
# Copyright 2019 miruka
|
|
|
|
# This file is part of harmonyqml, licensed under GPLv3.
|
|
|
|
|
|
|
|
import functools
|
|
|
|
import logging
|
|
|
|
import sys
|
|
|
|
import traceback
|
2019-04-20 08:21:19 +10:00
|
|
|
from concurrent.futures import Executor, Future
|
|
|
|
from typing import Callable, List, Optional, Tuple, Union
|
2019-04-19 16:07:01 +10:00
|
|
|
|
|
|
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
|
|
|
|
|
|
|
|
|
|
|
class PyQtFuture(QObject):
|
2019-04-22 06:30:56 +10:00
|
|
|
gotResult = pyqtSignal("QVariant")
|
2019-04-19 16:07:01 +10:00
|
|
|
|
|
|
|
def __init__(self, future: Future, parent: QObject) -> None:
|
|
|
|
super().__init__(parent)
|
|
|
|
self.future = future
|
|
|
|
self._result = None
|
|
|
|
|
2019-04-22 06:30:56 +10:00
|
|
|
self.future.add_done_callback(
|
|
|
|
lambda future: self.gotResult.emit(future.result())
|
|
|
|
)
|
2019-04-19 16:07:01 +10:00
|
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
return "%s(%s)" % (type(self).__name__, repr(self.future))
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
def cancel(self):
|
|
|
|
self.future.cancel()
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtProperty(bool)
|
|
|
|
def cancelled(self):
|
|
|
|
return self.future.cancelled()
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtProperty(bool)
|
|
|
|
def running(self):
|
|
|
|
return self.future.running()
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtProperty(bool)
|
|
|
|
def done(self):
|
|
|
|
return self.future.done()
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot(result="QVariant")
|
|
|
|
@pyqtSlot(int, result="QVariant")
|
|
|
|
@pyqtSlot(float, result="QVariant")
|
|
|
|
def result(self, timeout: Optional[Union[int, float]] = None):
|
|
|
|
return self.future.result(timeout)
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtProperty("QVariant", notify=gotResult)
|
|
|
|
def value(self):
|
|
|
|
return self.future.result() if self.done else None
|
|
|
|
|
|
|
|
|
|
|
|
def add_done_callback(self, fn: Callable[[Future], None]) -> None:
|
|
|
|
self.future.add_done_callback(fn)
|
|
|
|
|
|
|
|
|
2019-04-20 08:21:19 +10:00
|
|
|
_RUNNING: List[Tuple[Executor, Callable, tuple, dict]] = []
|
|
|
|
|
|
|
|
|
2019-04-27 08:47:25 +10:00
|
|
|
def futurize(max_instances: Optional[int] = None, pyqt: bool = True
|
|
|
|
) -> Callable:
|
2019-04-20 08:21:19 +10:00
|
|
|
|
|
|
|
def decorator(func: Callable) -> Callable:
|
|
|
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
def wrapper(self, *args, **kws) -> Optional[PyQtFuture]:
|
|
|
|
def run_and_catch_errs():
|
|
|
|
# Without this, exceptions are silently ignored
|
|
|
|
try:
|
|
|
|
return func(self, *args, **kws)
|
|
|
|
except Exception:
|
|
|
|
traceback.print_exc()
|
2019-05-01 15:23:38 +10:00
|
|
|
logging.error("Exiting thread/process due to exception.")
|
2019-04-20 08:21:19 +10:00
|
|
|
sys.exit(1)
|
|
|
|
finally:
|
|
|
|
del _RUNNING[_RUNNING.index((self.pool, func, args, kws))]
|
|
|
|
|
|
|
|
if max_instances is not None and \
|
|
|
|
_RUNNING.count((self.pool, func, args, kws)) >= max_instances:
|
|
|
|
return None
|
|
|
|
|
|
|
|
_RUNNING.append((self.pool, func, args, kws))
|
2019-04-27 08:47:25 +10:00
|
|
|
future = self.pool.submit(run_and_catch_errs)
|
|
|
|
return PyQtFuture(future, self) if pyqt else future
|
2019-04-20 08:21:19 +10:00
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
return decorator
|