From 1beee34e9f04d0a332c171810a057956b2b31182 Mon Sep 17 00:00:00 2001 From: Antti Soininen Date: Wed, 9 Aug 2023 13:23:28 +0300 Subject: [PATCH 1/3] Rename settings to app_settings in Project Project may have its own settings in the future. Re #2237 --- spinetoolbox/project.py | 18 +++++++++--------- spinetoolbox/ui_main.py | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spinetoolbox/project.py b/spinetoolbox/project.py index 3a1a9201a..7dbb346e9 100644 --- a/spinetoolbox/project.py +++ b/spinetoolbox/project.py @@ -107,13 +107,13 @@ class SpineToolboxProject(MetaObject): specification_saved = Signal(str, str) """Emitted after a specification has been saved.""" - def __init__(self, toolbox, p_dir, plugin_specs, settings, logger): + def __init__(self, toolbox, p_dir, plugin_specs, app_settings, logger): """ Args: toolbox (ToolboxUI): toolbox of this project p_dir (str): Project directory plugin_specs (Iterable of ProjectItemSpecification): specifications available as plugins - settings (QSettings): Toolbox settings + app_settings (QSettings): Toolbox settings logger (LoggerInterface): a logger instance """ _, name = os.path.split(p_dir) @@ -124,7 +124,7 @@ def __init__(self, toolbox, p_dir, plugin_specs, settings, logger): self._connections = list() self._jumps = list() self._logger = logger - self._settings = settings + self._app_settings = app_settings self._engine_workers = [] self._execution_in_progress = False self.project_dir = None # Full path to project directory @@ -296,7 +296,7 @@ def load(self, spec_factories, item_factories): specification_local_data = load_specification_local_data(self.config_dir) for path in deserialized_paths: spec = load_specification_from_file( - path, specification_local_data, spec_factories, self._settings, self._logger + path, specification_local_data, spec_factories, self._app_settings, self._logger ) if spec is not None: self.add_specification(spec, save_to_disk=False) @@ -973,7 +973,7 @@ def _execute_dags(self, dags, execution_permits_list): if not self.job_id: self.project_execution_finished.emit() return - settings = make_settings_dict_for_engine(self._settings) + settings = make_settings_dict_for_engine(self._app_settings) darker_fg_color = QColor(FG_COLOR).darker().name() darker = lambda x: f'{x}' for k, (dag, execution_permits) in enumerate(zip(dags, execution_permits_list)): @@ -1401,8 +1401,8 @@ def _update_ranks(self, dag): item.set_rank(ranks[item_name]) @property - def settings(self): - return self._settings + def app_settings(self): + return self._app_settings @busy_effect def prepare_remote_execution(self): @@ -1412,7 +1412,7 @@ def prepare_remote_execution(self): str: Job Id if server is ready for remote execution, empty string if something went wrong or "1" if local execution is enabled. """ - if not self._settings.value("engineSettings/remoteExecutionEnabled", defaultValue="false") == "true": + if not self._app_settings.value("engineSettings/remoteExecutionEnabled", defaultValue="false") == "true": return "1" # Something that isn't False host, port, sec_model, sec_folder = self._toolbox.engine_server_settings() if not host: @@ -1462,7 +1462,7 @@ def prepare_remote_execution(self): def finalize_remote_execution(self): """Sends a request to server to remove the project directory and removes the project ZIP file from client.""" - if not self._settings.value("engineSettings/remoteExecutionEnabled", defaultValue="false") == "true": + if not self._app_settings.value("engineSettings/remoteExecutionEnabled", defaultValue="false") == "true": return host, port, sec_model, sec_folder = self._toolbox.engine_server_settings() try: diff --git a/spinetoolbox/ui_main.py b/spinetoolbox/ui_main.py index be9ef09dd..0c5258097 100644 --- a/spinetoolbox/ui_main.py +++ b/spinetoolbox/ui_main.py @@ -513,7 +513,7 @@ def create_project(self, proj_dir): return self.undo_stack.clear() self._project = SpineToolboxProject( - self, proj_dir, self._plugin_manager.plugin_specs, settings=self._qsettings, logger=self + self, proj_dir, self._plugin_manager.plugin_specs, app_settings=self._qsettings, logger=self ) self.project_item_model.connect_to_project(self._project) self.specification_model.connect_to_project(self._project) @@ -575,7 +575,7 @@ def restore_project(self, project_dir, ask_confirmation=True): # Create project self.undo_stack.clear() self._project = SpineToolboxProject( - self, project_dir, self._plugin_manager.plugin_specs, settings=self._qsettings, logger=self + self, project_dir, self._plugin_manager.plugin_specs, app_settings=self._qsettings, logger=self ) self.project_item_model.connect_to_project(self._project) self.specification_model.connect_to_project(self._project) From c2fd1ad1dd6d25b69e5ca24cf19b07e503c279e6 Mon Sep 17 00:00:00 2001 From: Antti Soininen Date: Wed, 9 Aug 2023 16:21:07 +0300 Subject: [PATCH 2/3] Fix build_ui script The script wasn't fixing imports in the generated .py files correctly after the upgrade to PySide6. Re #2237 --- bin/build_ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/build_ui.py b/bin/build_ui.py index 0083dde79..17a6d5481 100755 --- a/bin/build_ui.py +++ b/bin/build_ui.py @@ -23,9 +23,9 @@ def fix_resources_imports(path): lines = list() with open(path, 'r') as in_file: for line in in_file: - if line == "from . import resources_icons_rc\n": + if line == "from . import resources_icons_rc\n": lines.append("from spinetoolbox import resources_icons_rc\n") - elif line == "from . import resources_logos_rc\n": + elif line == "from . import resources_logos_rc\n": lines.append("from spinetoolbox import resources_logos_rc\n") else: lines.append(line) From 5aec09b74e7c1eaa9bac7f3596ee41badea89c4e Mon Sep 17 00:00:00 2001 From: Antti Soininen Date: Wed, 9 Aug 2023 16:41:30 +0300 Subject: [PATCH 3/3] Add project-level option to disable the Execute project button - Renamed SpineToolboxProject.settings to app_settings, since settings now means project-level settings - Implemented a ProjectSettings class which holds the settings - The settings are now stored in project.json bumping LATEST_PROJECT_VERSION to 11 - The only project setting available is enable_execute_all which enables or disables the Execute Project button. - Implemented the enable_execute_all setting - Improved tooltips on the execute buttons Currently, there is no interface in Toolbox to change the project settings so it must be done through editing project.json by hand. This is intended. Re #2237 --- CHANGELOG.md | 4 + .../.spinetoolbox/project.json | 7 +- .../.spinetoolbox/project.json | 7 +- .../.spinetoolbox/project.json | 7 +- .../.spinetoolbox/project.json | 7 +- .../.spinetoolbox/project.json | 7 +- .../.spinetoolbox/project.json | 7 +- .../.spinetoolbox/project.json | 7 +- spinetoolbox/config.py | 2 +- spinetoolbox/project.py | 11 +- spinetoolbox/project_settings.py | 40 +++ spinetoolbox/project_upgrader.py | 68 +++- spinetoolbox/resources_icons_rc.py | 170 +++++----- spinetoolbox/ui/mainwindow.py | 11 +- spinetoolbox/ui/mainwindow.ui | 9 + spinetoolbox/ui_main.py | 25 +- tests/test_ProjectUpgrader.py | 193 +++++++----- tests/test_SpineToolboxProject.py | 9 +- .../.spinetoolbox/project.json | 7 +- .../project_json_versions/proj_v11.json | 293 ++++++++++++++++++ 20 files changed, 695 insertions(+), 196 deletions(-) create mode 100644 spinetoolbox/project_settings.py create mode 100644 tests/test_resources/project_json_versions/proj_v11.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7c010bf..e54b18ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) ## [Unreleased] ### Added +- Support for version 11 Spine Toolbox projects. - Executable Tool Specifications can be used to run any (shell) command. This enhancement duplicates the functionality of Gimlet project items and makes them obsolete. - There is now an option to select if new scenarios or tools are automatically used @@ -30,6 +31,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) - "Make Julia Kernel" and "Make Python Kernel" buttons in Settings->Tools page. Clicking them creates a new Julia or Python kernel based on selected Julia/Python executable on the same page if the kernel does not exist. If the kernel already exists, it is selected automatically. +- ``project.json`` now has an experimental option ["project"]["settings"]["enable_execute_all"] which disables the + Execute Project button when set to ``false``. The option is currently not settable in the UI. ### Changed - The console settings of Python tools as well as the command and shell settings of executable tools @@ -54,6 +57,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) Duplicating a previously duplicated item now has the number `xx` incremented instead of having a new number appended. - "Open kernel spec editor" buttons in Settings->Tools page have been changed "Make Julia kernel" and "Make Python Kernel" buttons +- ### Deprecated diff --git a/execution_tests/import_file_packs/.spinetoolbox/project.json b/execution_tests/import_file_packs/.spinetoolbox/project.json index ef8f1c031..750a58242 100644 --- a/execution_tests/import_file_packs/.spinetoolbox/project.json +++ b/execution_tests/import_file_packs/.spinetoolbox/project.json @@ -1,6 +1,6 @@ { "project": { - "version": 10, + "version": 11, "description": "", "specifications": { "Tool": [ @@ -42,7 +42,10 @@ ] } ], - "jumps": [] + "jumps": [], + "settings": { + "enable_execute_all": true + } }, "items": { "Create file pack": { diff --git a/execution_tests/loop_condition_with_cmd_line_args/.spinetoolbox/project.json b/execution_tests/loop_condition_with_cmd_line_args/.spinetoolbox/project.json index 734748375..34e59cefb 100644 --- a/execution_tests/loop_condition_with_cmd_line_args/.spinetoolbox/project.json +++ b/execution_tests/loop_condition_with_cmd_line_args/.spinetoolbox/project.json @@ -1,6 +1,6 @@ { "project": { - "version": 10, + "version": 11, "description": "", "specifications": { "Tool": [ @@ -106,7 +106,10 @@ } ] } - ] + ], + "settings": { + "enable_execute_all": true + } }, "items": { "Write data": { diff --git a/execution_tests/merger_write_order/.spinetoolbox/project.json b/execution_tests/merger_write_order/.spinetoolbox/project.json index 09f8b4654..c33bab31c 100644 --- a/execution_tests/merger_write_order/.spinetoolbox/project.json +++ b/execution_tests/merger_write_order/.spinetoolbox/project.json @@ -1,6 +1,6 @@ { "project": { - "version": 10, + "version": 11, "description": "", "specifications": {}, "connections": [ @@ -52,7 +52,10 @@ } } ], - "jumps": [] + "jumps": [], + "settings": { + "enable_execute_all": true + } }, "items": { "First source": { diff --git a/execution_tests/modify_connection_filter_by_script/.spinetoolbox/project.json b/execution_tests/modify_connection_filter_by_script/.spinetoolbox/project.json index 4a79daffd..456025258 100644 --- a/execution_tests/modify_connection_filter_by_script/.spinetoolbox/project.json +++ b/execution_tests/modify_connection_filter_by_script/.spinetoolbox/project.json @@ -1,6 +1,6 @@ { "project": { - "version": 10, + "version": 11, "description": "", "specifications": { "Exporter": [ @@ -34,7 +34,10 @@ } } ], - "jumps": [] + "jumps": [], + "settings": { + "enable_execute_all": true + } }, "items": { "Data": { diff --git a/execution_tests/parallel_importer/.spinetoolbox/project.json b/execution_tests/parallel_importer/.spinetoolbox/project.json index b0febb087..f9dd0bc3e 100644 --- a/execution_tests/parallel_importer/.spinetoolbox/project.json +++ b/execution_tests/parallel_importer/.spinetoolbox/project.json @@ -1,6 +1,6 @@ { "project": { - "version": 10, + "version": 11, "description": "", "specifications": { "Tool": [ @@ -53,7 +53,10 @@ ] } ], - "jumps": [] + "jumps": [], + "settings": { + "enable_execute_all": true + } }, "items": { "Source": { diff --git a/execution_tests/parallel_importer_with_datapackage/.spinetoolbox/project.json b/execution_tests/parallel_importer_with_datapackage/.spinetoolbox/project.json index 457e36ab8..9c40af724 100644 --- a/execution_tests/parallel_importer_with_datapackage/.spinetoolbox/project.json +++ b/execution_tests/parallel_importer_with_datapackage/.spinetoolbox/project.json @@ -1,6 +1,6 @@ { "project": { - "version": 10, + "version": 11, "description": "", "specifications": { "Tool": [ @@ -56,7 +56,10 @@ ] } ], - "jumps": [] + "jumps": [], + "settings": { + "enable_execute_all": true + } }, "items": { "Source": { diff --git a/execution_tests/scenario_filters/.spinetoolbox/project.json b/execution_tests/scenario_filters/.spinetoolbox/project.json index 4c9845ea7..3b24211ba 100644 --- a/execution_tests/scenario_filters/.spinetoolbox/project.json +++ b/execution_tests/scenario_filters/.spinetoolbox/project.json @@ -1,6 +1,6 @@ { "project": { - "version": 10, + "version": 11, "description": "Test project to test scenario filtering in a Tool project item.", "specifications": { "Importer": [ @@ -58,7 +58,10 @@ } } ], - "jumps": [] + "jumps": [], + "settings": { + "enable_execute_all": true + } }, "items": { "Data store": { diff --git a/spinetoolbox/config.py b/spinetoolbox/config.py index 039a33f55..b55e95ee1 100644 --- a/spinetoolbox/config.py +++ b/spinetoolbox/config.py @@ -18,7 +18,7 @@ from pathlib import Path # NOTE: All required Python package versions are in setup.cfg -LATEST_PROJECT_VERSION = 10 +LATEST_PROJECT_VERSION = 11 # For the Add/Update SpineOpt wizard REQUIRED_SPINE_OPT_VERSION = "0.6.9" diff --git a/spinetoolbox/project.py b/spinetoolbox/project.py index 7dbb346e9..701515d02 100644 --- a/spinetoolbox/project.py +++ b/spinetoolbox/project.py @@ -35,6 +35,7 @@ ) from spine_engine.utils.serialization import deserialize_path, serialize_path from spine_engine.server.util.zip_handler import ZipHandler +from .project_settings import ProjectSettings from .server.engine_client import EngineClient from .metaobject import MetaObject from .helpers import ( @@ -107,13 +108,14 @@ class SpineToolboxProject(MetaObject): specification_saved = Signal(str, str) """Emitted after a specification has been saved.""" - def __init__(self, toolbox, p_dir, plugin_specs, app_settings, logger): + def __init__(self, toolbox, p_dir, plugin_specs, app_settings, settings, logger): """ Args: toolbox (ToolboxUI): toolbox of this project p_dir (str): Project directory plugin_specs (Iterable of ProjectItemSpecification): specifications available as plugins app_settings (QSettings): Toolbox settings + settings (ProjectSettings): project settings logger (LoggerInterface): a logger instance """ _, name = os.path.split(p_dir) @@ -125,6 +127,7 @@ def __init__(self, toolbox, p_dir, plugin_specs, app_settings, logger): self._jumps = list() self._logger = logger self._app_settings = app_settings + self._settings = settings self._engine_workers = [] self._execution_in_progress = False self.project_dir = None # Full path to project directory @@ -148,6 +151,10 @@ def toolbox(self): def all_item_names(self): return list(self._project_items) + @property + def settings(self): + return self._settings + def _create_project_structure(self, directory): """Makes the given directory a Spine Toolbox project directory. Creates directories and files that are common to all projects. @@ -193,6 +200,7 @@ def save(self): project_dict = { "version": LATEST_PROJECT_VERSION, "description": self.description, + "settings": self._settings.to_dict(), "specifications": serialized_spec_paths, "connections": [connection.to_dict() for connection in self._connections], "jumps": [jump.to_dict() for jump in self._jumps], @@ -288,6 +296,7 @@ def load(self, spec_factories, item_factories): self._merge_local_data_to_project_info(local_data_dict, project_info) # Parse project info self.set_description(project_info["project"]["description"]) + self._settings = ProjectSettings.from_dict(project_info["project"]["settings"]) spec_paths_per_type = project_info["project"]["specifications"] deserialized_paths = [ deserialize_path(path, self.project_dir) for paths in spec_paths_per_type.values() for path in paths diff --git a/spinetoolbox/project_settings.py b/spinetoolbox/project_settings.py new file mode 100644 index 000000000..ff5d9553f --- /dev/null +++ b/spinetoolbox/project_settings.py @@ -0,0 +1,40 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# This file is part of Spine Toolbox. +# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General +# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) +# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General +# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with +# this program. If not, see . +###################################################################################################################### +"""Contains project-specific settings.""" + +import dataclasses + + +@dataclasses.dataclass +class ProjectSettings: + """Spine Toolbox project settings.""" + + enable_execute_all: bool = True + + def to_dict(self): + """Serializes the settings into a dictionary. + + Returns: + dict: serialized settings + """ + return dataclasses.asdict(self) + + @staticmethod + def from_dict(settings_dict): + """Deserializes settings from dictionary. + + Args: + settings_dict (dict): serialized settings + + Returns: + ProjectSettings: deserialized settings + """ + return ProjectSettings(**settings_dict) diff --git a/spinetoolbox/project_upgrader.py b/spinetoolbox/project_upgrader.py index 8352b5c66..199f8da33 100644 --- a/spinetoolbox/project_upgrader.py +++ b/spinetoolbox/project_upgrader.py @@ -22,6 +22,7 @@ from spine_engine.utils.serialization import serialize_path, deserialize_path from .config import LATEST_PROJECT_VERSION, PROJECT_FILENAME from .helpers import home_dir +from .project_settings import ProjectSettings class ProjectUpgrader: @@ -100,6 +101,8 @@ def upgrade_to_latest(self, v, project_dict, project_dir): project_dict = self.upgrade_v8_to_v9(project_dict) elif v == 9: project_dict = self.upgrade_v9_to_v10(project_dict) + elif v == 10: + project_dict = self.upgrade_v10_to_v11(project_dict) v += 1 self._toolbox.msg_success.emit(f"Project upgraded to version {v}") return project_dict @@ -502,6 +505,24 @@ def upgrade_v9_to_v10(old): new["items"].pop(name) return new + @staticmethod + def upgrade_v10_to_v11(old): + """Upgrades version 10 project dictionary to version 11. + + Changes: + 1. Add ["project"]["settings"] key + + Args: + old (dict): Version 10 project dictionary + + Returns: + dict: Version 11 project dictionary + """ + new = copy.deepcopy(old) + new["project"]["version"] = 11 + new["project"]["settings"] = ProjectSettings().to_dict() + return new + @staticmethod def make_unique_importer_specification_name(importer_name, label, k): return f"{importer_name} - {os.path.basename(label['path'])} - {k}" @@ -543,13 +564,23 @@ def get_project_directory(self): return answer # New project directory def is_valid(self, v, p): - """Checks given project dict if it is valid for given version.""" + """Checks given project dict if it is valid for given version. + + Args: + v (int): project version to validate against + p (dict): project dictionary + + Returns: + bool: True if project is valid, False otherwise + """ if v == 1: return self.is_valid_v1(p) if 2 <= v <= 8: return self.is_valid_v2_to_v8(p, v) if 9 <= v <= 10: return self.is_valid_v9_to_v10(p) + if v == 11: + return self.is_valid_v11(p) raise NotImplementedError(f"No validity check available for version {v}") def is_valid_v1(self, p): @@ -564,10 +595,10 @@ def is_valid_v1(self, p): Returns: bool: True if project is a valid version 1 project, False if it is not """ - if "project" not in p.keys(): + if "project" not in p: self._toolbox.msg_error.emit("Invalid project.json file. Key 'project' not found.") return False - if "objects" not in p.keys(): + if "objects" not in p: self._toolbox.msg_error.emit("Invalid project.json file. Key 'objects' not found.") return False required_project_keys = ["version", "name", "description", "tool_specifications", "connections"] @@ -611,10 +642,10 @@ def is_valid_v2_to_v8(self, p, v): Returns: bool: True if project is a valid version 2 to version 8 project, False if it is not """ - if "project" not in p.keys(): + if "project" not in p: self._toolbox.msg_error.emit("Invalid project.json file. Key 'project' not found.") return False - if "items" not in p.keys(): + if "items" not in p: self._toolbox.msg_error.emit("Invalid project.json file. Key 'items' not found.") return False required_project_keys = ["version", "name", "description", "specifications", "connections"] @@ -657,10 +688,10 @@ def is_valid_v9_to_v10(self, p): Returns: bool: True if project is a valid version 9 and 10 project, False otherwise """ - if "project" not in p.keys(): + if "project" not in p: self._toolbox.msg_error.emit("Invalid project.json file. Key 'project' not found.") return False - if "items" not in p.keys(): + if "items" not in p: self._toolbox.msg_error.emit("Invalid project.json file. Key 'items' not found.") return False required_project_keys = ["version", "description", "specifications", "connections"] @@ -678,6 +709,29 @@ def is_valid_v9_to_v10(self, p): return False return True + def is_valid_v11(self, p): + """Checks that the given project JSON dictionary contains + a valid version 11 Spine Toolbox project. Valid meaning, that + it contains all required keys and values are of the correct + type. + + Args: + p (dict): Project information JSON + + Returns: + bool: True if project is a valid version 11 project, False otherwise + """ + if "project" not in p: + self._toolbox.msg_error.emit("Invalid project.json file. Key 'project' not found.") + return False + if "settings" not in p["project"]: + self._toolbox.msg_error.emit("Invalid project.json file. Key 'items' not found in 'project'.") + return False + if not isinstance(p["project"]["settings"], dict): + self._toolbox.msg_error.emit("Invalid project.json file. 'settings' must be a dict.") + return False + return True + def backup_project_file(self, project_dir, v): """Makes a backup copy of project.json file.""" src = os.path.join(project_dir, ".spinetoolbox", PROJECT_FILENAME) diff --git a/spinetoolbox/resources_icons_rc.py b/spinetoolbox/resources_icons_rc.py index 89c91f904..d6e0ccd42 100644 --- a/spinetoolbox/resources_icons_rc.py +++ b/spinetoolbox/resources_icons_rc.py @@ -10,7 +10,7 @@ # this program. If not, see . ###################################################################################################################### # Created by: object code -# Created by: The Resource Compiler for Qt version 6.4.3 +# Created by: The Resource Compiler for Qt version 6.5.2 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore @@ -32065,177 +32065,177 @@ \x00\x00\x004\x00\x02\x00\x00\x00\x01\x00\x00\x00\x05\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00T\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x88\x04\xacr\xc1\ +\x00\x00\x01\x83\xb1\xb2\x06\x95\ \x00\x00\x00x\x00\x00\x00\x00\x00\x01\x00\x00\x09\xf5\ -\x00\x00\x01\x89\x05\xd7\x05\xe0\ +\x00\x00\x01\x89\xd8\xc60\x1f\ \x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x008\x89\ -\x00\x00\x01\x88\xc2\xdeI`\ +\x00\x00\x01\x89\xd8\xc60/\ \x00\x00\x00\x9e\x00\x01\x00\x00\x00\x01\x00\x00\x10N\ -\x00\x00\x01\x89\x05\xd73\xa6\ +\x00\x00\x01\x89\xd8\xc60\x1f\ \x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x06A\x9c\ -\x00\x00\x01\x88\x04\xacr\xc7\ +\x00\x00\x01\x83\xb1\xb2\x06\x99\ \x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x062\x92\ -\x00\x00\x01\x88\x04\xacr\xe0\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x04X\x00\x00\x00\x00\x00\x01\x00\x06\xa7<\ -\x00\x00\x01\x88\x04\xacr\xc6\ +\x00\x00\x01\x83\xb1\xb2\x06\x98\ \x00\x00\x04\xa0\x00\x00\x00\x00\x00\x01\x00\x06\xbc&\ -\x00\x00\x01\x88\x04\xacr\xdb\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x03D\x00\x00\x00\x00\x00\x01\x00\x06\x88\x1a\ -\x00\x00\x01\x88\x04\xacr\xe1\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x04\x80\x00\x00\x00\x00\x00\x01\x00\x06\xa9\x10\ -\x00\x00\x01\x88\x04\xacr\xc5\ +\x00\x00\x01\x83\xb1\xb2\x06\x98\ \x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x98\x16\ -\x00\x00\x01\x88\x04\xacr\xdb\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x06E\xb2\ -\x00\x00\x01\x88\x04\xacr\xc4\ +\x00\x00\x01\x83\xb1\xb2\x06\x97\ \x00\x00\x02h\x00\x00\x00\x00\x00\x01\x00\x06B\xab\ -\x00\x00\x01\x88\x04\xacr\xe0\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x03\xde\x00\x00\x00\x00\x00\x01\x00\x06\x95\xa7\ -\x00\x00\x01\x88\x04\xacr\xcb\ +\x00\x00\x01\x83\xb1\xb2\x06\x9d\ \x00\x00\x03^\x00\x00\x00\x00\x00\x01\x00\x06\x8a3\ -\x00\x00\x01\x88\x04\xacr\xc6\ +\x00\x00\x01\x83\xb1\xb2\x06\x98\ \x00\x00\x02\xc4\x00\x00\x00\x00\x00\x01\x00\x06Q\xfb\ -\x00\x00\x01\x88\x04\xacr\xc2\ +\x00\x00\x01\x83\xb1\xb2\x06\x96\ \x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x06?\xec\ -\x00\x00\x01\x88\x04\xacr\xc7\ +\x00\x00\x01\x83\xb1\xb2\x06\x9a\ \x00\x00\x03(\x00\x00\x00\x00\x00\x01\x00\x06b\xb3\ -\x00\x00\x01\x88\x04\xacr\xc4\ +\x00\x00\x01\x83\xb1\xb2\x06\x97\ \x00\x00\x01\xbe\x00\x00\x00\x00\x00\x01\x00\x060\xaf\ -\x00\x00\x01\x88\x04\xacr\xdb\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x06/\x9c\ -\x00\x00\x01\x88\x04\xacr\xe0\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x04\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x9e\x7f\ -\x00\x00\x01\x88\x04\xacr\xdf\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x03\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00O\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x03~\x00\x02\x00\x00\x00-\x00\x00\x00\x22\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x02\x0a\x00\x00\x00\x00\x00\x01\x00\x065G\ -\x00\x00\x01\x88\x04\xacr\xe2\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x04<\x00\x00\x00\x00\x00\x01\x00\x06\xa1\x98\ -\x00\x00\x01\x88\x04\xacr\xe1\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x03\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x94o\ -\x00\x00\x01\x88\x04\xacr\xc5\ +\x00\x00\x01\x83\xb1\xb2\x06\x98\ \x00\x00\x02\xda\x00\x00\x00\x00\x00\x01\x00\x06T\x05\ -\x00\x00\x01\x88\x04\xacr\xc4\ +\x00\x00\x01\x83\xb1\xb2\x06\x97\ \x00\x00\x02\x9e\x00\x00\x00\x00\x00\x01\x00\x06G\x93\ -\x00\x00\x01\x88\x04\xacr\xe1\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x03\x02\x00\x00\x00\x00\x00\x01\x00\x06`G\ -\x00\x00\x01\x88\x04\xacr\xc3\ +\x00\x00\x01\x83\xb1\xb2\x06\x97\ \x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x07\x05\xa0\ -\x00\x00\x01\x88\x04\xacr\xd2\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x0b\x18\x00\x00\x00\x00\x00\x01\x00\x07\xad\x12\ -\x00\x00\x01\x88\x04\xacr\xd8\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x06\x18\x00\x00\x00\x00\x00\x01\x00\x06\xebN\ -\x00\x00\x01\x88\x04\xacr\xda\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x08\xba\x00\x00\x00\x00\x00\x01\x00\x07A\xae\ -\x00\x00\x01\x88\x04\xacr\xcd\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x09\x16\x00\x00\x00\x00\x00\x01\x00\x07Vm\ -\x00\x00\x01\x88\x04\xacr\xce\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x06\xba\x00\x00\x00\x00\x00\x01\x00\x07\x03D\ -\x00\x00\x01\x88\x04\xacr\xd9\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x08>\x00\x00\x00\x00\x00\x01\x00\x07;\xea\ -\x00\x00\x01\x88\x04\xacr\xd7\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x08\xf2\x00\x00\x00\x00\x00\x01\x00\x07T\xab\ -\x00\x00\x01\x88\x04\xacr\xd2\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x0a2\x00\x00\x00\x00\x00\x01\x00\x07\x94\xae\ -\x00\x00\x01\x88\x04\xacr\xd2\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x07F\x00\x00\x00\x00\x00\x01\x00\x07\x17\xb0\ -\x00\x00\x01\x88\x04\xacr\xd9\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x06\xda\x00\x00\x00\x00\x00\x01\x00\x07\x08\xe0\ -\x00\x00\x01\x88\x04\xacr\xd6\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x08\x1e\x00\x00\x00\x00\x00\x01\x00\x07\x22\xda\ -\x00\x00\x01\x88\x04\xacr\xcf\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x08`\x00\x00\x00\x00\x00\x01\x00\x07=\xf7\ -\x00\x00\x01\x88\x04\xacr\xd6\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x07\x98\x00\x00\x00\x00\x00\x01\x00\x07\x1d\xab\ -\x00\x00\x01\x88\x04\xacr\xd8\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x08\x94\x00\x00\x00\x00\x00\x01\x00\x07?S\ -\x00\x00\x01\x88\x04\xacr\xd7\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x06>\x00\x00\x00\x00\x00\x01\x00\x06\xedW\ -\x00\x00\x01\x88\x04\xacr\xcc\ +\x00\x00\x01\x83\xb1\xb2\x06\x9e\ \x00\x00\x09d\x00\x00\x00\x00\x00\x01\x00\x07r\xb2\ -\x00\x00\x01\x88\x04\xacr\xcf\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x07^\x00\x00\x00\x00\x00\x01\x00\x07\x19\xa3\ -\x00\x00\x01\x88\x04\xacr\xd1\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x0a\x86\x00\x00\x00\x00\x00\x01\x00\x07\x97y\ -\x00\x00\x01\x88\x04\xacr\xd4\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x06\x02\x00\x00\x00\x00\x00\x01\x00\x06\xe8\xdf\ -\x00\x00\x01\x88\x04\xacr\xda\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x06\x98\x00\x00\x00\x00\x00\x01\x00\x06\xf5g\ -\x00\x00\x01\x88\x04\xacr\xcf\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x07\xfa\x00\x00\x00\x00\x00\x01\x00\x07!x\ -\x00\x00\x01\x88\x04\xacr\xd0\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x06\xfe\x00\x00\x00\x00\x00\x01\x00\x07\x13\x93\ -\x00\x00\x01\x88\x04\xacr\xcc\ +\x00\x00\x01\x83\xb1\xb2\x06\x9f\ \x00\x00\x0a\xd0\x00\x00\x00\x00\x00\x01\x00\x07\x9b;\ -\x00\x00\x01\x88\x04\xacr\xd3\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x0aV\x00\x00\x00\x00\x00\x01\x00\x07\x96t\ -\x00\x00\x01\x88\x04\xacr\xd6\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x05\xe6\x00\x00\x00\x00\x00\x01\x00\x06\xe5\xba\ -\x00\x00\x01\x88\x04\xacr\xd3\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x07\x80\x00\x00\x00\x00\x00\x01\x00\x07\x1a\xd0\ -\x00\x00\x01\x88\x04\xacr\xcc\ +\x00\x00\x01\x83\xb1\xb2\x06\x9f\ \x00\x00\x09N\x00\x00\x00\x00\x00\x01\x00\x07p\xff\ -\x00\x00\x01\x88\x04\xacr\xcd\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x0a\xf8\x00\x00\x00\x00\x00\x01\x00\x07\x9d\x96\ -\x00\x00\x01\x88\x04\xacr\xce\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x09\xb8\x00\x00\x00\x00\x00\x01\x00\x07\x82E\ -\x00\x00\x01\x88\x04\xacr\xd7\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x08\xda\x00\x00\x00\x00\x00\x01\x00\x07S\x8c\ -\x00\x00\x01\x88\x04\xacr\xd8\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x098\x00\x00\x00\x00\x00\x01\x00\x07d\xdb\ -\x00\x00\x01\x88\x04\xacr\xda\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x06\x84\x00\x00\x00\x00\x00\x01\x00\x06\xf0\xcd\ -\x00\x00\x01\x88\x04\xacr\xcd\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x09\xe4\x00\x00\x00\x00\x00\x01\x00\x07\x85\x07\ -\x00\x00\x01\x88\x04\xacr\xd5\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x09\x88\x00\x00\x00\x00\x00\x01\x00\x07\x80\xaf\ -\x00\x00\x01\x88\x04\xacr\xd3\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x070\x00\x00\x00\x00\x00\x01\x00\x07\x15a\ -\x00\x00\x01\x88\x04\xacr\xd0\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x09\xfc\x00\x00\x00\x00\x00\x01\x00\x07\x87Q\ -\x00\x00\x01\x88\x04\xacr\xcd\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x06n\x00\x00\x00\x00\x00\x01\x00\x06\xef#\ -\x00\x00\x01\x88\x04\xacr\xd7\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x0a\x14\x00\x00\x00\x00\x00\x01\x00\x07\x88\xb7\ -\x00\x00\x01\x88\x04\xacr\xce\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x07\xcc\x00\x01\x00\x00\x00\x01\x00\x07\x1f\x00\ -\x00\x00\x01\x88\x04\xacr\xd8\ +\x00\x00\x01\x84\x9aC*K\ \x00\x00\x07\xe6\x00\x00\x00\x00\x00\x01\x00\x07\x1f\xda\ -\x00\x00\x01\x88\x04\xacr\xd1\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x0a\xaa\x00\x00\x00\x00\x00\x01\x00\x07\x99\x5c\ -\x00\x00\x01\x88\x04\xacr\xd1\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x04\xd8\x00\x00\x00\x00\x00\x01\x00\x07\x06\xaf\ -\x00\x00\x01\x88\x04\xacr\xd1\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x05@\x00\x00\x00\x00\x00\x01\x00\x071X\ -\x00\x00\x01\x88\x04\xacr\xcf\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x05l\x00\x00\x00\x00\x00\x01\x00\x07f/\ -\x00\x00\x01\x88\x04\xacr\xd0\ +\x00\x00\x01\x83\xb1\xb2\x06\xa0\ \x00\x00\x05\x98\x00\x00\x00\x00\x00\x01\x00\x06\xe1\xa9\ -\x00\x00\x01\x88\x04\xacr\xdd\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x04\xf6\x00\x00\x00\x00\x00\x01\x00\x06\xc90\ -\x00\x00\x01\x88\x04\xacr\xde\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x06\xc5\x1e\ -\x00\x00\x01\x88\x04\xacr\xdd\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x04\xb6\x00\x00\x00\x00\x00\x01\x00\x06\xc3\x0b\ -\x00\x00\x01\x88\x04\xacr\xdb\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x06\xe3{\ -\x00\x00\x01\x88\x04\xacr\xdc\ +\x00\x00\x01\x83\xb1\xb2\x06\xa8\ \x00\x00\x05\x10\x00\x00\x00\x00\x00\x01\x00\x06\xcb\xba\ -\x00\x00\x01\x88\x04\xacr\xde\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x04\xd8\x00\x00\x00\x00\x00\x01\x00\x06\xc6\xff\ -\x00\x00\x01\x88\x04\xacr\xdd\ +\x00\x00\x01\x83\xb1\xb2\x06\xae\ \x00\x00\x05@\x00\x00\x00\x00\x00\x01\x00\x06\xcem\ -\x00\x00\x01\x89\x02*\x10\xe9\ +\x00\x00\x01\x89\xd8\xc60/\ \x00\x00\x05l\x00\x00\x00\x00\x00\x01\x00\x06\xd8'\ -\x00\x00\x01\x89\x02*\x10\xed\ +\x00\x00\x01\x89\xd8\xc60/\ \x00\x00\x01&\x00\x01\x00\x00\x00\x01\x00\x03\x04\x82\ -\x00\x00\x01\x88\x04\xacr\xc8\ +\x00\x00\x01\x83\xb1\xb2\x06\x9a\ \x00\x00\x00\xe6\x00\x01\x00\x00\x00\x01\x00\x02\xbaN\ -\x00\x00\x01\x88\x04\xacr\xc8\ +\x00\x00\x01\x83\xb1\xb2\x06\x9b\ \x00\x00\x01d\x00\x00\x00\x00\x00\x01\x00\x03\x10\xa8\ -\x00\x00\x01\x88\x04\xacr\xcb\ +\x00\x00\x01\x83\xb1\xb2\x06\x9d\ " def qInitResources(): diff --git a/spinetoolbox/ui/mainwindow.py b/spinetoolbox/ui/mainwindow.py index e366d3f87..58c31c411 100644 --- a/spinetoolbox/ui/mainwindow.py +++ b/spinetoolbox/ui/mainwindow.py @@ -13,7 +13,7 @@ ################################################################################ ## Form generated from reading UI file 'mainwindow.ui' ## -## Created by: Qt User Interface Compiler version 6.4.3 +## Created by: Qt User Interface Compiler version 6.5.2 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -599,14 +599,23 @@ def retranslateUi(self, MainWindow): self.actionSet_description.setToolTip(QCoreApplication.translate("MainWindow", u"Modify or set project description", None)) #endif // QT_CONFIG(tooltip) self.actionExecute_project.setText(QCoreApplication.translate("MainWindow", u"Project", None)) +#if QT_CONFIG(tooltip) + self.actionExecute_project.setToolTip(QCoreApplication.translate("MainWindow", u"Execute all items in project.", None)) +#endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) self.actionExecute_project.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None)) #endif // QT_CONFIG(shortcut) self.actionExecute_selection.setText(QCoreApplication.translate("MainWindow", u"Selection", None)) +#if QT_CONFIG(tooltip) + self.actionExecute_selection.setToolTip(QCoreApplication.translate("MainWindow", u"Execute selected items.", None)) +#endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) self.actionExecute_selection.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None)) #endif // QT_CONFIG(shortcut) self.actionStop_execution.setText(QCoreApplication.translate("MainWindow", u"Stop", None)) +#if QT_CONFIG(tooltip) + self.actionStop_execution.setToolTip(QCoreApplication.translate("MainWindow", u"Stop execution.", None)) +#endif // QT_CONFIG(tooltip) #if QT_CONFIG(shortcut) self.actionStop_execution.setShortcut(QCoreApplication.translate("MainWindow", u"F7", None)) #endif // QT_CONFIG(shortcut) diff --git a/spinetoolbox/ui/mainwindow.ui b/spinetoolbox/ui/mainwindow.ui index 3f60d75bf..080bf2be8 100644 --- a/spinetoolbox/ui/mainwindow.ui +++ b/spinetoolbox/ui/mainwindow.ui @@ -894,6 +894,9 @@ Project + + Execute all items in project. + F5 @@ -906,6 +909,9 @@ Selection + + Execute selected items. + F6 @@ -918,6 +924,9 @@ Stop + + Stop execution. + F7 diff --git a/spinetoolbox/ui_main.py b/spinetoolbox/ui_main.py index 0c5258097..704430827 100644 --- a/spinetoolbox/ui_main.py +++ b/spinetoolbox/ui_main.py @@ -60,6 +60,7 @@ from .mvcmodels.project_item_model import ProjectItemModel from .mvcmodels.project_item_specification_models import ProjectItemSpecificationModel, FilteredSpecificationModel from .mvcmodels.filter_execution_model import FilterExecutionModel +from .project_settings import ProjectSettings from .widgets.set_description_dialog import SetDescriptionDialog from .widgets.multi_tab_spec_editor import MultiTabSpecEditor from .widgets.about_widget import AboutWidget @@ -184,6 +185,7 @@ def __init__(self): self.ui.actionExecute_project, self.ui.actionExecute_selection, self.ui.actionStop_execution, self ) self.addToolBar(Qt.TopToolBarArea, self.main_toolbar) + self._original_execute_project_action_tooltip = self.ui.actionExecute_project.toolTip() self.setStatusBar(None) # Additional consoles for item execution self._item_consoles = {} # Mapping of ProjectItem to console @@ -365,7 +367,14 @@ def _update_qsettings(self): def _update_execute_enabled(self): first_index = next(self.project_item_model.leaf_indexes(), None) - self.ui.actionExecute_project.setEnabled(first_index is not None and not self.execution_in_progress) + enabled_by_project = self._project.settings.enable_execute_all if self._project is not None else False + self.ui.actionExecute_project.setEnabled( + enabled_by_project and first_index is not None and not self.execution_in_progress + ) + if not enabled_by_project: + self.ui.actionExecute_project.setToolTip("Executing entire project disabled by project settings.") + else: + self.ui.actionExecute_project.setToolTip(self._original_execute_project_action_tooltip) def _update_execute_selected_enabled(self): has_selection = bool(self._selected_item_names) @@ -513,7 +522,12 @@ def create_project(self, proj_dir): return self.undo_stack.clear() self._project = SpineToolboxProject( - self, proj_dir, self._plugin_manager.plugin_specs, app_settings=self._qsettings, logger=self + self, + proj_dir, + self._plugin_manager.plugin_specs, + app_settings=self._qsettings, + settings=ProjectSettings(), + logger=self, ) self.project_item_model.connect_to_project(self._project) self.specification_model.connect_to_project(self._project) @@ -575,7 +589,12 @@ def restore_project(self, project_dir, ask_confirmation=True): # Create project self.undo_stack.clear() self._project = SpineToolboxProject( - self, project_dir, self._plugin_manager.plugin_specs, app_settings=self._qsettings, logger=self + self, + project_dir, + self._plugin_manager.plugin_specs, + app_settings=self._qsettings, + settings=ProjectSettings(), + logger=self, ) self.project_item_model.connect_to_project(self._project) self.specification_model.connect_to_project(self._project) diff --git a/tests/test_ProjectUpgrader.py b/tests/test_ProjectUpgrader.py index 9e6146692..be566c6aa 100644 --- a/tests/test_ProjectUpgrader.py +++ b/tests/test_ProjectUpgrader.py @@ -22,6 +22,8 @@ from pathlib import Path from tempfile import TemporaryDirectory from PySide6.QtWidgets import QApplication + +from spinetoolbox.project_settings import ProjectSettings from spinetoolbox.project_upgrader import ProjectUpgrader from spinetoolbox.resources_icons_rc import qInitResources from spinetoolbox.config import LATEST_PROJECT_VERSION @@ -172,17 +174,17 @@ def test_upgrade_v2_to_v3(self): with open(spec_file_path, "w", encoding="utf-8") as tmp_spec_file: tmp_spec_file.write("hello") # Upgrade to version 3 - proj_v3 = pu.upgrade(proj_v2, project_dir) - mock_backup.assert_called_once() - mock_force_save.assert_called_once() - self.assertTrue(pu.is_valid(3, proj_v3)) - # Check that items were transferred successfully by checking that item names are found in new - # 'items' dict and that they contain a dict - v2_items = proj_v2["items"] - v3_items = proj_v3["items"] - for name in v2_items.keys(): - self.assertTrue(name in v3_items.keys()) - self.assertIsInstance(v3_items[name], dict) + proj_v3 = pu.upgrade(proj_v2, project_dir) + mock_backup.assert_called_once() + mock_force_save.assert_called_once() + self.assertTrue(pu.is_valid(3, proj_v3)) + # Check that items were transferred successfully by checking that item names are found in new + # 'items' dict and that they contain a dict + v2_items = proj_v2["items"] + v3_items = proj_v3["items"] + for name in v2_items.keys(): + self.assertTrue(name in v3_items.keys()) + self.assertIsInstance(v3_items[name], dict) def test_upgrade_v3_to_v4(self): pu = ProjectUpgrader(self.toolbox) @@ -201,18 +203,18 @@ def test_upgrade_v3_to_v4(self): spec_file_path = os.path.join(project_dir, "tool_specs", "preprocessing_tool.json") with open(spec_file_path, "w", encoding="utf-8") as tmp_spec_file: tmp_spec_file.write("hello") - # Upgrade to version 4 - proj_v4 = pu.upgrade(proj_v3, project_dir) - mock_backup.assert_called_once() - mock_force_save.assert_called_once() - self.assertTrue(pu.is_valid(4, proj_v4)) - # Check that items were transferred successfully by checking that item names are found in new - # 'items' dict and that they contain a dict - v3_items = proj_v3["items"] - v4_items = proj_v4["items"] - for name in v3_items.keys(): - self.assertTrue(name in v4_items.keys()) - self.assertIsInstance(v4_items[name], dict) + # Upgrade to version 4 + proj_v4 = pu.upgrade(proj_v3, project_dir) + mock_backup.assert_called_once() + mock_force_save.assert_called_once() + self.assertTrue(pu.is_valid(4, proj_v4)) + # Check that items were transferred successfully by checking that item names are found in new + # 'items' dict and that they contain a dict + v3_items = proj_v3["items"] + v4_items = proj_v4["items"] + for name in v3_items.keys(): + self.assertTrue(name in v4_items.keys()) + self.assertIsInstance(v4_items[name], dict) def test_upgrade_v4_to_v5(self): pu = ProjectUpgrader(self.toolbox) @@ -231,27 +233,27 @@ def test_upgrade_v4_to_v5(self): spec_file_path = os.path.join(project_dir, "tool_specs", "preprocessing_tool.json") with open(spec_file_path, "w", encoding="utf-8") as tmp_spec_file: tmp_spec_file.write("hello") - # Upgrade to version 5 - proj_v5 = pu.upgrade(proj_v4, project_dir) - mock_backup.assert_called_once() - mock_force_save.assert_called_once() - self.assertTrue(pu.is_valid(5, proj_v5)) - # Check that items were transferred successfully by checking that item names are found in new - # 'items' dict and that they contain a dict. Combiners should be gone in v5 - v4_items = proj_v4["items"] - # Make a list of Combiner names - combiners = list() - for name, d in v4_items.items(): - if d["type"] == "Combiner": - combiners.append(name) - v5_items = proj_v5["items"] - for name in v4_items.keys(): - if name in combiners: - # v5 should not have Combiners anymore - self.assertFalse(name in v5_items.keys()) - else: - self.assertTrue(name in v5_items.keys()) - self.assertIsInstance(v5_items[name], dict) + # Upgrade to version 5 + proj_v5 = pu.upgrade(proj_v4, project_dir) + mock_backup.assert_called_once() + mock_force_save.assert_called_once() + self.assertTrue(pu.is_valid(5, proj_v5)) + # Check that items were transferred successfully by checking that item names are found in new + # 'items' dict and that they contain a dict. Combiners should be gone in v5 + v4_items = proj_v4["items"] + # Make a list of Combiner names + combiners = list() + for name, d in v4_items.items(): + if d["type"] == "Combiner": + combiners.append(name) + v5_items = proj_v5["items"] + for name in v4_items.keys(): + if name in combiners: + # v5 should not have Combiners anymore + self.assertFalse(name in v5_items.keys()) + else: + self.assertTrue(name in v5_items.keys()) + self.assertIsInstance(v5_items[name], dict) def test_upgrade_v9_to_v10(self): pu = ProjectUpgrader(self.toolbox) @@ -270,28 +272,52 @@ def test_upgrade_v9_to_v10(self): spec_file_path = os.path.join(project_dir, "tool_specs", "preprocessing_tool.json") with open(spec_file_path, "w", encoding="utf-8") as tmp_spec_file: tmp_spec_file.write("hello") - # Upgrade to version 10 - proj_v10 = pu.upgrade(proj_v9, project_dir) - mock_backup.assert_called_once() - mock_force_save.assert_called_once() - self.assertTrue(pu.is_valid(10, proj_v10)) - v10_items = proj_v10["items"] - # Make a list of Gimlet and GdxExporter names in v9 - names = list() - for name, d in proj_v9["items"].items(): - if d["type"] in ["Gimlet", "GdxExporter"]: - names.append(name) - self.assertEqual(4, len(names)) # Old should have 3 Gimlets, 1 GdxExporter - # Check that connections have been removed - for conn in proj_v10["project"]["connections"]: - for name in names: - self.assertTrue(name not in conn["from"] and name not in conn["to"]) - # Check that gimlet and GdxExporter dicts are gone from items - for item_name in v10_items.keys(): - self.assertTrue(item_name not in names) - # Check number of connections - self.assertEqual(8, len(proj_v9["project"]["connections"])) - self.assertEqual(1, len(proj_v10["project"]["connections"])) + # Upgrade to version 10 + proj_v10 = pu.upgrade(proj_v9, project_dir) + mock_backup.assert_called_once() + mock_force_save.assert_called_once() + self.assertTrue(pu.is_valid(10, proj_v10)) + v10_items = proj_v10["items"] + # Make a list of Gimlet and GdxExporter names in v9 + names = list() + for name, d in proj_v9["items"].items(): + if d["type"] in ["Gimlet", "GdxExporter"]: + names.append(name) + self.assertEqual(4, len(names)) # Old should have 3 Gimlets, 1 GdxExporter + # Check that connections have been removed + for conn in proj_v10["project"]["connections"]: + for name in names: + self.assertTrue(name not in conn["from"] and name not in conn["to"]) + # Check that gimlet and GdxExporter dicts are gone from items + for item_name in v10_items.keys(): + self.assertTrue(item_name not in names) + # Check number of connections + self.assertEqual(8, len(proj_v9["project"]["connections"])) + self.assertEqual(1, len(proj_v10["project"]["connections"])) + + def test_upgrade_v10_to_v11(self): + pu = ProjectUpgrader(self.toolbox) + proj_v10 = make_v10_project_dict() + self.assertTrue(pu.is_valid(10, proj_v10)) + with TemporaryDirectory() as project_dir: + with mock.patch( + "spinetoolbox.project_upgrader.ProjectUpgrader.backup_project_file" + ) as mock_backup, mock.patch( + "spinetoolbox.project_upgrader.ProjectUpgrader.force_save" + ) as mock_force_save, mock.patch( + 'spinetoolbox.project_upgrader.LATEST_PROJECT_VERSION', 11 + ): + os.mkdir(os.path.join(project_dir, "tool_specs")) # Make /tool_specs dir + proj_v11 = pu.upgrade(proj_v10, project_dir) + mock_backup.assert_called_once() + mock_force_save.assert_called_once() + self.assertTrue(pu.is_valid(11, proj_v11)) + self.assertEqual(proj_v11["project"]["version"], 11) + self.assertIn("settings", proj_v11["project"]) + try: + ProjectSettings.from_dict(proj_v11["project"]["settings"]) + except: + self.fail("project settings cannot be deserialized") def test_upgrade_v1_to_latest(self): pu = ProjectUpgrader(self.toolbox) @@ -306,21 +332,22 @@ def test_upgrade_v1_to_latest(self): spec_file_path = os.path.join(project_dir, "tool_specs", "preprocessing_tool.json") with open(spec_file_path, "w", encoding="utf-8") as tmp_spec_file: tmp_spec_file.write("hello") - # Upgrade to latest version - proj_latest = pu.upgrade(proj_v1, project_dir) - mock_backup.assert_called_once() - mock_force_save.assert_called_once() - self.assertTrue(pu.is_valid(LATEST_PROJECT_VERSION, proj_latest)) - # Check that items were transferred successfully by checking that item names are found in new - # 'items' dict and that they contain a dict. Combiners should be gone in v5 - v1_items = proj_v1["objects"] - latest_items = proj_latest["items"] - # v1 project items were categorized under a dict which were inside an 'objects' dict - for item_category in v1_items.keys(): - for name in v1_items[item_category]: - self.assertTrue(name in latest_items.keys()) - self.assertIsInstance(latest_items[name], dict) - self.assertTrue(latest_items[name]["type"] == item_category[:-1]) + # Upgrade to latest version + proj_latest = pu.upgrade(proj_v1, project_dir) + mock_backup.assert_called_once() + mock_force_save.assert_called_once() + self.assertTrue(pu.is_valid(LATEST_PROJECT_VERSION, proj_latest)) + self.assertEqual(proj_latest["project"]["version"], LATEST_PROJECT_VERSION) + # Check that items were transferred successfully by checking that item names are found in new + # 'items' dict and that they contain a dict. Combiners should be gone in v5 + v1_items = proj_v1["objects"] + latest_items = proj_latest["items"] + # v1 project items were categorized under a dict which were inside an 'objects' dict + for item_category in v1_items.keys(): + for name in v1_items[item_category]: + self.assertTrue(name in latest_items.keys()) + self.assertIsInstance(latest_items[name], dict) + self.assertTrue(latest_items[name]["type"] == item_category[:-1]) def test_upgrade_with_too_recent_project_version(self): """Tests that projects with too recent versions are not opened.""" @@ -358,6 +385,10 @@ def make_v10_project_dict(): return _get_project_dict(10) +def make_v11_project_dict(): + return _get_project_dict(11) + + def _get_project_dict(v): """Returns a project dict read from a file according to given version.""" project_json_versions_dir = os.path.join(str(Path(__file__).parent), "test_resources", "project_json_versions") diff --git a/tests/test_SpineToolboxProject.py b/tests/test_SpineToolboxProject.py index f7c8705fa..28391f6a3 100644 --- a/tests/test_SpineToolboxProject.py +++ b/tests/test_SpineToolboxProject.py @@ -543,7 +543,14 @@ def test_save_when_storing_item_local_data(self): project_dict, { "items": {"test item": {"type": "Tester", "a": {"c": 2}}}, - "project": {"connections": [], "description": "", "jumps": [], "specifications": {}, "version": 10}, + "project": { + "connections": [], + "description": "", + "jumps": [], + "settings": {"enable_execute_all": True}, + "specifications": {}, + "version": 11, + }, }, ) with Path(project.config_dir, PROJECT_LOCAL_DATA_DIR_NAME, PROJECT_LOCAL_DATA_FILENAME).open() as fp: diff --git a/tests/test_resources/Project Directory/.spinetoolbox/project.json b/tests/test_resources/Project Directory/.spinetoolbox/project.json index 59ed29ed8..41e1ba08e 100644 --- a/tests/test_resources/Project Directory/.spinetoolbox/project.json +++ b/tests/test_resources/Project Directory/.spinetoolbox/project.json @@ -1,6 +1,6 @@ { "project": { - "version": 10, + "version": 11, "description": "Project for unit tests.", "specifications": { "Tool": [], @@ -38,7 +38,10 @@ "left" ] } - ] + ], + "settings": { + "enable_execute_all": true + } }, "items": { "a": { diff --git a/tests/test_resources/project_json_versions/proj_v11.json b/tests/test_resources/project_json_versions/proj_v11.json new file mode 100644 index 000000000..29c5ca97d --- /dev/null +++ b/tests/test_resources/project_json_versions/proj_v11.json @@ -0,0 +1,293 @@ +{ + "project": { + "version": 11, + "description": "Import and Export", + "settings": {"enable_execute_all": true}, + "specifications": { + "Importer": [ + { + "type": "path", + "relative": true, + "path": "Importer 1 - units.xlsx.json" + } + ], + "Exporter": [ + { + "type": "path", + "relative": true, + "path": ".spinetoolbox/specifications/Exporter/pekka.json" + }, + { + "type": "path", + "relative": true, + "path": ".spinetoolbox/specifications/Exporter/gdx_export_mapping.json" + } + ], + "Tool": [ + { + "type": "path", + "relative": true, + "path": ".spinetoolbox/specifications/Tool/testeri.json" + } + ] + }, + "connections": [ + { + "name": "from Raw data to Importer 1", + "from": [ + "Raw data", + "right" + ], + "to": [ + "Importer 1", + "left" + ] + }, + { + "name": "from Importer 1 to DS1", + "from": [ + "Importer 1", + "right" + ], + "to": [ + "DS1", + "left" + ], + "options": { + "purge_before_writing": true, + "purge_settings": null + } + }, + { + "name": "from DS2 to Merger 1", + "from": [ + "DS2", + "right" + ], + "to": [ + "Merger 1", + "left" + ] + }, + { + "name": "from Merger 1 to Output Db", + "from": [ + "Merger 1", + "right" + ], + "to": [ + "Output Db", + "left" + ], + "options": { + "purge_before_writing": true, + "purge_settings": { + "object_class": true, + "relationship_class": true, + "parameter_value_list": true, + "list_value": true, + "parameter_definition": true, + "object": true, + "relationship": true, + "entity_group": true, + "parameter_value": true, + "alternative": true, + "scenario": true, + "scenario_alternative": true, + "feature": true, + "tool": true, + "tool_feature": true, + "tool_feature_method": true, + "metadata": true, + "entity_metadata": true, + "parameter_value_metadata": true + } + } + }, + { + "name": "from GDX file to GDX Exporter", + "from": [ + "GDX file", + "right" + ], + "to": [ + "GDX Exporter", + "left" + ] + }, + { + "name": "from DS1 to Exporter 1", + "from": [ + "DS1", + "right" + ], + "to": [ + "Exporter 1", + "left" + ] + }, + { + "name": "from DS1 to Merger 1", + "from": [ + "DS1", + "right" + ], + "to": [ + "Merger 1", + "left" + ] + } + ], + "jumps": [] + }, + "items": { + "Importer 1": { + "type": "Importer", + "description": "", + "x": 37.00929801385436, + "y": -50.831770740600405, + "specification": "Importer 1 - units.xlsx", + "cancel_on_error": true, + "on_conflict": "replace", + "file_selection": [ + [ + "/a.csv", + false + ], + [ + "/c.ini", + false + ], + [ + "/d.txt", + false + ], + [ + "/units.xlsx", + true + ], + [ + "/data.txt", + false + ] + ] + }, + "DS1": { + "type": "Data Store", + "description": "", + "x": 161.72875313326617, + "y": -141.85480886591952, + "url": { + "dialect": "sqlite", + "host": "", + "port": "", + "database": { + "type": "path", + "relative": true, + "path": ".spinetoolbox/items/ds1/DS1.sqlite" + } + } + }, + "Raw data": { + "type": "Data Connection", + "description": "", + "x": -92.35946011852081, + "y": -139.9349488970861, + "file_references": [ + { + "type": "path", + "relative": true, + "path": "data.txt" + } + ], + "db_references": [] + }, + "Exporter 1": { + "type": "Exporter", + "description": "", + "x": 327.28798970484826, + "y": -34.157876811488, + "output_time_stamps": true, + "cancel_on_error": true, + "output_labels": [ + { + "in_label": "db_url@DS1", + "out_label": "output_file" + } + ], + "specification": "SpineOptToTable" + }, + "Tool 1": { + "type": "Tool", + "description": "", + "x": 8.781957645503311, + "y": -261.72457943295257, + "specification": "testeri", + "execute_in_work": true, + "cmd_line_args": [] + }, + "Merger 1": { + "type": "Merger", + "description": "", + "x": 318.01113350038577, + "y": -194.14196047822415, + "cancel_on_error": false + }, + "DS2": { + "type": "Data Store", + "description": "", + "x": 164.00244309606893, + "y": -266.32829810435675, + "url": { + "dialect": "sqlite", + "host": "", + "port": "", + "database": { + "type": "path", + "relative": true, + "path": ".spinetoolbox/items/ds2/DS2.sqlite" + } + } + }, + "Output Db": { + "type": "Data Store", + "description": "", + "x": 448.8029423394426, + "y": -215.88120679697633, + "url": { + "dialect": "sqlite", + "host": "", + "port": "", + "database": { + "type": "path", + "relative": true, + "path": ".spinetoolbox/items/output_db/Output Db.sqlite" + } + } + }, + "GDX Exporter": { + "type": "Exporter", + "description": "", + "x": 180.76090893795083, + "y": 36.76493063144763, + "output_time_stamps": false, + "cancel_on_error": true, + "output_labels": [], + "specification": "gdx export mapping" + }, + "GDX file": { + "type": "Data Connection", + "description": "", + "x": -69.44486897051219, + "y": 35.74368255835186, + "file_references": [ + { + "type": "path", + "relative": false, + "path": "C:/Users/ttepsa/OneDrive - Teknologian Tutkimuskeskus VTT/Documents/SpineToolboxProjects/Gdx Export Test/.spinetoolbox/items/gdx_exporter/file.gdx" + } + ], + "db_references": [] + } + } +}