174 lines
6.4 KiB
Python
174 lines
6.4 KiB
Python
"""
|
|
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
|
|
* Copyright (C) 2021-2024 Ad5001
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
"""
|
|
from typing import Callable
|
|
|
|
from PySide6.QtCore import QRunnable, Signal, Property, QObject, Slot, QThreadPool
|
|
from PySide6.QtQml import QJSValue
|
|
|
|
from LogarithmPlotter.util.js import PyJSValue
|
|
|
|
|
|
def check_callable(function: Callable|QJSValue) -> Callable|None:
|
|
"""
|
|
Checks if the given function can be called (either a python callable
|
|
or a QJSValue function), and returns the object that can be called directly.
|
|
Returns None if not a function.
|
|
"""
|
|
if isinstance(function, QJSValue) and function.isCallable():
|
|
return PyJSValue(function)
|
|
elif callable(function):
|
|
return function
|
|
return None
|
|
|
|
class InvalidReturnValue(Exception): pass
|
|
|
|
|
|
class PyPromiseRunner(QRunnable):
|
|
"""
|
|
QRunnable for running Promises in different threads.
|
|
"""
|
|
def __init__(self, runner, promise, args):
|
|
QRunnable.__init__(self)
|
|
self.runner = runner
|
|
self.promise = promise
|
|
self.args = args
|
|
|
|
def run(self):
|
|
try:
|
|
data = self.runner(*self.args)
|
|
if type(data) in [int, str, float, bool]:
|
|
data = QJSValue(data)
|
|
elif data is None:
|
|
data = QJSValue.SpecialValue.UndefinedValue
|
|
elif isinstance(data, QJSValue):
|
|
data = data
|
|
elif isinstance(data, PyJSValue):
|
|
data = data.qjs_value
|
|
else:
|
|
raise InvalidReturnValue("Must return either a primitive, a JS Value, or None.")
|
|
self.promise.fulfilled.emit(data)
|
|
except Exception as e:
|
|
try:
|
|
self.promise.rejected.emit(repr(e))
|
|
except RuntimeError as e2:
|
|
# Happens when the PyPromise has already been garbage collected.
|
|
# In other words, nothing to report to nowhere.
|
|
pass
|
|
|
|
|
|
class PyPromise(QObject):
|
|
"""
|
|
Threaded A+/Promise implementation meant to interface between Python and Javascript easily.
|
|
Runs to_run in another thread, and calls fulfilled (populated by then) with its return value.
|
|
"""
|
|
fulfilled = Signal(QJSValue)
|
|
rejected = Signal(str)
|
|
|
|
def __init__(self, to_run: Callable|QJSValue, args=[], start_automatically=True):
|
|
QObject.__init__(self)
|
|
self._fulfills = []
|
|
self._rejects = []
|
|
self._state = "pending"
|
|
self._started = False
|
|
self.fulfilled.connect(self._fulfill)
|
|
self.rejected.connect(self._reject)
|
|
to_run = check_callable(to_run)
|
|
if to_run is None:
|
|
raise ValueError("New PyPromise created with invalid function")
|
|
self._runner = PyPromiseRunner(to_run, self, args)
|
|
if start_automatically:
|
|
self.start()
|
|
|
|
@Slot()
|
|
def start(self, *args, **kwargs):
|
|
"""
|
|
Starts the thread that will run the promise.
|
|
"""
|
|
if not self._started: # Avoid getting started twice.
|
|
QThreadPool.globalInstance().start(self._runner)
|
|
self._started = True
|
|
|
|
@Property(str)
|
|
def state(self):
|
|
return self._state
|
|
|
|
@Slot(QJSValue, result=QObject)
|
|
@Slot(QJSValue, QJSValue, result=QObject)
|
|
def then(self, on_fulfill: QJSValue | Callable, on_reject: QJSValue | Callable = None):
|
|
"""
|
|
Adds listeners for both fulfilment and catching errors of the Promise.
|
|
"""
|
|
on_fulfill = check_callable(on_fulfill)
|
|
on_reject = check_callable(on_reject)
|
|
self._fulfills.append(on_fulfill)
|
|
self._rejects.append(on_reject)
|
|
return self
|
|
|
|
def calls_upon_fulfillment(self, function: Callable | QJSValue) -> bool:
|
|
"""
|
|
Returns True if the given function will be callback upon the promise fulfillment.
|
|
False otherwise.
|
|
"""
|
|
return self._calls_in(function, self._fulfills)
|
|
|
|
def calls_upon_rejection(self, function: Callable | QJSValue) -> bool:
|
|
"""
|
|
Returns True if the given function will be callback upon the promise rejection.
|
|
False otherwise.
|
|
"""
|
|
return self._calls_in(function, self._rejects)
|
|
|
|
def _calls_in(self, function: Callable | QJSValue, within: list) -> bool:
|
|
"""
|
|
Returns True if the given function resides in the given within list, False otherwise.
|
|
Internal method of calls_upon_fulfill
|
|
"""
|
|
function = check_callable(function)
|
|
ret = False
|
|
if isinstance(function, PyJSValue):
|
|
found = next((f for f in within if f.qjs_value == function.qjs_value), None)
|
|
ret = found is not None
|
|
elif callable(function):
|
|
found = next((f for f in within if f == function), None)
|
|
ret = found is not None
|
|
return ret
|
|
|
|
@Slot(QJSValue)
|
|
@Slot(QObject)
|
|
def _fulfill(self, data):
|
|
self._state = "fulfilled"
|
|
no_return = [None, QJSValue.SpecialValue.UndefinedValue]
|
|
print("Fulfill")
|
|
for i in range(len(self._fulfills)):
|
|
try:
|
|
result = self._fulfills[i](data)
|
|
result = result.qjs_value if isinstance(result, PyJSValue) else result
|
|
data = result if result not in no_return else data # Forward data.
|
|
except Exception as e:
|
|
self._reject(repr(e), start_at=i)
|
|
break
|
|
|
|
@Slot(QJSValue)
|
|
@Slot(str)
|
|
def _reject(self, error, start_at=0):
|
|
self._state = "rejected"
|
|
no_return = [None, QJSValue.SpecialValue.UndefinedValue]
|
|
for i in range(start_at, len(self._rejects)):
|
|
result = self._rejects[i](error)
|
|
result = result.qjs_value if isinstance(result, PyJSValue) else result
|
|
error = result if result not in no_return else error # Forward data.
|