Skip to content

Commit

Permalink
Add support for PySide6 v6.8.1 (#3024)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ptsavol authored Dec 18, 2024
1 parent 46ac860 commit fd040c3
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 87 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions spinetoolbox/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1041,7 +1041,7 @@ def darker(x):
self._logger.msg.emit(f"<b>Starting DAG {dag_identifier}</b>")
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)
Expand Down Expand Up @@ -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"],
Expand Down
11 changes: 6 additions & 5 deletions spinetoolbox/project_item_icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
QGraphicsTextItem,
QStyle,
QToolTip,
QApplication,
)
from spine_engine.spine_engine import ItemExecutionFinishState
from .helpers import LinkType, fix_lightness_color
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
150 changes: 74 additions & 76 deletions spinetoolbox/spine_engine_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,85 +10,81 @@
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################

"""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
from .spine_engine_manager import LocalSpineEngineManager, make_engine_manager
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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand All @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -240,15 +237,15 @@ 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(
f"Server is not responding. {e}. Check settings " f"in <b>File->Settings->Engine</b>."
)
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()
Expand All @@ -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 = {
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 5 additions & 3 deletions spinetoolbox/widgets/settings_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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? <b>Spine Toolbox will be shutdown</b> "
"for the changes to take effect.<br/>Continue?")
msg = (
"Do you want to reset all settings to factory defaults? <b>Spine Toolbox will be shutdown</b> "
"for the changes to take effect.<br/>Continue?"
)
box_title = "Close app and return to factory defaults?"
box = QMessageBox(
QMessageBox.Icon.Question,
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit fd040c3

Please sign in to comment.