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/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) 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 3a1a9201a..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, 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 - settings (QSettings): Toolbox settings + app_settings (QSettings): Toolbox settings + settings (ProjectSettings): project settings logger (LoggerInterface): a logger instance """ _, name = os.path.split(p_dir) @@ -124,6 +126,7 @@ def __init__(self, toolbox, p_dir, plugin_specs, settings, logger): self._connections = list() self._jumps = list() self._logger = logger + self._app_settings = app_settings self._settings = settings self._engine_workers = [] self._execution_in_progress = False @@ -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 @@ -296,7 +305,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 +982,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 +1410,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 +1421,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 +1471,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/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 be9ef09dd..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, 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, 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": [] + } + } +}