From fd040c33b494c5aff309ad0ecd20baa7cbcd0391 Mon Sep 17 00:00:00 2001
From: ptsavol <43600314+ptsavol@users.noreply.github.com>
Date: Wed, 18 Dec 2024 10:36:35 +0200
Subject: [PATCH] Add support for PySide6 v6.8.1 (#3024)
Changes that fixed crashes on Pyside6 6.8.1
- Insert homeless Slots into a class in spine_engine_worker.py
- Replace lambda connection from project.py
Re #2980
---
CHANGELOG.md | 1 +
pyproject.toml | 2 +-
spinetoolbox/project.py | 5 +-
spinetoolbox/project_item_icon.py | 11 +-
spinetoolbox/spine_engine_worker.py | 150 ++++++++++++------------
spinetoolbox/widgets/settings_widget.py | 8 +-
6 files changed, 90 insertions(+), 87 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e76d8c3d..3db30c4ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/)
- [Bundled App] **Embedded Python** now includes spinedb-api and pandas
in addition to ipykernel and jill.
+- Support PySide 6.8.1
### Changed
diff --git a/pyproject.toml b/pyproject.toml
index f194bb2b7..e8238c79a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,7 +13,7 @@ classifiers = [
]
requires-python = ">=3.9, <3.13"
dependencies = [
- "PySide6 >= 6.5.0, != 6.5.3, != 6.6.3, != 6.7.0, < 6.8",
+ "PySide6 >= 6.5.0, != 6.5.3, != 6.6.3, != 6.7.0, != 6.8.0",
"jupyter_client >=6.0",
"qtconsole >=5.1",
"spinedb_api>=0.32.1",
diff --git a/spinetoolbox/project.py b/spinetoolbox/project.py
index 352a1d79c..19ae875b4 100644
--- a/spinetoolbox/project.py
+++ b/spinetoolbox/project.py
@@ -17,7 +17,7 @@
import os
from pathlib import Path
import networkx as nx
-from PySide6.QtCore import QCoreApplication, Signal
+from PySide6.QtCore import QCoreApplication, Signal, Slot
from PySide6.QtGui import QColor
from PySide6.QtWidgets import QMessageBox
from spine_engine.exception import EngineInitFailed, RemoteEngineInitFailed
@@ -1041,7 +1041,7 @@ def darker(x):
self._logger.msg.emit(f"Starting DAG {dag_identifier}")
item_names = (darker(name) if not execution_permits[name] else name for name in nx.topological_sort(dag))
self._logger.msg.emit(darker(" -> ").join(item_names))
- worker.finished.connect(lambda worker=worker: self._handle_engine_worker_finished(worker))
+ worker.finished.connect(self._handle_engine_worker_finished)
self._engine_workers.append(worker)
timestamp = create_timestamp()
self._toolbox.make_execution_timestamp(timestamp)
@@ -1097,6 +1097,7 @@ def create_engine_worker(self, dag, execution_permits, dag_identifier, settings,
worker = SpineEngineWorker(data, dag, dag_identifier, items, connections, self._logger, job_id)
return worker
+ @Slot(object)
def _handle_engine_worker_finished(self, worker):
finished_outcomes = {
"USER_STOPPED": [self._logger.msg_warning, "stopped by the user"],
diff --git a/spinetoolbox/project_item_icon.py b/spinetoolbox/project_item_icon.py
index f14280a49..7cac52452 100644
--- a/spinetoolbox/project_item_icon.py
+++ b/spinetoolbox/project_item_icon.py
@@ -25,6 +25,7 @@
QGraphicsTextItem,
QStyle,
QToolTip,
+ QApplication,
)
from spine_engine.spine_engine import ItemExecutionFinishState
from .helpers import LinkType, fix_lightness_color
@@ -588,13 +589,13 @@ def __init__(self, parent):
self._text_item.setFont(font)
parent_rect = parent.rect()
self.setRect(0, 0, 0.5 * parent_rect.width(), 0.5 * parent_rect.height())
- self.setPen(Qt.NoPen)
+ self.setPen(Qt.PenStyle.NoPen)
# pylint: disable=undefined-variable
- self.normal_brush = qApp.palette().window()
- self.selected_brush = qApp.palette().highlight()
+ self.normal_brush = QApplication.palette().window()
+ self.selected_brush = QApplication.palette().highlight()
self.setBrush(self.normal_brush)
self.setAcceptHoverEvents(True)
- self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=False)
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=False)
self.hide()
def item_name(self):
@@ -667,7 +668,7 @@ def __init__(self, parent):
doc = self.document()
doc.setDocumentMargin(0)
self.setAcceptHoverEvents(True)
- self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=False)
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, enabled=False)
self.hide()
def clear_notifications(self):
diff --git a/spinetoolbox/spine_engine_worker.py b/spinetoolbox/spine_engine_worker.py
index bf7c66f9f..d6599e499 100644
--- a/spinetoolbox/spine_engine_worker.py
+++ b/spinetoolbox/spine_engine_worker.py
@@ -10,9 +10,10 @@
# this program. If not, see .
######################################################################################################################
-"""Contains SpineEngineWorker."""
+"""Contains GUIUpdater and SpineEngineWorker classes."""
import copy
from PySide6.QtCore import QObject, QThread, Signal, Slot
+from .project_item.project_item import ProjectItem
from spine_engine.exception import EngineInitFailed, RemoteEngineInitFailed
from spine_engine.spine_engine import ItemExecutionFinishState, SpineEngineState
from spine_engine.utils.helpers import ExecutionDirection
@@ -20,75 +21,70 @@
from .widgets.options_dialog import OptionsDialog
-@Slot(list)
-def _handle_dag_execution_started(project_items):
- for item in project_items:
- item.get_icon().execution_icon.mark_execution_waiting()
+class GUIUpdater(QObject):
+ """Contains slots for updating UI widgets based on messages received from the engine worker."""
+ @Slot(list)
+ def handle_dag_execution_started(self, project_items):
+ for item in project_items:
+ item.get_icon().execution_icon.mark_execution_waiting()
-@Slot(list)
-def _handle_node_execution_ignored(project_items):
- for item in project_items:
- item.get_icon().execution_icon.mark_execution_ignored()
+ @Slot(list)
+ def handle_node_execution_ignored(self, project_items):
+ for item in project_items:
+ item.get_icon().execution_icon.mark_execution_ignored()
+ @Slot(object, object)
+ def handle_node_execution_started(self, item, direction):
+ icon = item.get_icon()
+ if direction == ExecutionDirection.FORWARD:
+ icon.execution_icon.mark_execution_started()
+ if hasattr(icon, "animation_signaller"):
+ icon.animation_signaller.animation_started.emit()
-@Slot(object, object)
-def _handle_node_execution_started(item, direction):
- icon = item.get_icon()
- if direction == ExecutionDirection.FORWARD:
- icon.execution_icon.mark_execution_started()
- if hasattr(icon, "animation_signaller"):
- icon.animation_signaller.animation_started.emit()
-
-
-@Slot(object, object, object)
-def _handle_node_execution_finished(item, direction, item_state):
- icon = item.get_icon()
- if direction == ExecutionDirection.FORWARD:
- icon.execution_icon.mark_execution_finished(item_state)
- if hasattr(icon, "animation_signaller"):
- icon.animation_signaller.animation_stopped.emit()
-
-
-@Slot(object, str, str, str)
-def _handle_event_message_arrived(item, filter_id, msg_type, msg_text):
- item.add_event_message(filter_id, msg_type, msg_text)
-
-
-@Slot(object, str, str, str)
-def _handle_process_message_arrived(item, filter_id, msg_type, msg_text):
- item.add_process_message(filter_id, msg_type, msg_text)
-
-
-@Slot(dict, object)
-def _handle_prompt_arrived(prompt, engine_mngr, logger=None):
- prompter_id = prompt["prompter_id"]
- title, text, option_to_answer, notes, preferred = prompt["data"]
- answer = OptionsDialog.get_answer(logger, title, text, option_to_answer, notes=notes, preferred=preferred)
- engine_mngr.answer_prompt(prompter_id, answer)
-
-
-@Slot(object)
-def _handle_flash_arrived(connection):
- connection.graphics_item.run_execution_animation()
-
-
-@Slot(list)
-def _mark_all_items_failed(items):
- """Fails all project items.
-
- Args:
- items (list of ProjectItem): project items
- """
- for item in items:
+ @Slot(object, object, object)
+ def handle_node_execution_finished(self, item, direction, item_state):
icon = item.get_icon()
- icon.execution_icon.mark_execution_finished(ItemExecutionFinishState.FAILURE)
- if hasattr(icon, "animation_signaller"):
- icon.animation_signaller.animation_stopped.emit()
+ if direction == ExecutionDirection.FORWARD:
+ icon.execution_icon.mark_execution_finished(item_state)
+ if hasattr(icon, "animation_signaller"):
+ icon.animation_signaller.animation_stopped.emit()
+
+ @Slot(object, str, str, str)
+ def handle_event_message_arrived(self, item, filter_id, msg_type, msg_text):
+ item.add_event_message(filter_id, msg_type, msg_text)
+
+ @Slot(object, str, str, str)
+ def handle_process_message_arrived(self, item, filter_id, msg_type, msg_text):
+ item.add_process_message(filter_id, msg_type, msg_text)
+
+ @Slot(dict, object)
+ def handle_prompt_arrived(self, prompt, engine_mngr, logger=None):
+ prompter_id = prompt["prompter_id"]
+ title, text, option_to_answer, notes, preferred = prompt["data"]
+ answer = OptionsDialog.get_answer(logger, title, text, option_to_answer, notes=notes, preferred=preferred)
+ engine_mngr.answer_prompt(prompter_id, answer)
+
+ @Slot(object)
+ def handle_flash_arrived(self, connection):
+ connection.graphics_item.run_execution_animation()
+
+ @Slot(list)
+ def mark_all_items_failed(self, items):
+ """Fails all project items.
+
+ Args:
+ items (list of ProjectItem): project items
+ """
+ for item in items:
+ icon = item.get_icon()
+ icon.execution_icon.mark_execution_finished(ItemExecutionFinishState.FAILURE)
+ if hasattr(icon, "animation_signaller"):
+ icon.animation_signaller.animation_stopped.emit()
class SpineEngineWorker(QObject):
- finished = Signal()
+ finished = Signal(object)
_mark_items_ignored = Signal(list)
_dag_execution_started = Signal(list)
_node_execution_started = Signal(object, object)
@@ -128,6 +124,7 @@ def __init__(self, engine_data, dag, dag_identifier, project_items, connections,
self._thread = QThread()
self.moveToThread(self._thread)
self._thread.started.connect(self.do_work)
+ self._gui_updater = GUIUpdater()
@property
def job_id(self):
@@ -155,11 +152,11 @@ def set_engine_data(self, engine_data):
"""
self._engine_data = engine_data
- @Slot(object, str, str)
+ @Slot(ProjectItem, str, str, str)
def _handle_event_message_arrived_silent(self, item, filter_id, msg_type, msg_text):
self.event_messages.setdefault(msg_type, []).append(msg_text)
- @Slot(object, str, str)
+ @Slot(object, str, str, str)
def _handle_process_message_arrived_silent(self, item, filter_id, msg_type, msg_text):
self.process_messages.setdefault(msg_type, []).append(msg_text)
@@ -177,14 +174,14 @@ def _connect_log_signals(self, silent):
self._event_message_arrived.connect(self._handle_event_message_arrived_silent)
self._process_message_arrived.connect(self._handle_process_message_arrived_silent)
return
- self._mark_items_ignored.connect(_handle_node_execution_ignored)
- self._dag_execution_started.connect(_handle_dag_execution_started)
- self._node_execution_started.connect(_handle_node_execution_started)
- self._node_execution_finished.connect(_handle_node_execution_finished)
- self._event_message_arrived.connect(_handle_event_message_arrived)
- self._process_message_arrived.connect(_handle_process_message_arrived)
- self._prompt_arrived.connect(_handle_prompt_arrived)
- self._flash_arrived.connect(_handle_flash_arrived)
+ self._mark_items_ignored.connect(self._gui_updater.handle_node_execution_ignored)
+ self._dag_execution_started.connect(self._gui_updater.handle_dag_execution_started)
+ self._node_execution_started.connect(self._gui_updater.handle_node_execution_started)
+ self._node_execution_finished.connect(self._gui_updater.handle_node_execution_finished)
+ self._event_message_arrived.connect(self._gui_updater.handle_event_message_arrived)
+ self._process_message_arrived.connect(self._gui_updater.handle_process_message_arrived)
+ self._prompt_arrived.connect(self._gui_updater.handle_prompt_arrived)
+ self._flash_arrived.connect(self._gui_updater.handle_flash_arrived)
def start(self, silent=False):
"""Connects log signals.
@@ -194,7 +191,7 @@ def start(self, silent=False):
but saved in internal dicts.
"""
self._connect_log_signals(silent)
- self._all_items_failed.connect(_mark_all_items_failed)
+ self._all_items_failed.connect(self._gui_updater.mark_all_items_failed)
included_items, ignored_items = self._included_and_ignored_items()
self._dag_execution_started.emit(included_items)
self._mark_items_ignored.emit(ignored_items)
@@ -240,7 +237,7 @@ def do_work(self):
self._logger.msg_error.emit(f"Failed to start engine: {error}")
self._engine_final_state = str(SpineEngineState.FAILED)
self._all_items_failed.emit(list(self._project_items.values()))
- self.finished.emit()
+ self.finished.emit(self)
return
except RemoteEngineInitFailed as e:
self._logger.msg_error.emit(
@@ -248,7 +245,7 @@ def do_work(self):
)
self._engine_final_state = str(SpineEngineState.FAILED)
self._all_items_failed.emit(list(self._project_items.values()))
- self.finished.emit()
+ self.finished.emit(self)
return
while True:
event_type, data = self._engine_mngr.get_engine_event()
@@ -261,7 +258,7 @@ def do_work(self):
self._engine_final_state = str(SpineEngineState.FAILED)
self._all_items_failed.emit(list(self._project_items.values()))
break
- self.finished.emit()
+ self.finished.emit(self)
def _process_event(self, event_type, data):
handler = {
@@ -410,6 +407,7 @@ def clean_up(self):
self._engine_mngr.stop_engine()
else:
self._engine_mngr.clean_up()
+ self._gui_updater = None
self._thread.quit()
self._thread.wait()
self._thread.deleteLater()
diff --git a/spinetoolbox/widgets/settings_widget.py b/spinetoolbox/widgets/settings_widget.py
index 5b9a6e429..ccfa0253d 100644
--- a/spinetoolbox/widgets/settings_widget.py
+++ b/spinetoolbox/widgets/settings_widget.py
@@ -435,8 +435,10 @@ def _update_remote_execution_page_widget_status(self, state):
@Slot(bool)
def _remove_all_settings(self, _=False):
- msg = ("Do you want to reset all settings to factory defaults? Spine Toolbox will be shutdown "
- "for the changes to take effect.
Continue?")
+ msg = (
+ "Do you want to reset all settings to factory defaults? Spine Toolbox will be shutdown "
+ "for the changes to take effect.
Continue?"
+ )
box_title = "Close app and return to factory defaults?"
box = QMessageBox(
QMessageBox.Icon.Question,
@@ -1050,7 +1052,7 @@ def _edit_remote_host(self, new_text):
"""
prep_str = "tcp://"
if new_text.startswith(prep_str): # prep str already present
- new = new_text[len(prep_str):]
+ new = new_text[len(prep_str) :]
else: # First letter has been entered
new = new_text
# Clear when only prep str present or when clear (x) button is clicked