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..269181680 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.1", "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..feaaea741 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,69 @@ 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(self, project_items): + for item in project_items: + item.get_icon().execution_icon.mark_execution_ignored() -@Slot(list) -def _handle_node_execution_ignored(project_items): - for item in project_items: - item.get_icon().execution_icon.mark_execution_ignored() - - -@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. + @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() - 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 +123,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 +151,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 +173,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 +190,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 +236,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 +244,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 +257,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 +406,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()