From ccddb068a670ceb607c9e034c4dfb0accdebf2e9 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 15 Oct 2024 18:06:24 +0200 Subject: [PATCH 1/3] Fixing tests for Promises (new ones need to be written) --- runtime-pyside6/.coverage | Bin 0 -> 53248 bytes .../LogarithmPlotter/util/promise.py | 22 +++-- runtime-pyside6/tests/test_helper.py | 83 ++++++++---------- runtime-pyside6/tests/test_latex.py | 10 +-- 4 files changed, 57 insertions(+), 58 deletions(-) create mode 100644 runtime-pyside6/.coverage diff --git a/runtime-pyside6/.coverage b/runtime-pyside6/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..8eab2ed6efa5c8f1c8ebd2f83154347499e04eff GIT binary patch literal 53248 zcmeI4Uu+yl9mn^dZ*T99CykY!ToW=tsbkfDVxWm=B~q70q7X<+LtjwY?%j>;EqA-; z-95)fRM9m>k&s#m5aI#aClC+tzynD92`LpX1PM@t5J*rWifHpd2^B4hlm_#g{o}if z?W!+6tMPYrH?uoCGr!;OGryVHxxL+oA3E+vg3tRw(~0;8l|728D)(}(D2hejGJUhn zq#YwWpjhqM-)h%VroXUMW{)VP+|vsCT=@i>Dt)VbNAY5*X??l)oatCKI)M!YKmY_l zV0RPfJXA6(qoeARPee|&A)>&ki69BfAN};Pxs%8E$+`QEALB_HKUmCgD*wD# zos2W2(ju`EB?(lCa2Gt8gC9(bPw+s@i$Hia5hhw3bn6p^i8H4QW@ThVJu70HoS;UZ zVxKjX&^6_Od8(-92X(1^HE_Jzq6jCsvuY>vuyh+J4ds=v&l=J)yYqy{jO@(8fy>_!o z$RBSvTe5MeL?Sryw<@_+*~m|gecNrbNn3X9?Q6~qxopv-RNHfx+G4%&CwaHzWNe1# zPjYDldBFRNxypw}RE4}!tb|Jqy56##cI3y$HZ{IIOF`+WTk>Y*zybB_QtT)ak6m@6 zaDB(>bDH(!EnG*Q-`a0vFX?v@*^3$t?$5_vMwaGDf#bIO|k#E#p zCX?dhL%GU5`?gYCs$kE`Fn8v&TCDQf(U^Xd27UR}`&5=GU6nr}R;QMo28|{y#|@~< z(ioh~09l$~qR7$77wc$wdzHC#lnA{fl;* zO~s4kg}?sp;)xlO@kmROnCu}(Q5uLXim(p)+*uD_KV$kwuFWhht4d3h<SLAkPXI@z4^MxfKvrq&vcfL2 zH{O{BLTeBJ0T2KI5C8!X009sH0T2KI5CDOUKuO=PX1@Yx1%1p)e*}>C|BJ;jg?)wX zAO7C(-Q{1EKT~?8v|L;(K26EkKmY_l00ck)1V8`;Kw!5JXcY~mBW9YS?m9D0{jNiY zW@o~%E+0~!nQOJendAO~6S&c0^TCE6MIzXk5VSq{Q1Mi2HFWFZ@Wu@7$ZgEDTU4i@ zhnHK00ZH`eH()%aoT`4ipNTNG{ zcC>BRb0ga(nN5{s+S@C$N1VL>Uo5^$VP7pN?A^oPAHJu2p>(-)s`yO#5lY4e0w4ea zAOHd&00JNY0=tI5lFwhyaItzo4)?NSS2IV^I>;IuawPCjYHwIm;$mD4{x@{6DUY;5d3dks2whoLDy{?G5mXFv8A`^2|bZ`CPn zS|=l}`)fyE|N2wc_l{pZ{_k>5qx4}-wN%5hZq<|zJ*UUd|Fa({>>7KWegSxyU1EP? z7um1bFWEUt#s&f)00JNY0w4eaAOHd&00JNY0wAzc1mv#-YOxqcg+d%zRvhK?ab%it zl*`4@&`=y1MjYvS9BGH^_To3>O5C8!X009sH0T2KI5C8!X7zhD* z{~zoBfuM#E5C8!X009sH0T2KI5C8!X009u#F#^f+{}<%{|NqNgWB+8Yu$S12^bEl7 z*>Bl}9a9T}K>!3m00ck)1V8`;KmY_l00ck)1U4g}7wK*OdO-%34DvECWssA>kPHkN M=rYh`Am9K0e?Y`G00000 literal 0 HcmV?d00001 diff --git a/runtime-pyside6/LogarithmPlotter/util/promise.py b/runtime-pyside6/LogarithmPlotter/util/promise.py index f129d41..41b916d 100644 --- a/runtime-pyside6/LogarithmPlotter/util/promise.py +++ b/runtime-pyside6/LogarithmPlotter/util/promise.py @@ -17,7 +17,7 @@ """ from typing import Callable -from PySide6.QtCore import QRunnable, Signal, QObject, Slot, QThreadPool +from PySide6.QtCore import QRunnable, Signal, Property, QObject, Slot, QThreadPool from PySide6.QtQml import QJSValue from LogarithmPlotter.util.js import PyJSValue @@ -51,10 +51,10 @@ class PyPromiseRunner(QRunnable): data = data.qjs_value else: raise InvalidReturnValue("Must return either a primitive, a valid QObject, JS Value, or None.") - self.promise.finished.emit(data) + self.promise.fulfilled.emit(data) except Exception as e: try: - self.promise.errored.emit(repr(e)) + 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. @@ -66,18 +66,22 @@ 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) + fulfilled = Signal((QJSValue,), (QObject,)) + rejected = Signal(Exception) - def __init__(self, to_run: Callable, args): + def __init__(self, to_run: Callable, args=[]): QObject.__init__(self) self._fulfills = [] self._rejects = [] - self.finished.connect(self._fulfill) - self.errored.connect(self._reject) + self._state = "pending" + self.fulfilled.connect(self._fulfill) + self.rejected.connect(self._reject) self._runner = PyPromiseRunner(to_run, self, args) QThreadPool.globalInstance().start(self._runner) + @Property(str) + def state(self): + return self._state @Slot(QJSValue, result=QObject) @Slot(QJSValue, QJSValue, result=QObject) @@ -98,6 +102,7 @@ class PyPromise(QObject): @Slot(QJSValue) @Slot(QObject) def _fulfill(self, data): + self._state = "fulfilled" no_return = [None, QJSValue.SpecialValue.UndefinedValue] for on_fulfill in self._fulfills: try: @@ -110,6 +115,7 @@ class PyPromise(QObject): @Slot(QJSValue) @Slot(str) def _reject(self, error): + self._state = "rejected" no_return = [None, QJSValue.SpecialValue.UndefinedValue] for on_reject in self._rejects: result = on_reject(error) diff --git a/runtime-pyside6/tests/test_helper.py b/runtime-pyside6/tests/test_helper.py index 73dfb23..5cfb239 100644 --- a/runtime-pyside6/tests/test_helper.py +++ b/runtime-pyside6/tests/test_helper.py @@ -17,7 +17,7 @@ """ import pytest -from os import getcwd, remove +from os import getcwd, remove, path from os.path import join from tempfile import TemporaryDirectory from json import loads @@ -25,11 +25,12 @@ from shutil import copy2 from PySide6.QtCore import QObject, Signal, QThreadPool from PySide6.QtGui import QImage +from PySide6.QtQml import QJSValue from PySide6.QtWidgets import QApplication from LogarithmPlotter import __VERSION__ as version from LogarithmPlotter.util import config, helper -from LogarithmPlotter.util.helper import ChangelogFetcher, Helper, InvalidFileException +from LogarithmPlotter.util.helper import Helper, InvalidFileException pwd = getcwd() helper.SHOW_GUI_MESSAGES = False @@ -43,41 +44,45 @@ def temporary(): directory.cleanup() -class MockHelperSignals(QObject): - changelogFetched = Signal(str) +def create_changelog_callback_asserter(promise, expect_404=False): + def cb(changelog, expect_404=expect_404): + # print("Got changelog", changelog) + assert isinstance(changelog, QJSValue) + assert changelog.isString() + changlogValue = changelog.toVariant() + assert ('404' in changlogValue) == expect_404 + def error(e): + raise eval(e) + promise.then(cb, error) - def __init__(self, expect_404): - QObject.__init__(self) - self.expect_404 = expect_404 - self.changelogFetched.connect(self.changelog_fetched) - self.changelog = None - - def changelog_fetched(self, changelog): - self.changelog = changelog - - -class TestChangelog: - - def test_exists(self, qtbot): - helper.CHANGELOG_VERSION = '0.5.0' - mock_helper = MockHelperSignals(False) - fetcher = ChangelogFetcher(mock_helper) - fetcher.run() # Does not raise an exception - qtbot.waitSignal(mock_helper.changelogFetched, timeout=10000) - assert type(mock_helper.changelog) == str - assert '404' not in mock_helper.changelog - - def tests_no_exist(self, qtbot): - mock_helper = MockHelperSignals(True) - helper.CHANGELOG_VERSION = '1.0.0' - fetcher = ChangelogFetcher(mock_helper) - fetcher.run() - qtbot.waitSignal(mock_helper.changelogFetched, timeout=10000) - assert type(mock_helper.changelog) == str - assert '404' in mock_helper.changelog +CHANGELOG_BASE_PATH = path.realpath(path.join(path.dirname(path.realpath(__file__)), "..", "CHANGELOG.md")) class TestHelper: + def test_changelog(self, temporary, qtbot): + helper.CHANGELOG_VERSION = '0.5.0' + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + promise = obj.fetchChangelog() + create_changelog_callback_asserter(promise, expect_404=False) + qtbot.waitSignal(promise.fulfilled, timeout=10000) + # No exist + helper.CHANGELOG_VERSION = '2.0.0' + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + promise = obj.fetchChangelog() + create_changelog_callback_asserter(promise, expect_404=True) + qtbot.waitSignal(promise.fulfilled, timeout=10000) + # Local + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + assert path.exists(CHANGELOG_BASE_PATH) + copy2(CHANGELOG_BASE_PATH, helper.CHANGELOG_CACHE_PATH) + assert path.exists(helper.CHANGELOG_CACHE_PATH) + promise = obj.fetchChangelog() + create_changelog_callback_asserter(promise, expect_404=False) + qtbot.waitSignal(promise.fulfilled, timeout=10000) # Local + def test_read(self, temporary): # Test file reading and information loading. tmpfile, directory = temporary @@ -168,15 +173,3 @@ class TestHelper: obj.setSetting("last_install_greet", obj.getSetting("last_install_greet")) obj.setSetting("check_for_updates", obj.getSetting("check_for_updates")) obj.setSetting("default_graph.xzoom", obj.getSetting("default_graph.xzoom")) - - def test_fetch_changelog(self, temporary, qtbot): - tmpfile, directory = temporary - obj = Helper(pwd, tmpfile) - copy2("../../CHANGELOG.md", "../../LogarithmPlotter/util/CHANGELOG.md") - obj.fetchChangelog() - assert QThreadPool.globalInstance().activeThreadCount() == 0 - qtbot.waitSignal(obj.changelogFetched, timeout=10000) - remove("../../LogarithmPlotter/util/CHANGELOG.md") - obj.fetchChangelog() - assert QThreadPool.globalInstance().activeThreadCount() > 0 - qtbot.waitSignal(obj.changelogFetched, timeout=10000) \ No newline at end of file diff --git a/runtime-pyside6/tests/test_latex.py b/runtime-pyside6/tests/test_latex.py index d74f9f1..6fd5c97 100644 --- a/runtime-pyside6/tests/test_latex.py +++ b/runtime-pyside6/tests/test_latex.py @@ -54,8 +54,8 @@ class TestLatex: # Reset [latex.DVIPNG_PATH, latex.LATEX_PATH] = bkp - def test_render(self, latex_obj: latex.Latex) -> None: - result = latex_obj.render(r"\frac{d\sqrt{\mathrm{f}(x \times 2.3)}}{dx}", 14, QColor(0, 0, 0, 255)) + def test_render_sync(self, latex_obj: latex.Latex) -> None: + result = latex_obj.renderSync(r"\frac{d\sqrt{\mathrm{f}(x \times 2.3)}}{dx}", 14, QColor(0, 0, 0, 255)) # Ensure result format assert type(result) == str [path, width, height] = result.split(",") @@ -64,17 +64,17 @@ class TestLatex: assert match(r"\d+", height) # Ensure it returns errors on invalid latex. with pytest.raises(latex.RenderError): - latex_obj.render(r"\nonexistant", 14, QColor(0, 0, 0, 255)) + latex_obj.renderSync(r"\nonexistant", 14, QColor(0, 0, 0, 255)) # Replace latex bin with one that returns errors bkp = latex.LATEX_PATH latex.LATEX_PATH = which("false") with pytest.raises(latex.RenderError): - latex_obj.render(r"\mathrm{f}(x)", 14, QColor(0, 0, 0, 255)) + latex_obj.renderSync(r"\mathrm{f}(x)", 14, QColor(0, 0, 0, 255)) latex.LATEX_PATH = bkp def test_prerendered(self, latex_obj: latex.Latex) -> None: args = [r"\frac{d\sqrt{\mathrm{f}(x \times 2.3)}}{dx}", 14, QColor(0, 0, 0, 255)] - latex_obj.render(*args) + latex_obj.renderSync(*args) prerendered = latex_obj.findPrerendered(*args) assert type(prerendered) == str [path, width, height] = prerendered.split(",") From aeaaba759f90a86100c69cabf08531f385d50e85 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 15 Oct 2024 19:21:40 +0200 Subject: [PATCH 2/3] Starting latex render locking. --- common/src/module/canvas.mjs | 4 + runtime-pyside6/.coverage | Bin 53248 -> 0 bytes .../LogarithmPlotter/util/latex.py | 69 ++++++++++++++++-- .../LogarithmPlotter/util/promise.py | 43 ++++++++--- 4 files changed, 99 insertions(+), 17 deletions(-) delete mode 100644 runtime-pyside6/.coverage diff --git a/common/src/module/canvas.mjs b/common/src/module/canvas.mjs index 62649cf..6abbf2d 100644 --- a/common/src/module/canvas.mjs +++ b/common/src/module/canvas.mjs @@ -529,11 +529,15 @@ class CanvasAPI extends Module { this.#canvas.loadImageAsync(imgData.source).then(() => { if(this.#redrawCount === currentRedrawCount) callback(imgData) + else + console.log("2Discard render of", imgData.source, this.#redrawCount, currentRedrawCount) }) } else { // Callback directly if(this.#redrawCount === currentRedrawCount) callback(imgData) + else + console.log("2Discard render of", imgData.source, this.#redrawCount, currentRedrawCount) } } const prerendered = Latex.findPrerendered(ltxText, this.textsize, color) diff --git a/runtime-pyside6/.coverage b/runtime-pyside6/.coverage deleted file mode 100644 index 8eab2ed6efa5c8f1c8ebd2f83154347499e04eff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI4Uu+yl9mn^dZ*T99CykY!ToW=tsbkfDVxWm=B~q70q7X<+LtjwY?%j>;EqA-; z-95)fRM9m>k&s#m5aI#aClC+tzynD92`LpX1PM@t5J*rWifHpd2^B4hlm_#g{o}if z?W!+6tMPYrH?uoCGr!;OGryVHxxL+oA3E+vg3tRw(~0;8l|728D)(}(D2hejGJUhn zq#YwWpjhqM-)h%VroXUMW{)VP+|vsCT=@i>Dt)VbNAY5*X??l)oatCKI)M!YKmY_l zV0RPfJXA6(qoeARPee|&A)>&ki69BfAN};Pxs%8E$+`QEALB_HKUmCgD*wD# zos2W2(ju`EB?(lCa2Gt8gC9(bPw+s@i$Hia5hhw3bn6p^i8H4QW@ThVJu70HoS;UZ zVxKjX&^6_Od8(-92X(1^HE_Jzq6jCsvuY>vuyh+J4ds=v&l=J)yYqy{jO@(8fy>_!o z$RBSvTe5MeL?Sryw<@_+*~m|gecNrbNn3X9?Q6~qxopv-RNHfx+G4%&CwaHzWNe1# zPjYDldBFRNxypw}RE4}!tb|Jqy56##cI3y$HZ{IIOF`+WTk>Y*zybB_QtT)ak6m@6 zaDB(>bDH(!EnG*Q-`a0vFX?v@*^3$t?$5_vMwaGDf#bIO|k#E#p zCX?dhL%GU5`?gYCs$kE`Fn8v&TCDQf(U^Xd27UR}`&5=GU6nr}R;QMo28|{y#|@~< z(ioh~09l$~qR7$77wc$wdzHC#lnA{fl;* zO~s4kg}?sp;)xlO@kmROnCu}(Q5uLXim(p)+*uD_KV$kwuFWhht4d3h<SLAkPXI@z4^MxfKvrq&vcfL2 zH{O{BLTeBJ0T2KI5C8!X009sH0T2KI5CDOUKuO=PX1@Yx1%1p)e*}>C|BJ;jg?)wX zAO7C(-Q{1EKT~?8v|L;(K26EkKmY_l00ck)1V8`;Kw!5JXcY~mBW9YS?m9D0{jNiY zW@o~%E+0~!nQOJendAO~6S&c0^TCE6MIzXk5VSq{Q1Mi2HFWFZ@Wu@7$ZgEDTU4i@ zhnHK00ZH`eH()%aoT`4ipNTNG{ zcC>BRb0ga(nN5{s+S@C$N1VL>Uo5^$VP7pN?A^oPAHJu2p>(-)s`yO#5lY4e0w4ea zAOHd&00JNY0=tI5lFwhyaItzo4)?NSS2IV^I>;IuawPCjYHwIm;$mD4{x@{6DUY;5d3dks2whoLDy{?G5mXFv8A`^2|bZ`CPn zS|=l}`)fyE|N2wc_l{pZ{_k>5qx4}-wN%5hZq<|zJ*UUd|Fa({>>7KWegSxyU1EP? z7um1bFWEUt#s&f)00JNY0w4eaAOHd&00JNY0wAzc1mv#-YOxqcg+d%zRvhK?ab%it zl*`4@&`=y1MjYvS9BGH^_To3>O5C8!X009sH0T2KI5C8!X7zhD* z{~zoBfuM#E5C8!X009sH0T2KI5C8!X009u#F#^f+{}<%{|NqNgWB+8Yu$S12^bEl7 z*>Bl}9a9T}K>!3m00ck)1V8`;KmY_l00ck)1U4g}7wK*OdO-%34DvECWssA>kPHkN M=rYh`Am9K0e?Y`G00000 diff --git a/runtime-pyside6/LogarithmPlotter/util/latex.py b/runtime-pyside6/LogarithmPlotter/util/latex.py index 0d720c3..2db62eb 100644 --- a/runtime-pyside6/LogarithmPlotter/util/latex.py +++ b/runtime-pyside6/LogarithmPlotter/util/latex.py @@ -82,6 +82,7 @@ class Latex(QObject): def __init__(self, cache_path): QObject.__init__(self) self.tempdir = path.join(cache_path, "latex") + self.render_pipeline_locks = {} makedirs(self.tempdir, exist_ok=True) @Property(bool) @@ -116,20 +117,71 @@ class Latex(QObject): except MissingPackageException: valid_install = False # Should have sent an error message if failed to render return valid_install + + def lock(self, markup_hash, render_hash, promise): + """ + Locks the render pipeline for a given markup hash and render hash. + """ + # print("Locking", markup_hash, render_hash) + if markup_hash not in self.render_pipeline_locks: + self.render_pipeline_locks[markup_hash] = promise + self.render_pipeline_locks[render_hash] = promise + + + def release_lock(self, markup_hash, render_hash): + """ + Release locks on the markup and render hashes. + """ + # print("Releasing", markup_hash, render_hash) + if markup_hash in self.render_pipeline_locks: + del self.render_pipeline_locks[markup_hash] + del self.render_pipeline_locks[render_hash] @Slot(str, float, QColor, result=PyPromise) def renderAsync(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise: """ Prepares and renders a latex string into a png file asynchronously. """ - return PyPromise(self.renderSync, [latex_markup, font_size, color]) + markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color) + promise = None + if render_hash in self.render_pipeline_locks: + # A PyPromise for this specific render is already running. + # print("Already running render of", latex_markup) + promise = self.render_pipeline_locks[render_hash] + elif markup_hash in self.render_pipeline_locks: + # A PyPromise with the same markup, but not the same color or font size is already running. + print("Chaining render of", latex_markup) + existing_promise = self.render_pipeline_locks[markup_hash] + promise = self._create_async_promise(latex_markup, font_size, color) + existing_promise.then(lambda x, latex_markup=latex_markup: print("> Starting chained render of", latex_markup)) + promise.then(lambda x, latex_markup=latex_markup: print("> Fulfilled chained render of", latex_markup, "\n with", x.toVariant())) + existing_promise.then(promise.start) + else: + # No such PyPromise is running. + promise = self._create_async_promise(latex_markup, font_size, color) + promise.start() + return promise + + def _create_async_promise(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise: + """ + Createsa PyPromise to render a latex string into a PNG file. + Internal method. Use renderAsync that makes use of locks. + """ + markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color) + promise = PyPromise(self.renderSync, [latex_markup, font_size, color], start_automatically=False) + self.lock(markup_hash, render_hash, promise) + # Make the lock release at the end. + def unlock(data, markup_hash=markup_hash, render_hash=render_hash): + self.release_lock(markup_hash, render_hash) + promise.then(unlock, unlock) + return promise @Slot(str, float, QColor, result=str) def renderSync(self, latex_markup: str, font_size: float, color: QColor) -> str: """ Prepares and renders a latex string into a png file. """ - markup_hash, export_path = self.create_export_path(latex_markup, font_size, color) + markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color) if self.latexSupported and not path.exists(export_path + ".png"): print("Rendering", latex_markup, export_path) # Generating file @@ -138,14 +190,12 @@ class Latex(QObject): if not path.exists(latex_path + ".dvi"): self.create_latex_doc(latex_path, latex_markup) self.convert_latex_to_dvi(latex_path) - # self.cleanup(latex_path) + self.cleanup(latex_path) # Creating four pictures of different sizes to better handle dpi. self.convert_dvi_to_png(latex_path, export_path, font_size, color) # self.convert_dvi_to_png(latex_path, export_path+"@2", font_size*2, color) # self.convert_dvi_to_png(latex_path, export_path+"@3", font_size*3, color) # self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, color) - else: - sleep(0) img = QImage(export_path) # Small hack, not very optimized since we load the image twice, but you can't pass a QImage to QML and expect it to be loaded return f'{export_path}.png,{img.width()},{img.height()}' @@ -155,7 +205,7 @@ class Latex(QObject): """ Finds a prerendered image and returns its data if possible, and an empty string if not. """ - markup_hash, export_path = self.create_export_path(latex_markup, font_size, color) + markup_hash, render_hash, export_path = self.create_export_path(latex_markup, font_size, color) data = "" if path.exists(export_path + ".png"): img = QImage(export_path) @@ -165,10 +215,13 @@ class Latex(QObject): def create_export_path(self, latex_markup: str, font_size: float, color: QColor): """ Standardizes export path for renders. + Markup hash is unique for the markup + Render hash is unique for the markup, the font size and the color. """ markup_hash = "render" + str(sha512(latex_markup.encode()).hexdigest()) - export_path = path.join(self.tempdir, f'{markup_hash}_{int(font_size)}_{color.rgb()}') - return markup_hash, export_path + render_hash = f'{markup_hash}_{int(font_size)}_{color.rgb()}' + export_path = path.join(self.tempdir, render_hash) + return markup_hash, render_hash, export_path def create_latex_doc(self, export_path: str, latex_markup: str): """ diff --git a/runtime-pyside6/LogarithmPlotter/util/promise.py b/runtime-pyside6/LogarithmPlotter/util/promise.py index 41b916d..e475b36 100644 --- a/runtime-pyside6/LogarithmPlotter/util/promise.py +++ b/runtime-pyside6/LogarithmPlotter/util/promise.py @@ -23,6 +23,18 @@ 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 isinstance(function, Callable): + return function + return None + class InvalidReturnValue(Exception): pass @@ -69,16 +81,30 @@ class PyPromise(QObject): fulfilled = Signal((QJSValue,), (QObject,)) rejected = Signal(Exception) - def __init__(self, to_run: Callable, args=[]): + 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) - QThreadPool.globalInstance().start(self._runner) - + 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 @@ -89,13 +115,11 @@ class PyPromise(QObject): """ 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): + on_fulfill = check_callable(on_fulfill) + on_reject = check_callable(on_reject) + if on_fulfill is not None: self._fulfills.append(on_fulfill) - if isinstance(on_reject, QJSValue): - self._rejects.append(PyJSValue(on_reject)) - elif isinstance(on_reject, Callable): + if on_reject is not None: self._rejects.append(on_reject) return self @@ -107,6 +131,7 @@ class PyPromise(QObject): for on_fulfill in self._fulfills: try: result = on_fulfill(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)) From a85a4721e3988756e3da1524219381214f8d599b Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 15 Oct 2024 20:39:03 +0200 Subject: [PATCH 3/3] Fixing double redraw when opening a file. --- common/src/module/canvas.mjs | 2 +- common/src/module/io.mjs | 1 - runtime-pyside6/LogarithmPlotter/util/debug.py | 3 +++ runtime-pyside6/LogarithmPlotter/util/latex.py | 4 +--- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/src/module/canvas.mjs b/common/src/module/canvas.mjs index 6abbf2d..913442f 100644 --- a/common/src/module/canvas.mjs +++ b/common/src/module/canvas.mjs @@ -530,7 +530,7 @@ class CanvasAPI extends Module { if(this.#redrawCount === currentRedrawCount) callback(imgData) else - console.log("2Discard render of", imgData.source, this.#redrawCount, currentRedrawCount) + console.log("1Discard render of", imgData.source, this.#redrawCount, currentRedrawCount) }) } else { // Callback directly diff --git a/common/src/module/io.mjs b/common/src/module/io.mjs index 472b061..92096e9 100644 --- a/common/src/module/io.mjs +++ b/common/src/module/io.mjs @@ -201,7 +201,6 @@ class IOAPI extends Module { // TODO: Error handling return } - Canvas.redraw() this.#alert.show(qsTranslate("io", "Loaded file '%1'.").arg(basename)) this.#saved = true this.emit(new LoadedEvent()) diff --git a/runtime-pyside6/LogarithmPlotter/util/debug.py b/runtime-pyside6/LogarithmPlotter/util/debug.py index 8b57687..f899fd5 100644 --- a/runtime-pyside6/LogarithmPlotter/util/debug.py +++ b/runtime-pyside6/LogarithmPlotter/util/debug.py @@ -19,10 +19,12 @@ from PySide6.QtCore import QtMsgType, qInstallMessageHandler, QMessageLogContext from math import ceil, log10 from os import path +from re import compile CURRENT_PATH = path.dirname(path.realpath(__file__)) SOURCEMAP_PATH = path.realpath(f"{CURRENT_PATH}/../qml/eu/ad5001/LogarithmPlotter/js/index.mjs.map") SOURCEMAP_INDEX = None +INDEX_REG = compile(r"build\/runtime-pyside6\/LogarithmPlotter\/qml\/eu\/ad5001\/LogarithmPlotter\/js\/index.mjs:(\d+)") class LOG_COLORS: @@ -77,6 +79,7 @@ def create_log_terminal_message(mode: QtMsgType, context: QMessageLogContext, me # Check MJS if line is not None and source_file is not None and source_file.endswith("index.mjs"): source_file, line = map_javascript_source(source_file, line) + # Parse message prefix = f"{LOG_COLORS.INVERT}{mode[1]}[{mode[0].upper()}]{LOG_COLORS.RESET_INVERT}" message = message + LOG_COLORS.RESET context = f"{context.function} at {source_file}:{line}" diff --git a/runtime-pyside6/LogarithmPlotter/util/latex.py b/runtime-pyside6/LogarithmPlotter/util/latex.py index 2db62eb..f36f07f 100644 --- a/runtime-pyside6/LogarithmPlotter/util/latex.py +++ b/runtime-pyside6/LogarithmPlotter/util/latex.py @@ -150,11 +150,9 @@ class Latex(QObject): promise = self.render_pipeline_locks[render_hash] elif markup_hash in self.render_pipeline_locks: # A PyPromise with the same markup, but not the same color or font size is already running. - print("Chaining render of", latex_markup) + # print("Chaining render of", latex_markup) existing_promise = self.render_pipeline_locks[markup_hash] promise = self._create_async_promise(latex_markup, font_size, color) - existing_promise.then(lambda x, latex_markup=latex_markup: print("> Starting chained render of", latex_markup)) - promise.then(lambda x, latex_markup=latex_markup: print("> Fulfilled chained render of", latex_markup, "\n with", x.toVariant())) existing_promise.then(promise.start) else: # No such PyPromise is running.