Starting PyPromise

This commit is contained in:
Adsooi 2024-10-15 01:22:25 +02:00
parent b33e1329db
commit f734e40ad9
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
5 changed files with 157 additions and 44 deletions

View file

@ -45,17 +45,17 @@ Popup {
property bool changelogNeedsFetching: true property bool changelogNeedsFetching: true
onAboutToShow: if(changelogNeedsFetching) { onAboutToShow: if(changelogNeedsFetching) {
Helper.fetchChangelog() Helper.fetchChangelog().then((fetchedText) => {
} changelogNeedsFetching = false
changelog.text = fetchedText
Connections {
target: Helper
function onChangelogFetched(chl) {
changelogNeedsFetching = false;
changelog.text = chl
changelogView.contentItem.implicitHeight = changelog.height changelogView.contentItem.implicitHeight = changelog.height
// console.log(changelog.height, changelogView.contentItem.implicitHeight) }, (error) => {
} const e = qsTranslate("changelog", "Could not fetch update: {}.").replace('{}', error)
console.error(e)
changelogNeedsFetching = false
changelog.text = e
changelogView.contentItem.implicitHeight = changelog.height
})
} }
ScrollView { ScrollView {

View file

@ -29,13 +29,16 @@ from urllib.error import HTTPError, URLError
from LogarithmPlotter import __VERSION__ from LogarithmPlotter import __VERSION__
from LogarithmPlotter.util import config from LogarithmPlotter.util import config
from LogarithmPlotter.util.promise import PyPromise
SHOW_GUI_MESSAGES = "--test-build" not in argv SHOW_GUI_MESSAGES = "--test-build" not in argv
CHANGELOG_VERSION = __VERSION__ CHANGELOG_VERSION = __VERSION__
CHANGELOG_CACHE_PATH = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md")
class InvalidFileException(Exception): pass class InvalidFileException(Exception): pass
def show_message(msg: str) -> None: def show_message(msg: str) -> None:
""" """
Shows a GUI message if GUI messages are enabled Shows a GUI message if GUI messages are enabled
@ -46,31 +49,30 @@ def show_message(msg: str) -> None:
raise InvalidFileException(msg) raise InvalidFileException(msg)
def fetch_changelog():
msg_text = "Unknown changelog error."
try:
# Fetching version
r = urlopen("https://api.ad5001.eu/changelog/logarithmplotter/?version=" + CHANGELOG_VERSION)
lines = r.readlines()
r.close()
msg_text = "".join(map(lambda x: x.decode('utf-8'), lines)).strip()
except HTTPError as e:
msg_text = QCoreApplication.translate("changelog", "Could not fetch changelog: Server error {}.").format(
str(e.code))
except URLError as e:
msg_text = QCoreApplication.translate("changelog", "Could not fetch update: {}.").format(str(e.reason))
return msg_text
class ChangelogFetcher(QRunnable):
def __init__(self, helper):
QRunnable.__init__(self)
self.helper = helper
def run(self): def read_changelog():
msg_text = "Unknown changelog error." f = open(CHANGELOG_CACHE_PATH, 'r', -1)
try: data = f.read().strip()
# Fetching version f.close()
r = urlopen("https://api.ad5001.eu/changelog/logarithmplotter/?version=" + CHANGELOG_VERSION) return data
lines = r.readlines()
r.close()
msg_text = "".join(map(lambda x: x.decode('utf-8'), lines)).strip()
except HTTPError as e:
msg_text = QCoreApplication.translate("changelog", "Could not fetch changelog: Server error {}.").format(
str(e.code))
except URLError as e:
msg_text = QCoreApplication.translate("changelog", "Could not fetch update: {}.").format(str(e.reason))
self.helper.changelogFetched.emit(msg_text)
class Helper(QObject): class Helper(QObject):
changelogFetched = Signal(str)
def __init__(self, cwd: str, tmpfile: str): def __init__(self, cwd: str, tmpfile: str):
QObject.__init__(self) QObject.__init__(self)
self.cwd = cwd self.cwd = cwd
@ -150,15 +152,14 @@ class Helper(QObject):
msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}") msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}")
return msg.format(PySide6_version, sys_version.split("\n")[0]) return msg.format(PySide6_version, sys_version.split("\n")[0])
@Slot() @Slot(result=PyPromise)
def fetchChangelog(self): def fetchChangelog(self):
changelog_cache_path = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md") """
if path.exists(changelog_cache_path): Fetches the changelog and returns a Promise.
"""
if path.exists(CHANGELOG_CACHE_PATH):
# We have a cached version of the changelog, for env that don't have access to the internet. # We have a cached version of the changelog, for env that don't have access to the internet.
f = open(changelog_cache_path); return PyPromise(read_changelog)
self.changelogFetched.emit("".join(f.readlines()).strip())
f.close()
else: else:
# Fetch it from the internet. # Fetch it from the internet.
runnable = ChangelogFetcher(self) return PyPromise(fetch_changelog)
QThreadPool.globalInstance().start(runnable)

View file

@ -16,13 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from re import Pattern from re import Pattern
from typing import Callable
from PySide6.QtCore import QMetaObject, QObject, QDateTime from PySide6.QtCore import QMetaObject, QObject, QDateTime
from PySide6.QtQml import QJSValue from PySide6.QtQml import QJSValue
class InvalidAttributeValueException(Exception): pass class InvalidAttributeValueException(Exception): pass
class NotAPrimitiveException(Exception): pass class NotAPrimitiveException(Exception): pass
class Function: pass
class URL: pass class URL: pass
class PyJSValue: class PyJSValue:
@ -78,7 +78,7 @@ class PyJSValue:
matcher = [ matcher = [
(lambda: self.qjs_value.isArray(), list), (lambda: self.qjs_value.isArray(), list),
(lambda: self.qjs_value.isBool(), bool), (lambda: self.qjs_value.isBool(), bool),
(lambda: self.qjs_value.isCallable(), Function), (lambda: self.qjs_value.isCallable(), Callable),
(lambda: self.qjs_value.isDate(), QDateTime), (lambda: self.qjs_value.isDate(), QDateTime),
(lambda: self.qjs_value.isError(), Exception), (lambda: self.qjs_value.isError(), Exception),
(lambda: self.qjs_value.isNull(), None), (lambda: self.qjs_value.isNull(), None),
@ -104,3 +104,5 @@ class PyJSValue:
if self.type() not in [bool, float, str, None]: if self.type() not in [bool, float, str, None]:
raise NotAPrimitiveException() raise NotAPrimitiveException()
return self.qjs_value.toPrimitive().toVariant() return self.qjs_value.toPrimitive().toVariant()

View file

@ -16,18 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from PySide6.QtCore import QObject, Slot, Property, QCoreApplication from PySide6.QtCore import QObject, Slot, Property, QCoreApplication, Signal
from PySide6.QtGui import QImage, QColor from PySide6.QtGui import QImage, QColor
from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QMessageBox
from os import path, remove, environ, makedirs from os import path, remove, makedirs
from string import Template from string import Template
from subprocess import Popen, TimeoutExpired, PIPE from subprocess import Popen, TimeoutExpired, PIPE
from hashlib import sha512 from hashlib import sha512
from shutil import which from shutil import which
from sys import argv from sys import argv
""" """
Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/) Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/)
installation and collects the binary path in the DVIPNG_PATH variable. installation and collects the binary path in the DVIPNG_PATH variable.
@ -76,6 +75,7 @@ class Latex(QObject):
dvipng to be installed on the system. dvipng to be installed on the system.
""" """
def __init__(self, cache_path): def __init__(self, cache_path):
QObject.__init__(self) QObject.__init__(self)
self.tempdir = path.join(cache_path, "latex") self.tempdir = path.join(cache_path, "latex")

View file

@ -0,0 +1,110 @@
"""
* 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, QObject, Slot, QThreadPool
from PySide6.QtQml import QJSValue
from LogarithmPlotter.util.js import PyJSValue
class InvalidReturnValue(Exception): pass
class PyPromiseRunner(QRunnable):
"""
QRunnable for running Promises in different threads.
"""
def __init__(self, runner, promise):
QRunnable.__init__(self)
self.runner = runner
self.promise = promise
print("Initialized", self.runner)
def run(self):
try:
data = self.runner()
if isinstance(data, QObject):
data = data
elif type(data) in [int, str, float, bool, bytes]:
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 valid QObject, JS Value, or None.")
self.promise.finished.emit(data)
except Exception as e:
self.promise.errored.emit(repr(e))
class PyPromise(QObject):
"""
Asynchronous Promise-like object meant to interface between Python and Javascript easily.
Runs to_run in another thread, and calls fulfilled (populated by then) with its return value.
"""
finished = Signal((QJSValue,), (QObject,))
errored = Signal(Exception)
def __init__(self, to_run: Callable):
QObject.__init__(self)
self._fulfills = []
self._rejects = []
self.finished.connect(self._fulfill)
self.errored.connect(self._reject)
self._runner = PyPromiseRunner(to_run, self)
QThreadPool.globalInstance().start(self._runner)
@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.
"""
if isinstance(on_fulfill, QJSValue):
self._fulfills.append(PyJSValue(on_fulfill))
elif isinstance(on_fulfill, Callable):
self._fulfills.append(on_fulfill)
if isinstance(on_reject, QJSValue):
self._rejects.append(PyJSValue(on_reject))
elif isinstance(on_reject, Callable):
self._rejects.append(on_reject)
@Slot(QJSValue)
@Slot(QObject)
def _fulfill(self, data):
no_return = [None, QJSValue.SpecialValue.UndefinedValue]
for on_fulfill in self._fulfills:
try:
result = on_fulfill(data)
data = result if result not in no_return else data # Forward data.
except Exception as e:
self._reject(repr(e))
break
@Slot(QJSValue)
@Slot(str)
def _reject(self, error):
no_return = [None, QJSValue.SpecialValue.UndefinedValue]
for on_reject in self._rejects:
result = on_reject(error)
error = result if result not in no_return else error # Forward data.