From 501209f0d73c82cc4c7aff622dfad70c73c5d73a Mon Sep 17 00:00:00 2001 From: John Travers Date: Fri, 13 Nov 2026 20:34:45 +0000 Subject: [PATCH] GUI: take a route's description once, so its sheet is built once A setup's route is described ahead (the warm-up prefetch) and again when the setup opens if no answer has been taken yet. When the window's thread is slow, the reply filter accepts the first answer before the second question is sent, so both answers reach the document, `described` fires twice and the sheet is rebuilt, deleting every widget of the first build. Tests that held a derived number's label across that rebuild failed on Windows CI with "Internal C++ object (QLabel) already deleted". A second answer for a known route is now ignored. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XGENPwwkUUG9KSAwneHjgH --- changelog.d/791-sheet-built-twice.fixed.md | 4 ++++ gui/src/solprop_gui/ui/document.py | 7 +++++-- gui/tests/test_ui_workflow.py | 23 +++++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 changelog.d/791-sheet-built-twice.fixed.md diff --git a/changelog.d/791-sheet-built-twice.fixed.md b/changelog.d/791-sheet-built-twice.fixed.md new file mode 100644 index 000000000..d6c5d9a37 --- /dev/null +++ b/changelog.d/791-sheet-built-twice.fixed.md @@ -0,0 +1,4 @@ +The desktop front end no longer builds a class's sheet twice when the kernel answers its +route's description twice, once for the description fetched ahead and once for the one +asked as the setup opened; the second build replaced every widget on the sheet for +nothing ([#791](https://github.com/LupoLab/solprop/issues/791)). diff --git a/gui/src/solprop_gui/ui/document.py b/gui/src/solprop_gui/ui/document.py index 77ac1eed7..7f06d029d 100644 --- a/gui/src/solprop_gui/ui/document.py +++ b/gui/src/solprop_gui/ui/document.py @@ -268,8 +268,11 @@ def on_reply(self, reply: Reply) -> None: adapt = self._adapt.pop(reply.request_id, None) if purpose.startswith("describe-") and reply.ok and isinstance(value, dict): route = str(value["route"]) - self.descriptions[route] = value - self.described.emit(route) + # A route asked ahead and asked again as its setup opened can be + # answered twice; a second answer would rebuild the sheet for nothing. + if route not in self.descriptions: + self.descriptions[route] = value + self.described.emit(route) elif purpose == "example" and reply.ok and isinstance(value, str): model = RecipeModel(value) if adapt is not None: diff --git a/gui/tests/test_ui_workflow.py b/gui/tests/test_ui_workflow.py index cc93ffd06..d2c2e8463 100644 --- a/gui/tests/test_ui_workflow.py +++ b/gui/tests/test_ui_workflow.py @@ -11,10 +11,11 @@ import numpy as np import pytest -from PySide6.QtCore import QEvent, QSettings, QStandardPaths, Qt +from PySide6.QtCore import QCoreApplication, QEvent, QSettings, QStandardPaths, Qt from PySide6.QtGui import QFontDatabase, QStandardItemModel from PySide6.QtWidgets import QBoxLayout, QLabel, QLineEdit, QWidget from pytestqt.qtbot import QtBot +from shiboken6 import isValid from solprop.constants import C from solprop.output import open_file from solprop.service.messages import Event @@ -1436,6 +1437,26 @@ def test_a_step_index_fibre_solves_its_mode_for_its_numbers( ) +def test_a_second_answer_to_a_routes_description_leaves_its_sheet_standing( + window: WorkflowWindow, qtbot: QtBot +) -> None: + # A route asked ahead and asked again as its setup opened is answered twice when + # the first answer is through before the second question is sent (#791). + window.begin_class("gnlse") + sheet = window.sheet + qtbot.waitUntil(lambda: sheet.problem is GNLSE, timeout=WAIT_MS) + order = sheet.derived["soliton_order"] + model = window.document.model + assert model is not None + route = model.route + again = window.document._ask("describe", {"route": route}, f"describe-{route}") + qtbot.waitUntil(lambda: again not in window.document._pending, timeout=WAIT_MS) + QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) + assert sheet.derived["soliton_order"] is order + assert isValid(order) + qtbot.waitUntil(lambda: order.text() == "3", timeout=WAIT_MS) + + def test_a_graded_index_fibre_shows_its_profile_and_its_modes( window: WorkflowWindow, qtbot: QtBot, tmp_path: Path ) -> None: