diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 37628b76..c610bedb 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -15,13 +15,13 @@ jobs: os: [windows-latest, ubuntu-22.04] python-version: [3.8, 3.9, "3.10", 3.11] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Version from Git tags run: git describe --tags - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Display Python version @@ -43,6 +43,9 @@ jobs: - name: List packages run: pip list + - name: Install coverage + run: + python -m pip install coverage[toml] - name: Run tests run: | if [ "$RUNNER_OS" != "Windows" ]; then @@ -51,4 +54,6 @@ jobs: coverage run -m unittest discover --verbose shell: bash - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/bin/append_license.py b/bin/append_license.py index fd11f9e6..2f55f473 100755 --- a/bin/append_license.py +++ b/bin/append_license.py @@ -6,6 +6,7 @@ license_text = [ "######################################################################################################################\n", "# Copyright (C) 2017-2022 Spine project consortium\n", + "# Copyright Spine Items contributors\n", "# This file is part of Spine Items.\n", "# Spine Items is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General\n", "# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)\n", diff --git a/bin/update_copyrights.py b/bin/update_copyrights.py deleted file mode 100644 index 536f572b..00000000 --- a/bin/update_copyrights.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python - -from pathlib import Path -import time - - -current_year = time.gmtime().tm_year -root_dir = Path(__file__).parent.parent -project_source_dir = Path(root_dir, "spine_items") -test_source_dir = Path(root_dir, "tests") - -expected = f"# Copyright (C) 2017-{current_year} Spine project consortium" - - -def update_copyrights(path, suffix, recursive=True): - for path in path.iterdir(): - if path.suffix == suffix: - i = 0 - with open(path) as python_file: - lines = python_file.readlines() - for i, line in enumerate(lines[1:4]): - if line.startswith("# Copyright (C) "): - lines[i + 1] = lines[i + 1][:21] + str(current_year) + lines[i + 1][25:] - break - if len(lines) <= i + 1 or not lines[i + 1].startswith(expected): - print(f"Confusing or no copyright: {path}") - else: - with open(path, "w") as python_file: - python_file.writelines(lines) - elif recursive and path.is_dir(): - update_copyrights(path, suffix) - - -update_copyrights(root_dir, ".py", recursive=False) -update_copyrights(project_source_dir, ".py") -update_copyrights(project_source_dir, ".ui") -update_copyrights(test_source_dir, ".py") - -print("Done. Don't forget to update append_license.py!") diff --git a/pyproject.toml b/pyproject.toml index e0a725e8..c2ff02ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ classifiers = [ ] requires-python = ">=3.8.1, <3.12" dependencies = [ - "pyside6 >= 6.5.0, != 6.5.3", + "pyside6 >= 6.5.0, != 6.5.3, != 6.6.3", "pyodbc >=4.0", # v1.4 does not pass tests "sqlalchemy >=1.3, <1.4", @@ -27,9 +27,6 @@ dependencies = [ [project.urls] Repository = "https://github.com/spine-tools/spine-items" -[project.optional-dependencies] -dev = ["coverage[toml]"] - [build-system] requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2", "wheel", "build"] build-backend = "setuptools.build_meta" @@ -58,5 +55,4 @@ ignore_errors = true [tool.black] line-length = 120 -skip-string-normalization = true exclude = '\.git|ui|resources_icons_rc.py' diff --git a/requirements.txt b/requirements.txt index 2ec92a1f..51082f94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -e git+https://github.com/spine-tools/Spine-Database-API.git#egg=spinedb_api -e git+https://github.com/spine-tools/spine-engine.git#egg=spine_engine -e git+https://github.com/spine-tools/Spine-Toolbox.git#egg=spinetoolbox --e .[dev] +-e . diff --git a/spine_items/__init__.py b/spine_items/__init__.py index 6d178f1b..0c9759f5 100644 --- a/spine_items/__init__.py +++ b/spine_items/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,9 +10,57 @@ # this program. If not, see . ###################################################################################################################### -""" -Spine items. +""" Spine items. """ +from .version import __version__ -""" -from .version import __version__ +def _factories_and_executable_items(): + from . import data_connection + from .data_connection.data_connection_factory import DataConnectionFactory + from . import data_store + from .data_store.data_store_factory import DataStoreFactory + from . import data_transformer + from .data_transformer.data_transformer_factory import DataTransformerFactory + from .data_transformer import specification_factory + from . import exporter + from .exporter.exporter_factory import ExporterFactory + from .exporter import specification_factory + from . import importer + from .importer.importer_factory import ImporterFactory + from .importer import specification_factory + from . import merger + from .merger.merger_factory import MergerFactory + from . import tool + from .tool.tool_factory import ToolFactory + from .tool import specification_factory + from . import view + from .view.view_factory import ViewFactory + + modules = (data_connection, data_store, data_transformer, exporter, importer, merger, tool, view) + item_infos = tuple(module.item_info.ItemInfo for module in modules) + factories = ( + DataConnectionFactory, + DataStoreFactory, + DataTransformerFactory, + ExporterFactory, + ImporterFactory, + MergerFactory, + ToolFactory, + ViewFactory, + ) + factories = {info.item_type(): factory for info, factory in zip(item_infos, factories)} + executables = {module.item_info.ItemInfo.item_type(): module.executable_item.ExecutableItem for module in modules} + specification_item_submodules = (data_transformer, exporter, importer, tool) + specification_factories = { + module.item_info.ItemInfo.item_type(): module.specification_factory.SpecificationFactory + for module in specification_item_submodules + } + return ( + factories.copy, + executables.copy, + specification_factories.copy, + ) + + +item_factories, executable_items, item_specification_factories = _factories_and_executable_items() +del _factories_and_executable_items diff --git a/spine_items/animations.py b/spine_items/animations.py index 3c560928..98ede7c5 100644 --- a/spine_items/animations.py +++ b/spine_items/animations.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Animation class for importers and exporters. - -""" - +"""Animation class for importers and exporters.""" from PySide6.QtGui import QPainterPath, QFont, QFontMetrics from PySide6.QtCore import Qt, Signal, Slot, QObject, QTimeLine, QRectF, QPointF, QLineF from PySide6.QtWidgets import QGraphicsPathItem diff --git a/spine_items/commands.py b/spine_items/commands.py index 3a01817f..a9ca8e68 100644 --- a/spine_items/commands.py +++ b/spine_items/commands.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,53 +10,59 @@ # this program. If not, see . ###################################################################################################################### -""" -Undo/redo commands that can be used by multiple project items. - -""" +"""Undo/redo commands that can be used by multiple project items.""" from spinetoolbox.project_commands import SpineToolboxCommand class UpdateCancelOnErrorCommand(SpineToolboxCommand): - def __init__(self, project_item, cancel_on_error): + def __init__(self, item_name, cancel_on_error, project): """Command to update Importer, Exporter, and Merger cancel on error setting. Args: - project_item (ProjectItem): Item + item_name (str): Item's name cancel_on_error (bool): New setting + project (SpineToolboxProject): project """ super().__init__() - self._project_item = project_item + self._item_name = item_name self._redo_cancel_on_error = cancel_on_error self._undo_cancel_on_error = not cancel_on_error - self.setText(f"change {project_item.name} cancel on error setting") + self._project = project + self.setText(f"change {item_name} cancel on error setting") def redo(self): - self._project_item.set_cancel_on_error(self._redo_cancel_on_error) + item = self._project.get_item(self._item_name) + item.set_cancel_on_error(self._redo_cancel_on_error) def undo(self): - self._project_item.set_cancel_on_error(self._undo_cancel_on_error) + item = self._project.get_item(self._item_name) + item.set_cancel_on_error(self._undo_cancel_on_error) class UpdateOnConflictCommand(SpineToolboxCommand): - def __init__(self, project_item, on_conflict): + def __init__(self, item_name, on_conflict, project): """Command to update Importer and Merger 'on conflict' setting. Args: - project_item (ProjectItem): Item + item_name (str): Item's name on_conflict (str): New setting + project (SpineToolboxProject): project """ super().__init__() - self._project_item = project_item + self._item_name = item_name self._redo_on_conflict = on_conflict - self._undo_on_conflict = self._project_item.on_conflict - self.setText(f"change {project_item.name} on conflict setting") + project_item = project.get_item(item_name) + self._undo_on_conflict = project_item.on_conflict + self._project = project + self.setText(f"change {item_name} on conflict setting") def redo(self): - self._project_item.set_on_conflict(self._redo_on_conflict) + item = self._project.get_item(self._item_name) + item.set_on_conflict(self._redo_on_conflict) def undo(self): - self._project_item.set_on_conflict(self._undo_on_conflict) + item = self._project.get_item(self._item_name) + item.set_on_conflict(self._undo_on_conflict) class ChangeItemSelectionCommand(SpineToolboxCommand): @@ -82,42 +89,52 @@ def undo(self): class UpdateCmdLineArgsCommand(SpineToolboxCommand): - def __init__(self, item, cmd_line_args): + def __init__(self, item_name, cmd_line_args, project): """Command to update Tool command line args. Args: - item (ProjectItemBase): the item + item_name (str): item's name cmd_line_args (list): list of command line args + project (SpineToolboxProject): project """ super().__init__() - self.item = item - self.redo_cmd_line_args = cmd_line_args - self.undo_cmd_line_args = self.item.cmd_line_args - self.setText(f"change command line arguments of {item.name}") + self._item_name = item_name + self._redo_cmd_line_args = cmd_line_args + item = project.get_item(item_name) + self._undo_cmd_line_args = item.cmd_line_args + self._project = project + self.setText(f"change command line arguments of {item_name}") def redo(self): - self.item.update_cmd_line_args(self.redo_cmd_line_args) + item = self._project.get_item(self._item_name) + item.update_cmd_line_args(self._redo_cmd_line_args) def undo(self): - self.item.update_cmd_line_args(self.undo_cmd_line_args) + item = self._project.get_item(self._item_name) + item.update_cmd_line_args(self._undo_cmd_line_args) class UpdateGroupIdCommand(SpineToolboxCommand): - def __init__(self, item, group_id): + def __init__(self, item_name, group_id, project): """Command to update item group identifier. Args: - item (ProjectItemBase): the item + item_name (str): item's name group_id (str): group identifier + project (SpineToolboxProject): project """ super().__init__() - self._item = item + self._item_name = item_name self._redo_group_id = group_id - self._undo_group_id = self._item.group_id - self.setText(f"change group identifier of {item.name}") + item = project.get_item(item_name) + self._undo_group_id = item.group_id + self._project = project + self.setText(f"change group identifier of {item_name}") def redo(self): - self._item.do_set_group_id(self._redo_group_id) + item = self._project.get_item(self._item_name) + item.do_set_group_id(self._redo_group_id) def undo(self): - self._item.do_set_group_id(self._undo_group_id) + item = self._project.get_item(self._item_name) + item.do_set_group_id(self._undo_group_id) diff --git a/spine_items/data_connection/__init__.py b/spine_items/data_connection/__init__.py index 96706b48..3c83086f 100644 --- a/spine_items/data_connection/__init__.py +++ b/spine_items/data_connection/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Data connection plugin. - -""" +"""Data connection plugin.""" diff --git a/spine_items/data_connection/commands.py b/spine_items/data_connection/commands.py index f04c7dfc..9ca7d409 100644 --- a/spine_items/data_connection/commands.py +++ b/spine_items/data_connection/commands.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Undo/redo commands for the DataConnection project item. - -""" +"""Undo/redo commands for the DataConnection project item.""" from pathlib import Path from spine_items.commands import SpineToolboxCommand @@ -20,67 +18,79 @@ class AddDCReferencesCommand(SpineToolboxCommand): """Command to add DC references.""" - def __init__(self, dc, file_refs, db_refs): + def __init__(self, dc_name, file_refs, db_refs, project): """ Args: - dc (DataConnection): the DC + dc_name (str): DC name file_refs (list of str): list of file refs to add db_refs (list of str): list of db refs to add + project (SpineToolboxProject): project """ super().__init__() - self.dc = dc - self.file_refs = file_refs - self.db_refs = db_refs - self.setText(f"add references to {dc.name}") + self._dc_name = dc_name + self._file_refs = file_refs + self._db_refs = db_refs + self._project = project + self.setText(f"add references to {dc_name}") def redo(self): - self.dc.do_add_references(self.file_refs, self.db_refs) + dc = self._project.get_item(self._dc_name) + dc.do_add_references(self._file_refs, self._db_refs) def undo(self): - self.dc.do_remove_references(self.file_refs, self.db_refs) + dc = self._project.get_item(self._dc_name) + dc.do_remove_references(self._file_refs, self._db_refs) class RemoveDCReferencesCommand(SpineToolboxCommand): """Command to remove DC references.""" - def __init__(self, dc, file_refs, db_refs): + def __init__(self, dc_name, file_refs, db_refs, project): """ Args: - dc (DataConnection): the DC + dc_name (str): DC name file_refs (list of str): list of file refs to remove db_refs (list of str): list of db refs to remove + project (SpineToolboxProject): project """ super().__init__() - self.dc = dc - self.file_refs = file_refs - self.db_refs = db_refs - self.setText(f"remove references from {dc.name}") + self._dc_name = dc_name + self._file_refs = file_refs + self._db_refs = db_refs + self._project = project + self.setText(f"remove references from {dc_name}") def redo(self): - self.dc.do_remove_references(self.file_refs, self.db_refs) + dc = self._project.get_item(self._dc_name) + dc.do_remove_references(self._file_refs, self._db_refs) def undo(self): - self.dc.do_add_references(self.file_refs, self.db_refs) + dc = self._project.get_item(self._dc_name) + dc.do_add_references(self._file_refs, self._db_refs) class MoveReferenceToData(SpineToolboxCommand): """Command to move DC references to data.""" - def __init__(self, dc, paths): + def __init__(self, dc_name, paths, project): """ Args: - dc (DataConnection): the DC + dc_name (str): DC name paths (list of str): list of paths to move + project (SpineToolboxProject): project """ super().__init__() - self._dc = dc + self._dc_name = dc_name self._paths = paths - self.setText("copy references to data") + self._project = project + self.setText(f"copy references to data in {dc_name}") def redo(self): - self._dc.do_copy_to_project(self._paths) - self._dc.do_remove_references(self._paths, []) + dc = self._project.get_item(self._dc_name) + dc.do_copy_to_project(self._paths) + dc.do_remove_references(self._paths, []) def undo(self): - self._dc.delete_files_from_project([Path(p).name for p in self._paths]) - self._dc.do_add_references(self._paths, []) + dc = self._project.get_item(self._dc_name) + dc.delete_files_from_project([Path(p).name for p in self._paths]) + dc.do_add_references(self._paths, []) diff --git a/spine_items/data_connection/custom_file_system_watcher.py b/spine_items/data_connection/custom_file_system_watcher.py index 685d20f6..7a6da872 100644 --- a/spine_items/data_connection/custom_file_system_watcher.py +++ b/spine_items/data_connection/custom_file_system_watcher.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains CustomFileSystemWatcher. - -""" - +"""Contains CustomFileSystemWatcher.""" import os from PySide6.QtCore import QFileSystemWatcher, Signal, Slot diff --git a/spine_items/data_connection/data_connection.py b/spine_items/data_connection/data_connection.py index 1b9ffaf8..9b07d1e7 100644 --- a/spine_items/data_connection/data_connection.py +++ b/spine_items/data_connection/data_connection.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,8 +9,8 @@ # 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 . ###################################################################################################################### -"""Module for data connection class.""" +"""Module for data connection class.""" import os import shutil import logging @@ -97,11 +98,6 @@ def item_type(): """See base class.""" return ItemInfo.item_type() - @staticmethod - def item_category(): - """See base class.""" - return ItemInfo.item_category() - @property def executable_class(self): return ExecutableItem @@ -218,7 +214,7 @@ def _add_file_references(self, paths): if repeated_paths: self._logger.msg_warning.emit(f"Reference to file(s) {repeated_paths} already exists") if new_paths: - self._toolbox.undo_stack.push(AddDCReferencesCommand(self, new_paths, [])) + self._toolbox.undo_stack.push(AddDCReferencesCommand(self.name, new_paths, [], self._project)) @Slot(bool) def show_add_db_reference_dialog(self, _=False): @@ -235,7 +231,7 @@ def show_add_db_reference_dialog(self, _=False): self._database_validator.validate_url( url["dialect"], sa_url, self._log_database_reference_error, success_slot=None ) - self._toolbox.undo_stack.push(AddDCReferencesCommand(self, [], [url])) + self._toolbox.undo_stack.push(AddDCReferencesCommand(self.name, [], [url], self._project)) def _has_db_reference(self, url): """Checks if given database URL exists already. @@ -255,13 +251,20 @@ def _has_db_reference(self, url): return True return False - @Slot(str) - def _log_database_reference_error(self, error): + @Slot(str, object) + def _log_database_reference_error(self, error, url): """Logs final database validation error messages. Args: error (str): message + url (URL): SqlAlchemy URL of the database """ + url_text = remove_credentials_from_url(str(url)) + for row in range(self._db_ref_root.rowCount()): + item = self._db_ref_root.child(row) + if url_text == item.text(): + self._mark_as_missing(item) + break self._logger.msg_error.emit(f"{self.name}: invalid database URL: {error}") def do_add_references(self, file_refs, db_refs): @@ -299,7 +302,9 @@ def remove_references(self, _=False): file_references.append(index.data(Qt.ItemDataRole.DisplayRole)) elif parent == db_ref_root_index: db_references.append(index.data(_Role.DB_URL_REFERENCE)) - self._toolbox.undo_stack.push(RemoveDCReferencesCommand(self, file_references, db_references)) + self._toolbox.undo_stack.push( + RemoveDCReferencesCommand(self.name, file_references, db_references, self._project) + ) self._logger.msg.emit("Selected references removed") def do_remove_references(self, file_refs, db_refs): @@ -493,7 +498,9 @@ def copy_to_project(self, _=False): if not selected_indexes: self._logger.msg_warning.emit("No files to copy") return - self._toolbox.undo_stack.push(MoveReferenceToData(self, [index.data() for index in selected_indexes])) + self._toolbox.undo_stack.push( + MoveReferenceToData(self.name, [index.data() for index in selected_indexes], self._project) + ) def do_copy_to_project(self, paths): """Copies given files to item's data directory. diff --git a/spine_items/data_connection/data_connection_factory.py b/spine_items/data_connection/data_connection_factory.py index ead05841..cbe10234 100644 --- a/spine_items/data_connection/data_connection_factory.py +++ b/spine_items/data_connection/data_connection_factory.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -The DataConnectionFactory class. - -""" - +"""The DataConnectionFactory class.""" from PySide6.QtGui import QColor from spinetoolbox.project_item.project_item_factory import ProjectItemFactory from .data_connection_icon import DataConnectionIcon diff --git a/spine_items/data_connection/data_connection_icon.py b/spine_items/data_connection/data_connection_icon.py index 05fa1f2f..3fe67d1f 100644 --- a/spine_items/data_connection/data_connection_icon.py +++ b/spine_items/data_connection/data_connection_icon.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Module for data connection icon class. - -""" - +"""Module for data connection icon class.""" import os from PySide6.QtCore import QObject, Qt, QTimer, Signal from PySide6.QtWidgets import QGraphicsItem @@ -88,4 +85,3 @@ def select_on_drag_over(self): self._drag_over = False self._toolbox.ui.graphicsView.scene().clearSelection() self.setSelected(True) - self.select_item() diff --git a/spine_items/data_connection/executable_item.py b/spine_items/data_connection/executable_item.py index c46beeb2..8baeb8a3 100644 --- a/spine_items/data_connection/executable_item.py +++ b/spine_items/data_connection/executable_item.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains Data Connection's executable item as well as support utilities. - -""" +"""Contains Data Connection's executable item as well as support utilities.""" import os from spine_engine.project_item.executable_item_base import ExecutableItemBase from spine_engine.utils.serialization import deserialize_path @@ -29,7 +27,7 @@ def __init__(self, name, file_references, db_references, project_dir, logger): Args: name (str): item's name file_references (list): a list of absolute paths to connected files - db_references (list): a list of urls to connected dbs + db_references (list): a list of url dicts to connected dbs project_dir (str): absolute path to project directory logger (LoggerInterface): a logger """ diff --git a/spine_items/data_connection/item_info.py b/spine_items/data_connection/item_info.py index fa7297f0..13ed1188 100644 --- a/spine_items/data_connection/item_info.py +++ b/spine_items/data_connection/item_info.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,19 +10,11 @@ # this program. If not, see . ###################################################################################################################### -""" -Data Connection project item info. - -""" +"""Data Connection project item info.""" from spine_engine.project_item.project_item_info import ProjectItemInfo class ItemInfo(ProjectItemInfo): - @staticmethod - def item_category(): - """See base class.""" - return "Data Connections" - @staticmethod def item_type(): """See base class.""" diff --git a/spine_items/data_connection/output_resources.py b/spine_items/data_connection/output_resources.py index d7b66a00..6bb6c893 100644 --- a/spine_items/data_connection/output_resources.py +++ b/spine_items/data_connection/output_resources.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,15 +9,13 @@ # 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 utilities to scan for Data Connection's output resources. -""" +"""Contains utilities to scan for Data Connection's output resources.""" from pathlib import Path from spine_engine.project_item.project_item_resource import file_resource, transient_file_resource, url_resource from spine_engine.utils.serialization import path_in_dir from spinedb_api.helpers import remove_credentials_from_url -from ..utils import convert_to_sqlalchemy_url, unsplit_url_credentials +from ..utils import convert_to_sqlalchemy_url def scan_for_resources(provider, file_paths, urls, project_dir): diff --git a/spine_items/data_connection/ui/__init__.py b/spine_items/data_connection/ui/__init__.py index 4a162847..a7ecd49b 100644 --- a/spine_items/data_connection/ui/__init__.py +++ b/spine_items/data_connection/ui/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/data_connection/ui/data_connection_properties.py b/spine_items/data_connection/ui/data_connection_properties.py index a750371b..9e63b68b 100644 --- a/spine_items/data_connection/ui/data_connection_properties.py +++ b/spine_items/data_connection/ui/data_connection_properties.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/data_connection/utils.py b/spine_items/data_connection/utils.py index 1e8b5b4c..382b27d8 100644 --- a/spine_items/data_connection/utils.py +++ b/spine_items/data_connection/utils.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,6 +9,7 @@ # 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 . ###################################################################################################################### + """This module contains utilities for Data Connection.""" import sys import urllib.parse @@ -32,18 +34,20 @@ def restore_database_references(references_list, credentials_dict, project_dir): # legacy db reference url = urllib.parse.urlparse(reference_dict) dialect = _dialect_from_scheme(url.scheme) - database = url.path[1:] + path = url.path + if dialect == "sqlite" and sys.platform == "win32": + # Remove extra '/' from file path on Windows. + path = path[1:] db_reference = { "dialect": dialect, "host": url.hostname, "port": url.port, - "database": database, + "database": path, } else: db_reference = dict(reference_dict) if db_reference["dialect"] == "sqlite": db_reference["database"] = deserialize_path(db_reference["database"], project_dir) - db_reference["username"], db_reference["password"] = credentials_dict.get( convert_url_to_safe_string(db_reference), (None, None) ) diff --git a/spine_items/data_connection/widgets/__init__.py b/spine_items/data_connection/widgets/__init__.py index f722e088..672b5890 100644 --- a/spine_items/data_connection/widgets/__init__.py +++ b/spine_items/data_connection/widgets/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Widgets for the Data Connection project item. - -""" +"""Widgets for the Data Connection project item.""" diff --git a/spine_items/data_connection/widgets/add_data_connection_widget.py b/spine_items/data_connection/widgets/add_data_connection_widget.py index e5dce92d..861b3d8a 100644 --- a/spine_items/data_connection/widgets/add_data_connection_widget.py +++ b/spine_items/data_connection/widgets/add_data_connection_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Widget shown to user when a new Data Connection is created. - -""" - +"""Widget shown to user when a new Data Connection is created.""" from spinetoolbox.widgets.add_project_item_widget import AddProjectItemWidget from ..data_connection import DataConnection from ..item_info import ItemInfo diff --git a/spine_items/data_connection/widgets/custom_menus.py b/spine_items/data_connection/widgets/custom_menus.py index c970a005..9904b3ae 100644 --- a/spine_items/data_connection/widgets/custom_menus.py +++ b/spine_items/data_connection/widgets/custom_menus.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Classes for custom context menus and pop-up menus. - -""" - +"""Classes for custom context menus and pop-up menus.""" from spinetoolbox.widgets.custom_menus import CustomContextMenu @@ -37,7 +34,7 @@ def __init__(self, parent, position, index, dc): self.add_action("Remove reference(s)", enabled=dc.any_refs_selected) self.add_action("Copy file reference(s) to project", enabled=dc.file_refs_selected) self.addSeparator() - self.add_action("Refresh reference(s)", enabled=dc.file_refs_selected) + self.add_action("Refresh reference(s)", enabled=dc.any_refs_selected) class DcDataContextMenu(CustomContextMenu): diff --git a/spine_items/data_connection/widgets/data_connection_properties_widget.py b/spine_items/data_connection/widgets/data_connection_properties_widget.py index 3c5a9e3a..7dc2112f 100644 --- a/spine_items/data_connection/widgets/data_connection_properties_widget.py +++ b/spine_items/data_connection/widgets/data_connection_properties_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Data connection properties widget. - -""" - +"""Data connection properties widget.""" import os from PySide6.QtCore import QPoint, Qt, Slot, QUrl from spinetoolbox.widgets.properties_widget import PropertiesWidgetBase @@ -51,8 +48,7 @@ def show_references_context_menu(self, pos): pos (QPoint): Mouse position """ index = self.ui.treeView_dc_references.indexAt(pos) - dc_index = self._toolbox.ui.treeView_project.currentIndex() - dc = self._toolbox.project_item_model.item(dc_index).project_item + dc = self._active_item global_pos = self.ui.treeView_dc_references.viewport().mapToGlobal(pos) dc_ref_context_menu = DcRefContextMenu(self, global_pos, index, dc) option = dc_ref_context_menu.get_action() @@ -86,8 +82,7 @@ def show_data_context_menu(self, pos): pos (QPoint): Mouse position """ index = self.ui.treeView_dc_data.indexAt(pos) - dc_index = self._toolbox.ui.treeView_project.currentIndex() - dc = self._toolbox.project_item_model.item(dc_index).project_item + dc = self._active_item global_pos = self.ui.treeView_dc_data.viewport().mapToGlobal(pos) dc_data_context_menu = DcDataContextMenu(self, global_pos, index, dc) option = dc_data_context_menu.get_action() diff --git a/spine_items/data_store/__init__.py b/spine_items/data_store/__init__.py index 4086ac13..86f6272f 100644 --- a/spine_items/data_store/__init__.py +++ b/spine_items/data_store/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Data store plugin. - -""" +"""Data store plugin.""" diff --git a/spine_items/data_store/commands.py b/spine_items/data_store/commands.py index a68b813f..4f096164 100644 --- a/spine_items/data_store/commands.py +++ b/spine_items/data_store/commands.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Undo/redo commands for the DataStore project item. - -""" +"""Undo/redo commands for the DataStore project item.""" from enum import IntEnum, unique from spine_items.commands import SpineToolboxCommand @@ -23,33 +21,41 @@ class CommandId(IntEnum): class UpdateDSURLCommand(SpineToolboxCommand): - def __init__(self, ds, was_url_valid, **kwargs): - """Command to update DS url. + """Command to update DS url.""" + def __init__(self, ds_name, was_url_valid, project, **kwargs): + """ Args: - ds (DataStore): the DS + ds_name (str): DS name was_url_valid (bool): True if previous URL was valid, False otherwise - kwargs: url keys and their values + project (SpineToolboxProject): project + **kwargs: url keys and their values """ super().__init__() - self.ds = ds + self._ds_name = ds_name self._undo_url_is_valid = was_url_valid - self.redo_kwargs = kwargs - self.undo_kwargs = {k: self.ds.url()[k] for k in kwargs} + self._redo_kwargs = kwargs + ds = project.get_item(ds_name) + self._undo_kwargs = {k: ds.url()[k] for k in kwargs} + self._project = project if len(kwargs) == 1: - self.setText(f"change {list(kwargs.keys())[0]} of {ds.name}") + self.setText(f"change {list(kwargs.keys())[0]} of {ds_name}") else: - self.setText(f"change url of {ds.name}") + self.setText(f"change url of {ds_name}") def id(self): return CommandId.UPDATE_URL.value def mergeWith(self, command): - if not isinstance(command, UpdateDSURLCommand) or self.ds is not command.ds or command._undo_url_is_valid: + if ( + not isinstance(command, UpdateDSURLCommand) + or self._ds_name != command._ds_name + or command._undo_url_is_valid + ): return False diff_key = None - for key, value in self.redo_kwargs.items(): - old_value = self.undo_kwargs[key] + for key, value in self._redo_kwargs.items(): + old_value = self._undo_kwargs[key] if value != old_value: if diff_key is not None: return False @@ -59,16 +65,18 @@ def mergeWith(self, command): raise RuntimeError("Logic error: nothing changes between undo and redo URLs.") if diff_key == "dialect": return False - changed_value = command.redo_kwargs[diff_key] - if self.redo_kwargs[diff_key] == changed_value: + changed_value = command._redo_kwargs[diff_key] + if self._redo_kwargs[diff_key] == changed_value: return False - self.redo_kwargs[diff_key] = changed_value - if self.redo_kwargs[diff_key] == self.undo_kwargs[diff_key]: + self._redo_kwargs[diff_key] = changed_value + if self._redo_kwargs[diff_key] == self._undo_kwargs[diff_key]: self.setObsolete(True) return True def redo(self): - self.ds.do_update_url(**self.redo_kwargs) + ds = self._project.get_item(self._ds_name) + ds.do_update_url(**self._redo_kwargs) def undo(self): - self.ds.do_update_url(**self.undo_kwargs) + ds = self._project.get_item(self._ds_name) + ds.do_update_url(**self._undo_kwargs) diff --git a/spine_items/data_store/data_store.py b/spine_items/data_store/data_store.py index 536ae6b7..fa19e24d 100644 --- a/spine_items/data_store/data_store.py +++ b/spine_items/data_store/data_store.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,18 +10,14 @@ # this program. If not, see . ###################################################################################################################### -""" -Module for data store class. - -""" - +""" Module for data store class. """ import os from dataclasses import dataclass from shutil import copyfile from PySide6.QtCore import Slot from PySide6.QtWidgets import QFileDialog, QApplication, QMenu from PySide6.QtGui import QAction -from spinedb_api.helpers import vacuum +from spinedb_api.helpers import remove_credentials_from_url, vacuum from spine_engine.project_item.project_item_resource import database_resource, ProjectItemResource from spinetoolbox.project_item.project_item import ProjectItem from spinetoolbox.helpers import create_dir @@ -74,17 +71,21 @@ def __init__(self, name, description, x, y, toolbox, project, url): self._purge_settings = None self._purge_dialog = None self._database_validator = DatabaseConnectionValidator(self) + db_map = self.get_db_map_for_ds() + # Notify db manager about the Data Stores in the project so it can notify abobut the dirtyness of them + self._toolbox.db_mngr.add_data_store_db_map(db_map, self) + + def get_db_map_for_ds(self): + """Returns the db map for the Data Store""" + if self._url.get("dialect"): + return self._toolbox.db_mngr.get_db_map(self.sql_alchemy_url(), self._logger, codename=self.name) + return None @staticmethod def item_type(): """See base class.""" return ItemInfo.item_type() - @staticmethod - def item_category(): - """See base class.""" - return ItemInfo.item_category() - @property def executable_class(self): return ExecutableItem @@ -157,7 +158,7 @@ def _new_sqlite_file(self): url = dict(self._url) url["database"] = abs_path sa_url = convert_to_sqlalchemy_url(url, self.name) - self._toolbox.db_mngr.create_new_spine_database(sa_url, self._logger) + self._toolbox.db_mngr.create_new_spine_database(sa_url, self._logger, overwrite=True) self.update_url(dialect="sqlite", database=abs_path) return True @@ -188,7 +189,7 @@ def update_url(self, **kwargs): kwargs = {k: v for k, v in kwargs.items() if v != self._url[k]} if not kwargs: return False - self._toolbox.undo_stack.push(UpdateDSURLCommand(self, invalidating_url, **kwargs)) + self._toolbox.undo_stack.push(UpdateDSURLCommand(self.name, invalidating_url, self._project, **kwargs)) return True def do_update_url(self, **kwargs): @@ -214,6 +215,20 @@ def do_update_url(self, **kwargs): self._resources_to_successors_changed() self._check_notifications() + def has_listeners(self): + """Checks whether the Data Store has listeners or not + + Returns: + (bool): True if there are listeners for the Data Store, False otherwise + """ + if self._multi_db_editors_open: + return bool( + self._toolbox.db_mngr.db_map_listeners( + self._toolbox.db_mngr.get_db_map(self.sql_alchemy_url(), self._logger, codename=self.name) + ) + ) + return False + def _update_actions_enabled(self): url_exists = convert_to_sqlalchemy_url(self._url, self.name) is not None url_valid = url_exists and self._url_validated @@ -244,7 +259,7 @@ def _show_purge_dialog(self, _=False): def _purge(self): """Purges the database.""" self._purge_settings = self._purge_dialog.get_checked_states() - db_map = self._toolbox.db_mngr.get_db_map(self.sql_alchemy_url(), self._logger, self.name) + db_map = self._toolbox.db_mngr.get_db_map(self.sql_alchemy_url(), self._logger, codename=self.name) if db_map is None: return db_map_purge_data = {db_map: {item_type for item_type, checked in self._purge_settings.items() if checked}} @@ -280,18 +295,17 @@ def _handle_open_url_menu_triggered(self, action): @Slot(bool) def open_url_in_spine_db_editor(self, checked=False): """Opens current url in the Spine database editor.""" - if not self._url_validated: - self._logger.msg_error.emit( - f"{self.name} is still validating the database URL or the URL is invalid." - ) - return - sa_url = self.sql_alchemy_url() - if sa_url is not None: - db_url_codenames = {sa_url: self.name} - self._toolbox.db_mngr.open_db_editor(db_url_codenames) - self._check_notifications() + self._open_spine_db_editor(reuse_existing=True) def _open_url_in_new_db_editor(self, checked=False): + self._open_spine_db_editor(reuse_existing=False) + + def _open_spine_db_editor(self, reuse_existing): + """Opens Data Store's URL in Spine Database editor. + + Args: + reuse_existing (bool): if True and the URL is already open, just raise the window + """ if not self._url_validated: self._logger.msg_error.emit( f"{self.name} is still validating the database URL or the URL is invalid." @@ -299,7 +313,9 @@ def _open_url_in_new_db_editor(self, checked=False): return sa_url = self.sql_alchemy_url() if sa_url is not None: - MultiSpineDBEditor(self._toolbox.db_mngr, {sa_url: self.name}).show() + db_url_codenames = {sa_url: self.name} + self._toolbox.db_mngr.open_db_editor(db_url_codenames, reuse_existing) + self._check_notifications() def _open_url_in_existing_db_editor(self, db_editor): if not self._url_validated: @@ -344,6 +360,7 @@ def create_new_spine_database(self, checked=False): def _check_notifications(self): """Updates the SqlAlchemy format URL and checks for notifications""" + self.clear_notifications() self._update_actions_enabled() sa_url = convert_to_sqlalchemy_url(self._url, self.name) if sa_url is None: @@ -355,25 +372,45 @@ def _check_notifications(self): self._database_validator.validate_url( self._url["dialect"], sa_url, self._set_invalid_url_notification, self._accept_url ) + db_map = self.get_db_map_for_ds() + if db_map: + clean = not self._toolbox.db_mngr.is_dirty(db_map) + self.notify_about_dirtiness(clean) - @Slot(str) - def _set_invalid_url_notification(self, error_message): + @Slot(bool) + def notify_about_dirtiness(self, clean): + """ + Handles the notification for the dirtiness of the Data Store + + Args: + clean (bool): Whether the db_map corresponding to the DS is clean + """ + if not clean: + self.add_notification(f"{self.name} has uncommitted changes") + else: + self.remove_notification(f"{self.name} has uncommitted changes") + + @Slot(str, object) + def _set_invalid_url_notification(self, error_message, url): """Sets a single notification that warns about broken URL. Args: error_message (str): URL failure message + url (URL): SqlAlchemy URL """ self.clear_notifications() - self.add_notification(f"Couldn't connect to the database: {error_message}") + self.add_notification( + f"Couldn't connect to the database {remove_credentials_from_url(str(url))}: {error_message}" + ) if self._resource_to_replace is None: self._resources_to_predecessors_changed() self._resources_to_successors_changed() - @Slot() - def _accept_url(self): + @Slot(object) + def _accept_url(self, url): """Sets URL as validated and updates advertised resources.""" self._url_validated = True - self.clear_notifications() + self.clear_other_notifications(f"{self.name} has uncommitted changes") if self._resource_to_replace is not None and self._resource_to_replace.is_valid: old = self._resource_to_replace.resource sa_url = convert_to_sqlalchemy_url(self._url, self.name) @@ -385,6 +422,7 @@ def _accept_url(self): self._resources_to_predecessors_changed() self._resources_to_successors_changed() self._update_actions_enabled() + self._toolbox.db_mngr.update_data_store_db_maps() def is_url_validated(self): """Tests whether the URL has been validated. @@ -446,6 +484,8 @@ def from_dict(name, item_dict, toolbox, project): def rename(self, new_name, rename_data_dir_message): """See base class.""" old_data_dir = os.path.abspath(self.data_dir) # Old data_dir before rename + old_name = self.name + self.rename_data_store_in_db_mngr(old_name) # Notify db manager about the rename if not super().rename(new_name, rename_data_dir_message): return False # If dialect is sqlite and db line edit refers to a file in the old data_dir, db line edit needs updating @@ -494,7 +534,17 @@ def resources_for_direct_predecessors(self): """See base class.""" return self.resources_for_direct_successors() + def rename_data_store_in_db_mngr(self, old_name): + """Renames the Data Store in the used db manager""" + db_map = self.get_db_map_for_ds() + self._toolbox.db_mngr.update_data_store_db_maps() + index = next((i for i, store in enumerate(self._toolbox.db_mngr.data_stores[db_map]) if store.name == old_name)) + if index is not None: + self._toolbox.db_mngr.data_stores[db_map][index] = self + def tear_down(self): """See base class""" self._database_validator.wait_for_finish() + db_map = self.get_db_map_for_ds() + self._toolbox.db_mngr.remove_data_store_db_map(db_map, self) super().tear_down() diff --git a/spine_items/data_store/data_store_factory.py b/spine_items/data_store/data_store_factory.py index 7cc897f6..07d2a4ad 100644 --- a/spine_items/data_store/data_store_factory.py +++ b/spine_items/data_store/data_store_factory.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -The DataStoreFactory class. - -""" - +"""The DataStoreFactory class.""" from PySide6.QtGui import QColor from spinetoolbox.project_item.project_item_factory import ProjectItemFactory from .data_store import DataStore diff --git a/spine_items/data_store/data_store_icon.py b/spine_items/data_store/data_store_icon.py index 9ba6f51d..8104719d 100644 --- a/spine_items/data_store/data_store_icon.py +++ b/spine_items/data_store/data_store_icon.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Module for data store icon class. - -""" - +"""Module for data store icon class.""" from spinetoolbox.project_item_icon import ProjectItemIcon @@ -35,5 +32,5 @@ def mouseDoubleClickEvent(self, e): e (QGraphicsSceneMouseEvent): Event """ super().mouseDoubleClickEvent(e) - item = self._toolbox.project_item_model.get_item(self._name) - item.project_item.open_url_in_spine_db_editor() + item = self._toolbox.project().get_item(self._name) + item.open_url_in_spine_db_editor() diff --git a/spine_items/data_store/executable_item.py b/spine_items/data_store/executable_item.py index 342e02b5..693edc9c 100644 --- a/spine_items/data_store/executable_item.py +++ b/spine_items/data_store/executable_item.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,13 +10,10 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains Data Store's executable item as well as support utilities. - -""" +"""Contains Data Store's executable item as well as support utilities.""" from pathlib import Path from spinedb_api import DatabaseMapping -from spinedb_api.exception import SpineDBAPIError, SpineDBVersionError +from spinedb_api.exception import SpineDBAPIError from spine_engine.project_item.executable_item_base import ExecutableItemBase from spine_engine.utils.serialization import deserialize_path from .item_info import ItemInfo @@ -58,14 +56,15 @@ def _get_url(self): self._logger.msg_error.emit("SQLite file does not exist.") return None if not self._validated: - try: - DatabaseMapping.create_engine(self._url, create=True) - return self._url - except SpineDBVersionError as v_err: - prompt = {"type": "upgrade_db", "url": self._url, "current": v_err.current, "expected": v_err.expected} - if not self._logger.prompt.emit(prompt): + prompt_data = DatabaseMapping.get_upgrade_db_prompt_data(self._url, create=True) + if prompt_data is not None: + kwargs = self._logger.prompt.emit(prompt_data) + if kwargs is None: return None - DatabaseMapping.create_engine(self._url, upgrade=True) + else: + kwargs = {} + try: + DatabaseMapping.create_engine(self._url, create=True, **kwargs) return self._url except SpineDBAPIError as err: self._logger.msg_error.emit(str(err)) diff --git a/spine_items/data_store/item_info.py b/spine_items/data_store/item_info.py index c35bfae4..53258e0a 100644 --- a/spine_items/data_store/item_info.py +++ b/spine_items/data_store/item_info.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,19 +10,11 @@ # this program. If not, see . ###################################################################################################################### -""" -Data Store project item info. - -""" +"""Data Store project item info.""" from spine_engine.project_item.project_item_info import ProjectItemInfo class ItemInfo(ProjectItemInfo): - @staticmethod - def item_category(): - """See base class.""" - return "Data Stores" - @staticmethod def item_type(): """See base class.""" diff --git a/spine_items/data_store/output_resources.py b/spine_items/data_store/output_resources.py index 30ed936e..9705d459 100644 --- a/spine_items/data_store/output_resources.py +++ b/spine_items/data_store/output_resources.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 utilities to scan for Data Store's output resources. -""" +"""Contains utilities to scan for Data Store's output resources.""" from spine_engine.project_item.project_item_resource import database_resource from spine_items.utils import database_label diff --git a/spine_items/data_store/ui/__init__.py b/spine_items/data_store/ui/__init__.py index b448fd15..2636b0c5 100644 --- a/spine_items/data_store/ui/__init__.py +++ b/spine_items/data_store/ui/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/data_store/ui/data_store_properties.py b/spine_items/data_store/ui/data_store_properties.py index 946d9656..ebcd671f 100644 --- a/spine_items/data_store/ui/data_store_properties.py +++ b/spine_items/data_store/ui/data_store_properties.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/data_store/widgets/__init__.py b/spine_items/data_store/widgets/__init__.py index 8a671642..f12430e0 100644 --- a/spine_items/data_store/widgets/__init__.py +++ b/spine_items/data_store/widgets/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/data_store/widgets/add_data_store_widget.py b/spine_items/data_store/widgets/add_data_store_widget.py index 4291e0c2..60b2abe0 100644 --- a/spine_items/data_store/widgets/add_data_store_widget.py +++ b/spine_items/data_store/widgets/add_data_store_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Widget shown to user when a new Data Store is created. - -""" - +"""Widget shown to user when a new Data Store is created.""" from spinetoolbox.widgets.add_project_item_widget import AddProjectItemWidget from ..data_store import DataStore from ..item_info import ItemInfo diff --git a/spine_items/data_store/widgets/data_store_properties_widget.py b/spine_items/data_store/widgets/data_store_properties_widget.py index 9fa22c4c..68e0d612 100644 --- a/spine_items/data_store/widgets/data_store_properties_widget.py +++ b/spine_items/data_store/widgets/data_store_properties_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Data store properties widget. - -""" - +"""Data store properties widget.""" from spinetoolbox.widgets.properties_widget import PropertiesWidgetBase from spinedb_api import SUPPORTED_DIALECTS from ...widgets import UrlSelectorMixin diff --git a/spine_items/data_transformer/__init__.py b/spine_items/data_transformer/__init__.py index ccec3891..9ee74808 100644 --- a/spine_items/data_transformer/__init__.py +++ b/spine_items/data_transformer/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Data transformer plugin. - -""" +"""Data transformer plugin.""" diff --git a/spine_items/data_transformer/commands.py b/spine_items/data_transformer/commands.py index 1865679f..6122a4b7 100644 --- a/spine_items/data_transformer/commands.py +++ b/spine_items/data_transformer/commands.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 Data transformer's undo commands. -""" +"""Contains Data transformer's undo commands.""" from PySide6.QtGui import QUndoCommand diff --git a/spine_items/data_transformer/data_transformer.py b/spine_items/data_transformer/data_transformer.py index 02e9f9f7..01c5b7ee 100644 --- a/spine_items/data_transformer/data_transformer.py +++ b/spine_items/data_transformer/data_transformer.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains the :class:`DataTransformer` project item. - -""" +"""Contains the :class:`DataTransformer` project item.""" from json import dump from PySide6.QtCore import Slot from spinetoolbox.project_item.project_item import ProjectItem @@ -52,11 +50,6 @@ def item_type(): """See base class.""" return ItemInfo.item_type() - @staticmethod - def item_category(): - """See base class.""" - return ItemInfo.item_category() - @property def executable_class(self): return ExecutableItem diff --git a/spine_items/data_transformer/data_transformer_factory.py b/spine_items/data_transformer/data_transformer_factory.py index b4a069c9..f586982a 100644 --- a/spine_items/data_transformer/data_transformer_factory.py +++ b/spine_items/data_transformer/data_transformer_factory.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains the :class:`DataTransformerFactory` class. - -""" - +"""Contains the :class:`DataTransformerFactory` class.""" from PySide6.QtGui import QColor from spinetoolbox.project_item.project_item_factory import ProjectItemFactory from spinetoolbox.widgets.custom_menus import ItemSpecificationMenu diff --git a/spine_items/data_transformer/data_transformer_icon.py b/spine_items/data_transformer/data_transformer_icon.py index a2db920b..300b9e5f 100644 --- a/spine_items/data_transformer/data_transformer_icon.py +++ b/spine_items/data_transformer/data_transformer_icon.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains :class:`DataTransformerIcon`. - -""" - +"""Contains :class:`DataTransformerIcon`.""" from spinetoolbox.project_item_icon import ProjectItemIcon @@ -36,5 +33,5 @@ def mouseDoubleClickEvent(self, e): e (QGraphicsSceneMouseEvent): Event """ super().mouseDoubleClickEvent(e) - item = self._toolbox.project_item_model.get_item(self._name) - item.project_item.show_specification_window() + item = self._toolbox.project().get_item(self._name) + item.show_specification_window() diff --git a/spine_items/data_transformer/data_transformer_specification.py b/spine_items/data_transformer/data_transformer_specification.py index 5c27b2ef..2d738fd9 100644 --- a/spine_items/data_transformer/data_transformer_specification.py +++ b/spine_items/data_transformer/data_transformer_specification.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains Data transformer's specification. - -""" - +"""Contains Data transformer's specification.""" from spine_engine.project_item.project_item_specification import ProjectItemSpecification from .item_info import ItemInfo from .settings import EntityClassRenamingSettings, settings_from_dict @@ -34,7 +31,7 @@ def __init__(self, name, settings=None, description=None): settings (FilterSettings, optional): filter settings description (str, optional): specification's description """ - super().__init__(name, description, ItemInfo.item_type(), ItemInfo.item_category()) + super().__init__(name, description, ItemInfo.item_type()) self.settings = settings def is_equivalent(self, other): diff --git a/spine_items/data_transformer/executable_item.py b/spine_items/data_transformer/executable_item.py index 6e29e064..23d66e15 100644 --- a/spine_items/data_transformer/executable_item.py +++ b/spine_items/data_transformer/executable_item.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains Data transformer's executable item as well as support utilities. - -""" +"""Contains Data transformer's executable item as well as support utilities.""" from spine_engine.project_item.executable_item_base import ExecutableItemBase from spine_engine.spine_engine import ItemExecutionFinishState from .filter_config_path import filter_config_path diff --git a/spine_items/data_transformer/filter_config_path.py b/spine_items/data_transformer/filter_config_path.py index 5ccf3abb..a21470ae 100644 --- a/spine_items/data_transformer/filter_config_path.py +++ b/spine_items/data_transformer/filter_config_path.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains utilities for filter config paths. - -""" +"""Contains utilities for filter config paths.""" from pathlib import Path diff --git a/spine_items/data_transformer/item_info.py b/spine_items/data_transformer/item_info.py index 1acdf33f..e5129957 100644 --- a/spine_items/data_transformer/item_info.py +++ b/spine_items/data_transformer/item_info.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,19 +10,11 @@ # this program. If not, see . ###################################################################################################################### -""" -Data transformer project item info. - -""" +"""Data transformer project item info.""" from spine_engine.project_item.project_item_info import ProjectItemInfo class ItemInfo(ProjectItemInfo): - @staticmethod - def item_category(): - """See base class.""" - return "Manipulators" - @staticmethod def item_type(): """See base class.""" diff --git a/spine_items/data_transformer/mvcmodels/__init__.py b/spine_items/data_transformer/mvcmodels/__init__.py index 8095b663..046209e7 100644 --- a/spine_items/data_transformer/mvcmodels/__init__.py +++ b/spine_items/data_transformer/mvcmodels/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/data_transformer/mvcmodels/class_renames_table_model.py b/spine_items/data_transformer/mvcmodels/class_renames_table_model.py index 94300f44..71db5641 100644 --- a/spine_items/data_transformer/mvcmodels/class_renames_table_model.py +++ b/spine_items/data_transformer/mvcmodels/class_renames_table_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,13 +10,9 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains the :class:`ClassRenamesTableModel` class. - -""" +"""Contains the :class:`ClassRenamesTableModel` class.""" from enum import IntEnum, unique import pickle - from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt from ..commands import InsertRow, SetData from ..widgets.drop_target_table import DROP_MIME_TYPE diff --git a/spine_items/data_transformer/mvcmodels/parameter_drop_target_table_model.py b/spine_items/data_transformer/mvcmodels/parameter_drop_target_table_model.py index 9c926cf2..e5ca439f 100644 --- a/spine_items/data_transformer/mvcmodels/parameter_drop_target_table_model.py +++ b/spine_items/data_transformer/mvcmodels/parameter_drop_target_table_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 a table model that can be used as drop target. -""" +"""Contains a table model that can be used as drop target.""" import pickle from PySide6.QtCore import QAbstractTableModel from ..commands import InsertRow diff --git a/spine_items/data_transformer/mvcmodels/parameter_renames_table_model.py b/spine_items/data_transformer/mvcmodels/parameter_renames_table_model.py index 6f99c095..f406ef78 100644 --- a/spine_items/data_transformer/mvcmodels/parameter_renames_table_model.py +++ b/spine_items/data_transformer/mvcmodels/parameter_renames_table_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 :class:`ParameterRenamesTableModel`. -""" +"""Contains :class:`ParameterRenamesTableModel`.""" from enum import IntEnum, unique from PySide6.QtCore import QModelIndex, Qt from .parameter_drop_target_table_model import ParameterDropTargetTableModel diff --git a/spine_items/data_transformer/mvcmodels/value_transformations_table_model.py b/spine_items/data_transformer/mvcmodels/value_transformations_table_model.py index de4726af..57baf503 100644 --- a/spine_items/data_transformer/mvcmodels/value_transformations_table_model.py +++ b/spine_items/data_transformer/mvcmodels/value_transformations_table_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 :class:`ValueTransformTableModel`. -""" +"""Contains :class:`ValueTransformTableModel`.""" from enum import IntEnum, unique from PySide6.QtCore import QModelIndex, Qt from .parameter_drop_target_table_model import ParameterDropTargetTableModel diff --git a/spine_items/data_transformer/output_resources.py b/spine_items/data_transformer/output_resources.py index 2997ebbb..05146ea2 100644 --- a/spine_items/data_transformer/output_resources.py +++ b/spine_items/data_transformer/output_resources.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,11 +9,8 @@ # 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 utilities to scan for Data Transformer's output resources. - -""" +"""Contains utilities to scan for Data Transformer's output resources.""" from spine_engine.project_item.project_item_resource import database_resource from spinedb_api import append_filter_config from spinedb_api.filters.tools import store_filter diff --git a/spine_items/data_transformer/settings.py b/spine_items/data_transformer/settings.py index e68a0947..2cc24adc 100644 --- a/spine_items/data_transformer/settings.py +++ b/spine_items/data_transformer/settings.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains settings classes for filters and manipulators. - -""" +"""Contains settings classes for filters and manipulators.""" from spinedb_api.filters.renamer import entity_class_renamer_config, parameter_renamer_config from spinedb_api.filters.value_transformer import value_transformer_config diff --git a/spine_items/data_transformer/specification_factory.py b/spine_items/data_transformer/specification_factory.py index 97cda2d1..a3527afd 100644 --- a/spine_items/data_transformer/specification_factory.py +++ b/spine_items/data_transformer/specification_factory.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Data transformer's specification factory. - -""" +"""Data transformer's specification factory.""" from spine_engine.project_item.project_item_specification_factory import ProjectItemSpecificationFactory from .item_info import ItemInfo from .data_transformer_specification import DataTransformerSpecification diff --git a/spine_items/data_transformer/ui/__init__.py b/spine_items/data_transformer/ui/__init__.py index 8095b663..046209e7 100644 --- a/spine_items/data_transformer/ui/__init__.py +++ b/spine_items/data_transformer/ui/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/data_transformer/ui/class_renamer_editor.py b/spine_items/data_transformer/ui/class_renamer_editor.py deleted file mode 100644 index bf2ad0ac..00000000 --- a/spine_items/data_transformer/ui/class_renamer_editor.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -###################################################################################################################### -# Copyright (C) 2017-2022 Spine project consortium -# This file is part of Spine Items. -# Spine Items 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 . -###################################################################################################################### - -################################################################################ -## Form generated from reading UI file 'class_renamer_editor.ui' -## -## Created by: Qt User Interface Compiler version 6.5.2 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, - QCursor, QFont, QFontDatabase, QGradient, - QIcon, QImage, QKeySequence, QLinearGradient, - QPainter, QPalette, QPixmap, QRadialGradient, - QTransform) -from PySide6.QtWidgets import (QAbstractItemView, QApplication, QHBoxLayout, QHeaderView, - QPushButton, QSizePolicy, QSpacerItem, QSplitter, - QTreeWidgetItem, QVBoxLayout, QWidget) - -from spine_items.data_transformer.widgets.class_tree_widget import ClassTreeWidget -from spine_items.data_transformer.widgets.drop_target_table import DropTargetTable -from spine_items import resources_icons_rc - -class Ui_Form(object): - def setupUi(self, Form): - if not Form.objectName(): - Form.setObjectName(u"Form") - Form.resize(619, 368) - self.remove_class_action = QAction(Form) - self.remove_class_action.setObjectName(u"remove_class_action") - self.remove_class_action.setShortcutContext(Qt.WidgetShortcut) - self.verticalLayout_2 = QVBoxLayout(Form) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.splitter = QSplitter(Form) - self.splitter.setObjectName(u"splitter") - self.splitter.setOrientation(Qt.Horizontal) - self.splitter.setChildrenCollapsible(False) - self.available_classes_tree_widget = ClassTreeWidget(self.splitter) - __qtreewidgetitem = QTreeWidgetItem() - __qtreewidgetitem.setText(0, u"1"); - self.available_classes_tree_widget.setHeaderItem(__qtreewidgetitem) - self.available_classes_tree_widget.setObjectName(u"available_classes_tree_widget") - self.available_classes_tree_widget.setDragEnabled(True) - self.available_classes_tree_widget.setDragDropMode(QAbstractItemView.DragOnly) - self.available_classes_tree_widget.setDefaultDropAction(Qt.CopyAction) - self.available_classes_tree_widget.setSelectionMode(QAbstractItemView.ExtendedSelection) - self.splitter.addWidget(self.available_classes_tree_widget) - self.available_classes_tree_widget.header().setVisible(False) - self.verticalLayoutWidget = QWidget(self.splitter) - self.verticalLayoutWidget.setObjectName(u"verticalLayoutWidget") - self.verticalLayout = QVBoxLayout(self.verticalLayoutWidget) - self.verticalLayout.setObjectName(u"verticalLayout") - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.add_class_button = QPushButton(self.verticalLayoutWidget) - self.add_class_button.setObjectName(u"add_class_button") - - self.horizontalLayout.addWidget(self.add_class_button) - - self.remove_class_button = QPushButton(self.verticalLayoutWidget) - self.remove_class_button.setObjectName(u"remove_class_button") - - self.horizontalLayout.addWidget(self.remove_class_button) - - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) - - self.horizontalLayout.addItem(self.horizontalSpacer) - - - self.verticalLayout.addLayout(self.horizontalLayout) - - self.renaming_table_view = DropTargetTable(self.verticalLayoutWidget) - self.renaming_table_view.setObjectName(u"renaming_table_view") - self.renaming_table_view.setAcceptDrops(True) - self.renaming_table_view.setDragDropMode(QAbstractItemView.DropOnly) - - self.verticalLayout.addWidget(self.renaming_table_view) - - self.splitter.addWidget(self.verticalLayoutWidget) - - self.verticalLayout_2.addWidget(self.splitter) - - - self.retranslateUi(Form) - - QMetaObject.connectSlotsByName(Form) - # setupUi - - def retranslateUi(self, Form): - Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) - self.remove_class_action.setText(QCoreApplication.translate("Form", u"Remove class", None)) -#if QT_CONFIG(shortcut) - self.remove_class_action.setShortcut(QCoreApplication.translate("Form", u"Del", None)) -#endif // QT_CONFIG(shortcut) - self.add_class_button.setText(QCoreApplication.translate("Form", u"Add", None)) - self.remove_class_button.setText(QCoreApplication.translate("Form", u"Remove", None)) - # retranslateUi - diff --git a/spine_items/data_transformer/ui/class_renamer_editor.ui b/spine_items/data_transformer/ui/class_renamer_editor.ui deleted file mode 100644 index d286c662..00000000 --- a/spine_items/data_transformer/ui/class_renamer_editor.ui +++ /dev/null @@ -1,135 +0,0 @@ - - - - Form - - - - 0 - 0 - 619 - 368 - - - - Form - - - - - - Qt::Horizontal - - - false - - - - true - - - QAbstractItemView::DragOnly - - - Qt::CopyAction - - - QAbstractItemView::ExtendedSelection - - - false - - - - 1 - - - - - - - - - - - Add - - - - - - - Remove - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - true - - - QAbstractItemView::DropOnly - - - - - - - - - - - Remove class - - - Del - - - Qt::WidgetShortcut - - - - - - DropTargetTable - QTableView -
spine_items/data_transformer/widgets/drop_target_table.h
-
- - ClassTreeWidget - QTreeWidget -
spine_items/data_transformer/widgets/class_tree_widget.h
-
-
- - - - -
diff --git a/spine_items/data_transformer/ui/data_transformer_properties.py b/spine_items/data_transformer/ui/data_transformer_properties.py index 96f4d075..fc9ce728 100644 --- a/spine_items/data_transformer/ui/data_transformer_properties.py +++ b/spine_items/data_transformer/ui/data_transformer_properties.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -85,7 +86,7 @@ def setupUi(self, Form): def retranslateUi(self, Form): Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) - self.specification_label.setText(QCoreApplication.translate("Form", u"Specification", None)) + self.specification_label.setText(QCoreApplication.translate("Form", u"Specification:", None)) #if QT_CONFIG(tooltip) self.specification_combo_box.setToolTip(QCoreApplication.translate("Form", u"

Tool specification for this Tool

", None)) #endif // QT_CONFIG(tooltip) diff --git a/spine_items/data_transformer/ui/data_transformer_properties.ui b/spine_items/data_transformer/ui/data_transformer_properties.ui index 7ef1222f..d98c12d6 100644 --- a/spine_items/data_transformer/ui/data_transformer_properties.ui +++ b/spine_items/data_transformer/ui/data_transformer_properties.ui @@ -2,6 +2,7 @@ - - Form - - - - 0 - 0 - 616 - 530 - - - - Form - - - - - - Qt::Horizontal - - - false - - - - true - - - QAbstractItemView::DragOnly - - - Qt::CopyAction - - - false - - - - 1 - - - - - - - - - - - Add - - - - - - - Remove - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - QAbstractItemView::DropOnly - - - - - - - - - - - Remove parameter - - - Del - - - Qt::WidgetShortcut - - - - - - ParameterTreeWidget - QTreeWidget -
./widgets/parameter_tree_widget.h
-
- - DropTargetTable - QTableView -
spine_items/data_transformer/widgets/drop_target_table.h
-
-
- - -
diff --git a/spine_items/data_transformer/ui/specification_editor_widget.py b/spine_items/data_transformer/ui/specification_editor_widget.py index 4aa25c2e..ca6839d9 100644 --- a/spine_items/data_transformer/ui/specification_editor_widget.py +++ b/spine_items/data_transformer/ui/specification_editor_widget.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/data_transformer/ui/value_transformer_editor.py b/spine_items/data_transformer/ui/value_transformer_editor.py deleted file mode 100644 index 14f86a51..00000000 --- a/spine_items/data_transformer/ui/value_transformer_editor.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- -###################################################################################################################### -# Copyright (C) 2017-2022 Spine project consortium -# This file is part of Spine Items. -# Spine Items 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 . -###################################################################################################################### - -################################################################################ -## Form generated from reading UI file 'value_transformer_editor.ui' -## -## Created by: Qt User Interface Compiler version 6.5.2 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, - QCursor, QFont, QFontDatabase, QGradient, - QIcon, QImage, QKeySequence, QLinearGradient, - QPainter, QPalette, QPixmap, QRadialGradient, - QTransform) -from PySide6.QtWidgets import (QAbstractItemView, QApplication, QComboBox, QFormLayout, - QHBoxLayout, QHeaderView, QLabel, QListWidget, - QListWidgetItem, QPushButton, QSizePolicy, QSpacerItem, - QSplitter, QTreeWidgetItem, QVBoxLayout, QWidget) - -from ..widgets.parameter_tree_widget import ParameterTreeWidget -from spine_items.data_transformer.widgets.drop_target_table import DropTargetTable - -class Ui_Form(object): - def setupUi(self, Form): - if not Form.objectName(): - Form.setObjectName(u"Form") - Form.resize(938, 368) - self.remove_parameter_action = QAction(Form) - self.remove_parameter_action.setObjectName(u"remove_parameter_action") - self.remove_parameter_action.setShortcutContext(Qt.WidgetShortcut) - self.remove_instruction_action = QAction(Form) - self.remove_instruction_action.setObjectName(u"remove_instruction_action") - self.remove_instruction_action.setShortcutContext(Qt.WidgetShortcut) - self.horizontalLayout = QHBoxLayout(Form) - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.splitter = QSplitter(Form) - self.splitter.setObjectName(u"splitter") - self.splitter.setOrientation(Qt.Horizontal) - self.splitter.setChildrenCollapsible(False) - self.available_parameters_tree_view = ParameterTreeWidget(self.splitter) - __qtreewidgetitem = QTreeWidgetItem() - __qtreewidgetitem.setText(0, u"1"); - self.available_parameters_tree_view.setHeaderItem(__qtreewidgetitem) - self.available_parameters_tree_view.setObjectName(u"available_parameters_tree_view") - self.available_parameters_tree_view.setDragEnabled(True) - self.available_parameters_tree_view.setDragDropMode(QAbstractItemView.DragOnly) - self.available_parameters_tree_view.setDefaultDropAction(Qt.CopyAction) - self.splitter.addWidget(self.available_parameters_tree_view) - self.available_parameters_tree_view.header().setVisible(False) - self.verticalLayoutWidget_2 = QWidget(self.splitter) - self.verticalLayoutWidget_2.setObjectName(u"verticalLayoutWidget_2") - self.verticalLayout_2 = QVBoxLayout(self.verticalLayoutWidget_2) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_2 = QHBoxLayout() - self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") - self.add_parameter_button = QPushButton(self.verticalLayoutWidget_2) - self.add_parameter_button.setObjectName(u"add_parameter_button") - - self.horizontalLayout_2.addWidget(self.add_parameter_button) - - self.remove_parameter_button = QPushButton(self.verticalLayoutWidget_2) - self.remove_parameter_button.setObjectName(u"remove_parameter_button") - - self.horizontalLayout_2.addWidget(self.remove_parameter_button) - - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) - - self.horizontalLayout_2.addItem(self.horizontalSpacer) - - - self.verticalLayout_2.addLayout(self.horizontalLayout_2) - - self.transformations_table_view = DropTargetTable(self.verticalLayoutWidget_2) - self.transformations_table_view.setObjectName(u"transformations_table_view") - self.transformations_table_view.setAcceptDrops(True) - self.transformations_table_view.setDragDropMode(QAbstractItemView.DropOnly) - self.transformations_table_view.setSelectionBehavior(QAbstractItemView.SelectRows) - self.transformations_table_view.setShowGrid(False) - self.transformations_table_view.horizontalHeader().setStretchLastSection(True) - - self.verticalLayout_2.addWidget(self.transformations_table_view) - - self.splitter.addWidget(self.verticalLayoutWidget_2) - - self.horizontalLayout.addWidget(self.splitter) - - self.verticalLayout_3 = QVBoxLayout() - self.verticalLayout_3.setObjectName(u"verticalLayout_3") - self.horizontalLayout_3 = QHBoxLayout() - self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") - self.add_instruction_button = QPushButton(Form) - self.add_instruction_button.setObjectName(u"add_instruction_button") - self.add_instruction_button.setEnabled(False) - - self.horizontalLayout_3.addWidget(self.add_instruction_button) - - self.remove_instruction_button = QPushButton(Form) - self.remove_instruction_button.setObjectName(u"remove_instruction_button") - self.remove_instruction_button.setEnabled(False) - - self.horizontalLayout_3.addWidget(self.remove_instruction_button) - - self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) - - self.horizontalLayout_3.addItem(self.horizontalSpacer_2) - - - self.verticalLayout_3.addLayout(self.horizontalLayout_3) - - self.instructions_list_view = QListWidget(Form) - self.instructions_list_view.setObjectName(u"instructions_list_view") - self.instructions_list_view.setEnabled(False) - - self.verticalLayout_3.addWidget(self.instructions_list_view) - - self.instruction_options_layout = QFormLayout() - self.instruction_options_layout.setObjectName(u"instruction_options_layout") - self.label = QLabel(Form) - self.label.setObjectName(u"label") - - self.instruction_options_layout.setWidget(0, QFormLayout.LabelRole, self.label) - - self.operation_combo_box = QComboBox(Form) - self.operation_combo_box.addItem("") - self.operation_combo_box.addItem("") - self.operation_combo_box.addItem("") - self.operation_combo_box.setObjectName(u"operation_combo_box") - - self.instruction_options_layout.setWidget(0, QFormLayout.FieldRole, self.operation_combo_box) - - - self.verticalLayout_3.addLayout(self.instruction_options_layout) - - - self.horizontalLayout.addLayout(self.verticalLayout_3) - - - self.retranslateUi(Form) - - QMetaObject.connectSlotsByName(Form) - # setupUi - - def retranslateUi(self, Form): - Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) - self.remove_parameter_action.setText(QCoreApplication.translate("Form", u"Remove parameter", None)) -#if QT_CONFIG(shortcut) - self.remove_parameter_action.setShortcut(QCoreApplication.translate("Form", u"Del", None)) -#endif // QT_CONFIG(shortcut) - self.remove_instruction_action.setText(QCoreApplication.translate("Form", u"Remove instruction", None)) -#if QT_CONFIG(shortcut) - self.remove_instruction_action.setShortcut(QCoreApplication.translate("Form", u"Del", None)) -#endif // QT_CONFIG(shortcut) - self.add_parameter_button.setText(QCoreApplication.translate("Form", u"Add", None)) - self.remove_parameter_button.setText(QCoreApplication.translate("Form", u"Remove", None)) - self.add_instruction_button.setText(QCoreApplication.translate("Form", u"Add", None)) - self.remove_instruction_button.setText(QCoreApplication.translate("Form", u"Remove", None)) - self.label.setText(QCoreApplication.translate("Form", u"Operation:", None)) - self.operation_combo_box.setItemText(0, QCoreApplication.translate("Form", u"multiply", None)) - self.operation_combo_box.setItemText(1, QCoreApplication.translate("Form", u"negate", None)) - self.operation_combo_box.setItemText(2, QCoreApplication.translate("Form", u"invert", None)) - - # retranslateUi - diff --git a/spine_items/data_transformer/ui/value_transformer_editor.ui b/spine_items/data_transformer/ui/value_transformer_editor.ui deleted file mode 100644 index 371fa11d..00000000 --- a/spine_items/data_transformer/ui/value_transformer_editor.ui +++ /dev/null @@ -1,228 +0,0 @@ - - - - Form - - - - 0 - 0 - 938 - 368 - - - - Form - - - - - - Qt::Horizontal - - - false - - - - true - - - QAbstractItemView::DragOnly - - - Qt::CopyAction - - - false - - - - 1 - - - - - - - - - - - Add - - - - - - - Remove - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - true - - - QAbstractItemView::DropOnly - - - QAbstractItemView::SelectRows - - - false - - - true - - - - - - - - - - - - - - - false - - - Add - - - - - - - false - - - Remove - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - false - - - - - - - - - Operation: - - - - - - - - multiply - - - - - negate - - - - - invert - - - - - - - - - - - - Remove parameter - - - Del - - - Qt::WidgetShortcut - - - - - Remove instruction - - - Del - - - Qt::WidgetShortcut - - - - - - ParameterTreeWidget - QTreeWidget -
./widgets/parameter_tree_widget.h
-
- - DropTargetTable - QTableView -
spine_items/data_transformer/widgets/drop_target_table.h
-
-
- - -
diff --git a/spine_items/data_transformer/widgets/__init__.py b/spine_items/data_transformer/widgets/__init__.py index 8095b663..046209e7 100644 --- a/spine_items/data_transformer/widgets/__init__.py +++ b/spine_items/data_transformer/widgets/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/data_transformer/widgets/add_data_transformer_widget.py b/spine_items/data_transformer/widgets/add_data_transformer_widget.py index a5f081de..7e3fad9c 100644 --- a/spine_items/data_transformer/widgets/add_data_transformer_widget.py +++ b/spine_items/data_transformer/widgets/add_data_transformer_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Widget shown to user when a new Data transformer is created. - -""" - +"""Widget shown to user when a new Data transformer is created.""" from spinetoolbox.widgets.add_project_item_widget import AddProjectItemWidget from ..item_info import ItemInfo from ..data_transformer import DataTransformer diff --git a/spine_items/data_transformer/widgets/class_rename.py b/spine_items/data_transformer/widgets/class_rename.py index f637808d..f7d4d8f9 100644 --- a/spine_items/data_transformer/widgets/class_rename.py +++ b/spine_items/data_transformer/widgets/class_rename.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,12 +9,9 @@ # 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 classes to manage entity class renaming. -""" +"""Contains classes to manage entity class renaming.""" from PySide6.QtCore import QObject, Qt, Slot, QSortFilterProxyModel - from ..commands import RemoveRow, InsertRow from ..mvcmodels.class_renames_table_model import ClassRenamesTableModel from ..settings import EntityClassRenamingSettings diff --git a/spine_items/data_transformer/widgets/class_tree_widget.py b/spine_items/data_transformer/widgets/class_tree_widget.py index 523937df..b36addad 100644 --- a/spine_items/data_transformer/widgets/class_tree_widget.py +++ b/spine_items/data_transformer/widgets/class_tree_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,14 +9,11 @@ # 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 :class:`ClassTreeWidget`. -""" +"""Contains :class:`ClassTreeWidget`.""" import pickle from PySide6.QtCore import QMimeData from PySide6.QtWidgets import QTreeWidget, QMessageBox, QTreeWidgetItem - from spinedb_api import DatabaseMapping, SpineDBAPIError from .drop_target_table import DROP_MIME_TYPE @@ -50,7 +48,7 @@ def load_data(self, url): self, "Error while reading database", f"Could not read from database {url}:\n{error}" ) finally: - db_map.connection.close() + db_map.close() self.clear() for class_name in classes: self.addTopLevelItem(QTreeWidgetItem([class_name])) diff --git a/spine_items/data_transformer/widgets/copy_paste.py b/spine_items/data_transformer/widgets/copy_paste.py index a0fee2c1..2c1b2ac6 100644 --- a/spine_items/data_transformer/widgets/copy_paste.py +++ b/spine_items/data_transformer/widgets/copy_paste.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 shared functions that provide copy-paste functionality. -""" +"""Contains shared functions that provide copy-paste functionality.""" import pickle from PySide6.QtCore import QMimeData from PySide6.QtWidgets import QApplication diff --git a/spine_items/data_transformer/widgets/data_transformer_properties_widget.py b/spine_items/data_transformer/widgets/data_transformer_properties_widget.py index 0c1e42ee..0e208392 100644 --- a/spine_items/data_transformer/widgets/data_transformer_properties_widget.py +++ b/spine_items/data_transformer/widgets/data_transformer_properties_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Data transformer properties widget. - -""" +"""Data transformer properties widget.""" from spinetoolbox.widgets.properties_widget import PropertiesWidgetBase from ..item_info import ItemInfo diff --git a/spine_items/data_transformer/widgets/drop_target_table.py b/spine_items/data_transformer/widgets/drop_target_table.py index 29c7385b..cfc53eab 100644 --- a/spine_items/data_transformer/widgets/drop_target_table.py +++ b/spine_items/data_transformer/widgets/drop_target_table.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 a table view that can accept drops. -""" +"""Contains a table view that can accept drops.""" from PySide6.QtCore import Qt from PySide6.QtWidgets import QTableView diff --git a/spine_items/data_transformer/widgets/instructions_editor.py b/spine_items/data_transformer/widgets/instructions_editor.py index 75971ef5..3e908870 100644 --- a/spine_items/data_transformer/widgets/instructions_editor.py +++ b/spine_items/data_transformer/widgets/instructions_editor.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 controller that manages value transformations editor. -""" +"""Contains controller that manages value transformations editor.""" from PySide6.QtCore import QModelIndex, QObject, Slot from PySide6.QtWidgets import QLineEdit, QFormLayout from ..commands import AppendInstruction, ChangeInstructionParameter, ChangeOperation, RemoveInstruction diff --git a/spine_items/data_transformer/widgets/parameter_rename.py b/spine_items/data_transformer/widgets/parameter_rename.py index 1c507f8f..93fad56a 100644 --- a/spine_items/data_transformer/widgets/parameter_rename.py +++ b/spine_items/data_transformer/widgets/parameter_rename.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,12 +9,9 @@ # 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 classes to manage parameter renaming. -""" +"""Contains classes to manage parameter renaming.""" from PySide6.QtCore import QObject, QSortFilterProxyModel, Qt, Slot - from ..commands import InsertRow, RemoveRow from ..mvcmodels.parameter_renames_table_model import ParameterRenamesTableModel, RenamesTableColumn from ..settings import ParameterRenamingSettings diff --git a/spine_items/data_transformer/widgets/parameter_tree_widget.py b/spine_items/data_transformer/widgets/parameter_tree_widget.py index 9f7a4d23..45615de6 100644 --- a/spine_items/data_transformer/widgets/parameter_tree_widget.py +++ b/spine_items/data_transformer/widgets/parameter_tree_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,14 +9,11 @@ # 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 :class:`ParameterTreeWidget`. -""" +"""Contains :class:`ParameterTreeWidget`.""" import pickle from PySide6.QtCore import QMimeData from PySide6.QtWidgets import QTreeWidget, QMessageBox, QTreeWidgetItem - from spinedb_api import DatabaseMapping, SpineDBAPIError from .drop_target_table import DROP_MIME_TYPE @@ -56,7 +54,7 @@ def load_data(self, url): self, "Error while reading database", f"Could not read from database {url}:\n{error}" ) finally: - db_map.connection.close() + db_map.close() self.clear() for class_name, parameter_names in parameters.items(): class_item = QTreeWidgetItem([class_name]) diff --git a/spine_items/data_transformer/widgets/specification_editor_window.py b/spine_items/data_transformer/widgets/specification_editor_window.py index 0226fe97..c223fe4a 100644 --- a/spine_items/data_transformer/widgets/specification_editor_window.py +++ b/spine_items/data_transformer/widgets/specification_editor_window.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,12 +9,12 @@ # 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 :class:`SpecificationEditorWindow`. -""" +"""Contains :class:`SpecificationEditorWindow`.""" from PySide6.QtCore import Qt, Slot -from PySide6.QtWidgets import QFileDialog +from PySide6.QtWidgets import QFileDialog, QHeaderView + +from spinetoolbox.helpers import disconnect from spinetoolbox.project_item.specification_editor_window import ( SpecificationEditorWindowBase, ChangeSpecPropertyCommand, @@ -77,6 +78,17 @@ def __init__(self, toolbox, specification=None, item=None, urls=None): self._ui.database_url_combo_box.addItems(urls if urls is not None else []) self._ui.filter_combo_box.currentTextChanged.connect(self._change_filter_widget) self._set_current_filter_name(filter_name) + for view in ( + self._ui.available_parameters_tree_view, + self._ui.available_classes_tree_widget, + ): + view.header().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + for view in ( + self._ui.parameter_rename_table_view, + self._ui.transformations_table_view, + self._ui.class_rename_table_view, + ): + view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) @property def settings_group(self): @@ -87,7 +99,7 @@ def _make_ui(self): return Ui_MainWindow() - def _make_new_specification(self, spec_name, exiting=None): + def _make_new_specification(self, spec_name): """See base class.""" description = self._spec_toolbar.description() filter_name = self._ui.filter_combo_box.currentText() @@ -124,9 +136,8 @@ def _set_current_filter_name(self, filter_name): if previous_interface is not None: previous_interface.tear_down() self._current_filter_name = filter_name - self._ui.filter_combo_box.currentTextChanged.disconnect(self._change_filter_widget) - self._ui.filter_combo_box.setCurrentText(filter_name) - self._ui.filter_combo_box.currentTextChanged.connect(self._change_filter_widget) + with disconnect(self._ui.filter_combo_box.currentTextChanged, self._change_filter_widget): + self._ui.filter_combo_box.setCurrentText(filter_name) interface = self._filter_sub_interfaces.get(filter_name) if interface is None: interface = dict(zip(_FILTER_NAMES, (ClassRename, ParameterRename, ValueTransformation)))[filter_name]( diff --git a/spine_items/data_transformer/widgets/value_transformation.py b/spine_items/data_transformer/widgets/value_transformation.py index 54364cb8..baebb6a9 100644 --- a/spine_items/data_transformer/widgets/value_transformation.py +++ b/spine_items/data_transformer/widgets/value_transformation.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,12 +9,9 @@ # 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 classes to manage parameter value transformation. -""" +"""Contains classes to manage parameter value transformation.""" from PySide6.QtCore import QObject, QSortFilterProxyModel, Qt, Slot - from ..commands import InsertRow, RemoveRow from ..mvcmodels.value_transformations_table_model import ( ValueTransformationsTableModel, diff --git a/spine_items/database_validation.py b/spine_items/database_validation.py index 6eb55129..9d3e5d46 100644 --- a/spine_items/database_validation.py +++ b/spine_items/database_validation.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,13 +9,11 @@ # 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 . ###################################################################################################################### + """Utilities to validate that a database exists.""" from pathlib import Path -from sqlalchemy.engine.url import make_url - from PySide6.QtCore import QObject, QRunnable, QThread, QThreadPool, QTimer, Signal, Slot from PySide6.QtWidgets import QApplication - from spine_items.utils import check_database_url @@ -32,7 +31,7 @@ def __init__(self, dialect, sa_url, finish_slot, fail_slot, success_slot): """ super().__init__() self._dialect = dialect - self._sa_url = make_url(sa_url) + self._sa_url = sa_url self._signals = _TaskSignals() self._signals.moveToThread(None) self._signals.validation_failed.connect(fail_slot) @@ -47,20 +46,21 @@ def run(self): if self._dialect == "sqlite": database_path = Path(self._sa_url.database) if not database_path.exists(): - self._signals.validation_failed.emit("File does not exist. Check the Database field in the URL.") + self._signals.validation_failed.emit( + "File does not exist. Check the Database field in the URL.", self._sa_url + ) return elif database_path.is_dir(): self._signals.validation_failed.emit( - "Database points to a directory, not a file." " Check the Database field in the URL." + "Database points to a directory, not a file. Check the Database field in the URL.", + self._sa_url, ) return error = check_database_url(self._sa_url) if error is not None: - self._signals.validation_failed.emit(error) + self._signals.validation_failed.emit(error, self._sa_url) return - self._signals.validation_succeeded.emit() - except Exception as error: - self._signals.validation_failed.emit(str(error)) + self._signals.validation_succeeded.emit(self._sa_url) finally: self._signals.finished.emit() application = QApplication.instance() @@ -71,8 +71,8 @@ def run(self): class _TaskSignals(QObject): """Signals for validation task.""" - validation_failed = Signal(str) - validation_succeeded = Signal() + validation_failed = Signal(str, object) + validation_succeeded = Signal(object) finished = Signal() @@ -94,6 +94,10 @@ def __init__(self, parent=None): self._deferred_task = None self._closed = False + def is_busy(self): + """Tests if there is a validator task running.""" + return self._busy + def validate_url(self, dialect, sa_url, fail_slot, success_slot): """Connects signals and starts a task to validate the given URL. diff --git a/spine_items/db_writer_executable_item_base.py b/spine_items/db_writer_executable_item_base.py index 771c64af..994df7e8 100644 --- a/spine_items/db_writer_executable_item_base.py +++ b/spine_items/db_writer_executable_item_base.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains base classes for items that write to db. - -""" +"""Contains base classes for items that write to db.""" from spine_engine.project_item.executable_item_base import ExecutableItemBase from spinedb_api.spine_db_client import SpineDBClient diff --git a/spine_items/db_writer_item_base.py b/spine_items/db_writer_item_base.py index 018e3f0f..2dc25986 100644 --- a/spine_items/db_writer_item_base.py +++ b/spine_items/db_writer_item_base.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,9 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains base classes for items that write to db. - -""" +"""Contains base classes for items that write to db.""" from PySide6.QtCore import Slot +from spine_engine.utils.helpers import ExecutionDirection from spinetoolbox.project_item.project_item import ProjectItem @@ -29,7 +28,7 @@ def successor_data_stores(self): @Slot(object, object) def handle_execution_successful(self, execution_direction, engine_state): """Notifies Toolbox of successful database import.""" - if execution_direction != "FORWARD": + if execution_direction != ExecutionDirection.FORWARD: return committed_db_maps = set() for successor in self.successor_data_stores(): diff --git a/spine_items/exporter/__init__.py b/spine_items/exporter/__init__.py index 8095b663..1a3c9f14 100644 --- a/spine_items/exporter/__init__.py +++ b/spine_items/exporter/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,3 +9,5 @@ # 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 . ###################################################################################################################### + +"""Exporter plugin.""" diff --git a/spine_items/exporter/commands.py b/spine_items/exporter/commands.py index 993ba4fc..3536fc53 100644 --- a/spine_items/exporter/commands.py +++ b/spine_items/exporter/commands.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,71 +9,102 @@ # 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 Exporter's undo commands. -""" +"""Contains Exporter's undo commands.""" from copy import copy, deepcopy from enum import IntEnum, unique from PySide6.QtCore import QModelIndex, Qt from PySide6.QtGui import QUndoCommand + +from spinetoolbox.helpers import SealCommand from spinetoolbox.project_commands import SpineToolboxCommand from .mvcmodels.mappings_table_model import MappingsTableModel @unique -class _ID(IntEnum): +class CommandId(IntEnum): CHANGE_POSITION = 1 + CHANGE_FIXED_TABLE_NAME = 2 + CHANGE_OUT_LABEL = 3 class UpdateOutLabel(SpineToolboxCommand): """Command to update exporter's output label.""" - def __init__(self, exporter, out_label, in_label, previous_label): + def __init__(self, exporter_name, out_label, in_label, previous_label, project): """ Args: - exporter (Exporter): exporter + exporter_name (str): exporter's name out_label (str): new output resource label in_label (str): associated input resource label previous_label (str): previous output resource label + project (SpineToolboxProject): project """ super().__init__() - self._exporter = exporter + self._exporter_name = exporter_name self._out_label = out_label self._previous_out_label = previous_label self._in_label = in_label - self.setText(f"change output label in {exporter.name}") + self._project = project + self.setText(f"change output label in {exporter_name}") + self._sealed = False + + def id(self): + return 1 def redo(self): - self._exporter.set_out_label(self._out_label, self._in_label) + exporter = self._project.get_item(self._exporter_name) + exporter.set_out_label(self._out_label, self._in_label) def undo(self): - self._exporter.set_out_label(self._previous_out_label, self._in_label) + exporter = self._project.get_item(self._exporter_name) + exporter.set_out_label(self._previous_out_label, self._in_label) + + def mergeWith(self, other): + if not self._sealed: + if ( + isinstance(other, UpdateOutLabel) + and self._exporter_name == other._exporter_name + and self._in_label == other._in_label + ): + if self._previous_out_label != other._out_label: + self._out_label = other._out_label + else: + self.setObsolete(True) + return True + if isinstance(other, SealCommand): + self._sealed = True + return True + return False class UpdateOutUrl(SpineToolboxCommand): """Command to update exporter's output URL.""" - def __init__(self, exporter, in_label, url, previous_url): + def __init__(self, exporter_name, in_label, url, previous_url, project): """ Args: - exporter (Exporter): exporter + exporter_name (str): exporter's name in_label (str): input resource label url (dict, optional): new URL dict previous_url (dict, optional): previous URL dict + project (SpineToolboxProject): project """ super().__init__() - self._exporter = exporter + self._exporter_name = exporter_name self._in_label = in_label self._url = copy(url) self._previous_url = copy(previous_url) - self.setText(f"change output URL in {exporter.name}") + self._project = project + self.setText(f"change output URL in {exporter_name}") def redo(self): - self._exporter.set_out_url(self._in_label, copy(self._url)) + exporter = self._project.get_item(self._exporter_name) + exporter.set_out_url(self._in_label, copy(self._url)) def undo(self): - self._exporter.set_out_url(self._in_label, copy(self._previous_url)) + exporter = self._project.get_item(self._exporter_name) + exporter.set_out_url(self._in_label, copy(self._previous_url)) class NewMapping(QUndoCommand): @@ -294,6 +326,8 @@ def undo(self): class SetFixedTableName(QUndoCommand): + ID = CommandId.CHANGE_FIXED_TABLE_NAME.value + def __init__(self, index, old_name, new_name): """ Args: @@ -305,6 +339,7 @@ def __init__(self, index, old_name, new_name): self._index = index self._old_name = old_name self._new_name = new_name + self._sealed = False def redo(self): self._index.model().setData(self._index, self._new_name, MappingsTableModel.FIXED_TABLE_NAME_ROLE) @@ -312,6 +347,22 @@ def redo(self): def undo(self): self._index.model().setData(self._index, self._old_name, MappingsTableModel.FIXED_TABLE_NAME_ROLE) + def id(self): + return self.ID + + def mergeWith(self, other): + if not self._sealed: + if isinstance(other, SetFixedTableName) and self.id() == other.id() and self._index == other._index: + if self._old_name != other._new_name: + self._new_name = other._new_name + else: + self.setObsolete(True) + return True + if isinstance(other, SealCommand): + self._sealed = True + return True + return False + class SetGroupFunction(QUndoCommand): def __init__(self, index, old_function, new_function): @@ -347,10 +398,10 @@ def __init__(self, index, old_dimension, new_dimension): self._new_dimension = new_dimension def redo(self): - self._index.model().setData(self._index, self._new_dimension, MappingsTableModel.HIGHLIGHT_DIMENSION_ROLE) + self._index.model().setData(self._index, self._new_dimension, MappingsTableModel.HIGHLIGHT_POSITION_ROLE) def undo(self): - self._index.model().setData(self._index, self._old_dimension, MappingsTableModel.HIGHLIGHT_DIMENSION_ROLE) + self._index.model().setData(self._index, self._old_dimension, MappingsTableModel.HIGHLIGHT_POSITION_ROLE) class SetMappingPositions(QUndoCommand): @@ -385,7 +436,7 @@ def previous_positions(self): return self._previous_positions def id(self): - return int(_ID.CHANGE_POSITION) + return int(CommandId.CHANGE_POSITION) def redo(self): self._mapping_editor_table_model.set_positions(self._positions, self._mapping_name) @@ -417,7 +468,7 @@ def __init__(self, editor, is_fixed_table_checked, previous_fixed_table_name, ma self._previous_mapping_root = self._mapping_index.data(MappingsTableModel.MAPPING_ROOT_ROLE) def id(self): - return int(_ID.CHANGE_POSITION) + return int(CommandId.CHANGE_POSITION) def mergeWith(self, other): if self._mapping_editor_table_model is not None: @@ -534,19 +585,23 @@ def undo(self): class UpdateOutputTimeStampsFlag(SpineToolboxCommand): """Command to set exporter's output directory time stamps flag.""" - def __init__(self, exporter, value): + def __init__(self, exporter_name, value, project): """ Args: - exporter (Exporter): exporter item + exporter_name (str): exporter's name value (bool): flag's new value + project (SpineToolboxProject): project """ super().__init__() - self.setText(f"toggle output time stamps setting of {exporter.name}") - self._exporter = exporter + self.setText(f"toggle output time stamps setting of {exporter_name}") + self._exporter_name = exporter_name self._value = value + self._project = project def redo(self): - self._exporter.set_output_time_stamps_flag(self._value) + exporter = self._project.get_item(self._exporter_name) + exporter.set_output_time_stamps_flag(self._value) def undo(self): - self._exporter.set_output_time_stamps_flag(not self._value) + exporter = self._project.get_item(self._exporter_name) + exporter.set_output_time_stamps_flag(not self._value) diff --git a/spine_items/exporter/do_work.py b/spine_items/exporter/do_work.py index bb866ef6..5ced6ff7 100644 --- a/spine_items/exporter/do_work.py +++ b/spine_items/exporter/do_work.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,15 +10,11 @@ # this program. If not, see . ###################################################################################################################### -""" -Exporter's execute kernel (do_work), as target for a multiprocess.Process - -""" +"""Exporter's execute kernel (do_work), as target for a multiprocess.Process""" import os from datetime import datetime from pathlib import Path from time import time - from spinedb_api.spine_io.exporters.writer import write, WriterException from spinedb_api.spine_io.exporters.csv_writer import CsvWriter from spinedb_api.spine_io.exporters.excel_writer import ExcelWriter @@ -98,7 +95,7 @@ def do_work( if not successful: return False, written_files finally: - database_map.connection.close() + database_map.close() return all(successes), written_files diff --git a/spine_items/exporter/executable_item.py b/spine_items/exporter/executable_item.py index 2c115310..0efdab8f 100644 --- a/spine_items/exporter/executable_item.py +++ b/spine_items/exporter/executable_item.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,15 +10,11 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains Exporter's executable item as well as support utilities. - -""" +"""Contains Exporter's executable item as well as support utilities.""" import json import os from json import dump from pathlib import Path - from spine_engine.project_item.executable_item_base import ExecutableItemBase from spine_engine.project_item.project_item_resource import file_resource_in_pack from spine_engine.utils.returning_process import ReturningProcess diff --git a/spine_items/exporter/export_manifest.py b/spine_items/exporter/export_manifest.py index 53ab7188..a08d2547 100644 --- a/spine_items/exporter/export_manifest.py +++ b/spine_items/exporter/export_manifest.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains utilities to manage export manifest files. - -""" +"""Contains utilities to manage export manifest files.""" import json from itertools import dropwhile from pathlib import Path diff --git a/spine_items/exporter/exporter.py b/spine_items/exporter/exporter.py index 65ff93bd..aa42afb7 100644 --- a/spine_items/exporter/exporter.py +++ b/spine_items/exporter/exporter.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,19 +10,16 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains the :class:`Exporter` project item. - -""" +"""Contains the :class:`Exporter` project item.""" from dataclasses import dataclass from itertools import combinations, zip_longest from operator import itemgetter from pathlib import Path - from PySide6.QtCore import Slot, Qt - +from spinetoolbox.helpers import SealCommand from spinetoolbox.project_item.project_item import ProjectItem from spine_engine.utils.serialization import deserialize_path +from spine_engine.utils.helpers import ExecutionDirection from spinedb_api import clear_filter_configs from .export_manifest import exported_files_as_resources from .specification import OutputFormat @@ -30,7 +28,7 @@ from .widgets.export_list_item import ExportListItem from .item_info import ItemInfo from .executable_item import ExecutableItem -from .commands import UpdateOutLabel, UpdateOutputTimeStampsFlag, UpdateOutUrl +from .commands import CommandId, UpdateOutLabel, UpdateOutputTimeStampsFlag, UpdateOutUrl from .output_channel import OutputChannel from .utils import EXPORTER_EXECUTION_MANIFEST_FILE_PREFIX, output_database_resources @@ -100,11 +98,6 @@ def item_type(): """See base class.""" return ItemInfo.item_type() - @staticmethod - def item_category(): - """See base class.""" - return ItemInfo.item_category() - @property def executable_class(self): return ExecutableItem @@ -119,7 +112,7 @@ def has_out_url(self): def handle_execution_successful(self, execution_direction, engine_state): """See base class.""" - if execution_direction != "FORWARD": + if execution_direction != ExecutionDirection.FORWARD: return self._resources_to_successors_changed() @@ -138,7 +131,7 @@ def _cancel_on_error_option_changed(self, checkbox_state): cancel = checkbox_state == Qt.CheckState.Checked.value if self._cancel_on_error == cancel: return - self._toolbox.undo_stack.push(UpdateCancelOnErrorCommand(self, cancel)) + self._toolbox.undo_stack.push(UpdateCancelOnErrorCommand(self.name, cancel, self._project)) def set_cancel_on_error(self, cancel): """Sets the Cancel export on error option.""" @@ -166,6 +159,7 @@ def _update_properties_tab(self): output_format = self._specification.output_format if self._specification is not None else None item.set_out_url_enabled(output_format is None or output_format == OutputFormat.SQL) item.out_label_changed.connect(self._update_out_label) + item.out_label_editing_finished.connect(self._seal_out_label_update) item.out_url_changed.connect(self._update_out_url) self._properties_ui.output_time_stamps_check_box.setCheckState( Qt.CheckState.Checked if self._append_output_time_stamps else Qt.CheckState.Unchecked @@ -300,7 +294,16 @@ def _update_out_label(self, out_label, in_label): in_label (str): associated in label """ previous = next(c for c in self._output_channels if c.in_label == in_label) - self._toolbox.undo_stack.push(UpdateOutLabel(self, out_label, in_label, previous.out_label)) + self._toolbox.undo_stack.push(UpdateOutLabel(self.name, out_label, in_label, previous.out_label, self._project)) + + @Slot(str) + def _seal_out_label_update(self, in_label): + """Pushes a sealing command to undo stack. + + Args: + in_label (str): associated in label + """ + self._toolbox.undo_stack.push(SealCommand(CommandId.CHANGE_OUT_LABEL.value)) @Slot(str, dict) def _update_out_url(self, in_label, url_dict): @@ -313,7 +316,9 @@ def _update_out_url(self, in_label, url_dict): for channel in self._output_channels: if channel.in_label == in_label: if channel.out_url != url_dict: - self._toolbox.undo_stack.push(UpdateOutUrl(self, in_label, url_dict, channel.out_url)) + self._toolbox.undo_stack.push( + UpdateOutUrl(self.name, in_label, url_dict, channel.out_url, self._project) + ) break else: raise RuntimeError(f"Logic error: cannot find channel for input label {in_label}") @@ -484,7 +489,7 @@ def _change_output_time_stamps_flag(self, checkbox_state): flag = checkbox_state == Qt.CheckState.Checked.value if flag == self._append_output_time_stamps: return - self._toolbox.undo_stack.push(UpdateOutputTimeStampsFlag(self, flag)) + self._toolbox.undo_stack.push(UpdateOutputTimeStampsFlag(self.name, flag, self._project)) def set_output_time_stamps_flag(self, flag): """ diff --git a/spine_items/exporter/exporter_factory.py b/spine_items/exporter/exporter_factory.py index ef07e24e..14207d68 100644 --- a/spine_items/exporter/exporter_factory.py +++ b/spine_items/exporter/exporter_factory.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains :class:`ExporterFactory`. - -""" - +"""Contains :class:`ExporterFactory`.""" from PySide6.QtGui import QColor from spinetoolbox.project_item.project_item_factory import ProjectItemFactory from .exporter import Exporter diff --git a/spine_items/exporter/exporter_icon.py b/spine_items/exporter/exporter_icon.py index 06e9b3ef..8a4d6a23 100644 --- a/spine_items/exporter/exporter_icon.py +++ b/spine_items/exporter/exporter_icon.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains :class:`ExporterIcon`. - -""" - +"""Contains :class:`ExporterIcon`.""" from spinetoolbox.project_item_icon import ProjectItemIcon from ..animations import ExporterAnimation, AnimationSignaller @@ -41,5 +38,5 @@ def mouseDoubleClickEvent(self, e): e (QGraphicsSceneMouseEvent): Event """ super().mouseDoubleClickEvent(e) - item = self._toolbox.project_item_model.get_item(self._name) - item.project_item.show_specification_window() + item = self._toolbox.project().get_item(self._name) + item.show_specification_window() diff --git a/spine_items/exporter/item_info.py b/spine_items/exporter/item_info.py index 11a5fb1e..d08ec8e3 100644 --- a/spine_items/exporter/item_info.py +++ b/spine_items/exporter/item_info.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,19 +9,12 @@ # 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 . ###################################################################################################################### -""" -Exporter project item info. -""" +"""Exporter project item info.""" from spine_engine.project_item.project_item_info import ProjectItemInfo class ItemInfo(ProjectItemInfo): - @staticmethod - def item_category(): - """See base class.""" - return "Exporters" - @staticmethod def item_type(): """See base class.""" diff --git a/spine_items/exporter/mvcmodels/__init__.py b/spine_items/exporter/mvcmodels/__init__.py index 8095b663..046209e7 100644 --- a/spine_items/exporter/mvcmodels/__init__.py +++ b/spine_items/exporter/mvcmodels/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/exporter/mvcmodels/database_list_model.py b/spine_items/exporter/mvcmodels/database_list_model.py deleted file mode 100644 index 1370ae5e..00000000 --- a/spine_items/exporter/mvcmodels/database_list_model.py +++ /dev/null @@ -1,125 +0,0 @@ -###################################################################################################################### -# Copyright (C) 2017-2022 Spine project consortium -# This file is part of Spine Items. -# Spine Items 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 a model for Exporter's output preview. - -""" -from PySide6.QtCore import QAbstractListModel, QModelIndex, Qt -from spine_items.utils import Database - - -class DatabaseListModel(QAbstractListModel): - """A model for exporter database lists.""" - - def __init__(self, databases): - """ - Args: - databases (list of Database): databases to list - """ - super().__init__() - self._databases = databases - - def add(self, database): - """ - Appends a database to the list. - - Args: - database (Database): a database to add - """ - row = len(self._databases) - self.beginInsertRows(QModelIndex(), row, row) - self._databases.append(database) - self.endInsertRows() - - def data(self, index, role=Qt.ItemDataRole.DisplayRole): - if not index.isValid(): - return None - if role == Qt.ItemDataRole.DisplayRole: - return self._databases[index.row()].url - return None - - def insertRows(self, row, count, parent=QModelIndex()): - self.beginInsertRows(parent, row, row + count - 1) - self._databases = self._databases[:row] + [Database() for _ in range(count)] + self._databases[row:] - self.endInsertRows() - - def item(self, url): - """ - Returns database item for given URL. - - Args: - url (str): database URL - - Returns: - Database: a database - """ - for db in self._databases: - if db.url == url: - return db - raise RuntimeError(f"Database '{url}' not found.") - - def items(self): - """ - Returns a list of databases this model contains. - - Returns: - list of Database: database - """ - return self._databases - - def remove(self, url): - """ - Removes database item with given URL. - - Args: - url (str): database URL - - Returns: - Database: removed database or None if not found - """ - for row, db in enumerate(self._databases): - if db.url == url: - self.removeRows(row, 1) - return db - return None - - def removeRows(self, row, count, parent=QModelIndex()): - self.beginRemoveRows(parent, row, row + count - 1) - self._databases = self._databases[:row] + self._databases[row + count :] - self.endRemoveRows() - - def rowCount(self, parent=QModelIndex()): - return len(self._databases) - - def update_url(self, old, new): - """ - Updates a database URL. - - Args: - old (str): old URL - new (str): new URL - """ - for row, db in enumerate(self._databases): - if old == db.url: - db.url = new - index = self.index(row, 0) - self.dataChanged.emit(index, index, [Qt.ItemDataRole.DisplayRole]) - return - - def urls(self): - """ - Returns database URLs. - - Returns: - set of str: database URLs - """ - return {db.url for db in self._databases} diff --git a/spine_items/exporter/mvcmodels/full_url_list_model.py b/spine_items/exporter/mvcmodels/full_url_list_model.py index a44a6769..166d2428 100644 --- a/spine_items/exporter/mvcmodels/full_url_list_model.py +++ b/spine_items/exporter/mvcmodels/full_url_list_model.py @@ -8,6 +8,7 @@ # 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 . ###################################################################################################################### + """Exporter's ``FullUrlListModel``.""" from PySide6.QtCore import QAbstractListModel, QModelIndex, Qt diff --git a/spine_items/exporter/mvcmodels/mapping_editor_table_model.py b/spine_items/exporter/mvcmodels/mapping_editor_table_model.py index 20db81aa..9c15edcf 100644 --- a/spine_items/exporter/mvcmodels/mapping_editor_table_model.py +++ b/spine_items/exporter/mvcmodels/mapping_editor_table_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,13 +9,10 @@ # 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 model for export mapping setup table. -""" +"""Contains model for export mapping setup table.""" from enum import IntEnum, unique from operator import itemgetter - from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt from PySide6.QtGui import QFont, QColor from spinedb_api.mapping import is_pivoted, is_regular, Position, value_index @@ -24,12 +22,10 @@ FixedValueMapping, ExpandedParameterValueMapping, ExpandedParameterDefaultValueMapping, - FeatureEntityClassMapping, - FeatureParameterDefinitionMapping, - ObjectClassMapping, - ObjectGroupMapping, - ObjectGroupObjectMapping, - ObjectMapping, + EntityClassMapping, + EntityGroupMapping, + EntityGroupEntityMapping, + EntityMapping, ParameterDefaultValueMapping, ParameterDefaultValueIndexMapping, ParameterDefinitionMapping, @@ -38,27 +34,18 @@ ParameterValueListValueMapping, ParameterValueMapping, ParameterValueTypeMapping, - RelationshipClassMapping, - RelationshipClassObjectClassMapping, - RelationshipMapping, - RelationshipObjectMapping, + DimensionMapping, + ElementMapping, ScenarioActiveFlagMapping, ScenarioAlternativeMapping, ScenarioBeforeAlternativeMapping, ScenarioDescriptionMapping, ScenarioMapping, - ToolFeatureEntityClassMapping, - ToolFeatureMethodEntityClassMapping, - ToolFeatureMethodMethodMapping, - ToolFeatureMethodParameterDefinitionMapping, - ToolFeatureParameterDefinitionMapping, - ToolFeatureRequiredFlagMapping, - ToolMapping, IndexNameMapping, DefaultValueIndexNameMapping, ParameterDefaultValueTypeMapping, ) -from spinetoolbox.helpers import color_from_index +from spinetoolbox.helpers import color_from_index, plain_to_rich from ..commands import SetMappingNullable, SetMappingPositions, SetMappingProperty @@ -165,9 +152,9 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): return self._mapping_colors.get(m.position, QColor(Qt.GlobalColor.gray).lighter()) elif role == Qt.ItemDataRole.ToolTipRole: if column == EditorColumn.FILTER: - return "Regular expression to filter database items." + return plain_to_rich("Regular expression to filter database items.") elif column == EditorColumn.NULLABLE: - return "When checked, ignore this row if it yields nothing to export." + return plain_to_rich("When checked, ignore this row if it yields nothing to export.") if role == self.MAPPING_ITEM_ROLE: return self._mappings[index.row()] return None @@ -443,13 +430,12 @@ def compact(self): DefaultValueIndexNameMapping: "Default value index names", ExpandedParameterDefaultValueMapping: "Default values", ExpandedParameterValueMapping: "Parameter values", - FeatureEntityClassMapping: "Entity classes", - FeatureParameterDefinitionMapping: "Parameter definitions", IndexNameMapping: "Parameter index names", - ObjectClassMapping: "Object classes", - ObjectGroupMapping: "Object groups", - ObjectGroupObjectMapping: "Objects", - ObjectMapping: "Objects", + EntityClassMapping: "Entity classes", + EntityGroupMapping: "Entity groups", + EntityGroupEntityMapping: "Entities", + EntityMapping: "Entities", + ElementMapping: "Elements", ParameterDefaultValueMapping: "Default values", ParameterDefaultValueIndexMapping: "Default value indexes", ParameterDefaultValueTypeMapping: "Default value types", @@ -459,22 +445,12 @@ def compact(self): ParameterValueListValueMapping: "Value list values", ParameterValueMapping: "Parameter values", ParameterValueTypeMapping: "Value types", - RelationshipClassMapping: "Relationship classes", - RelationshipClassObjectClassMapping: "Object classes", - RelationshipMapping: "Relationships", - RelationshipObjectMapping: "Objects", + DimensionMapping: "Dimensions", ScenarioActiveFlagMapping: "Active flags", ScenarioAlternativeMapping: "Alternatives", ScenarioBeforeAlternativeMapping: "Before alternatives", ScenarioDescriptionMapping: "Scenarios description", ScenarioMapping: "Scenarios", - ToolFeatureEntityClassMapping: "Entity classes", - ToolFeatureMethodEntityClassMapping: "Entity classes", - ToolFeatureMethodMethodMapping: "Methods", - ToolFeatureMethodParameterDefinitionMapping: "Parameter definitions", - ToolFeatureParameterDefinitionMapping: "Parameter definitions", - ToolFeatureRequiredFlagMapping: "Required flags", - ToolMapping: "Tools", } diff --git a/spine_items/exporter/mvcmodels/mappings_table_model.py b/spine_items/exporter/mvcmodels/mappings_table_model.py index 0d1d7108..8994be8f 100644 --- a/spine_items/exporter/mvcmodels/mappings_table_model.py +++ b/spine_items/exporter/mvcmodels/mappings_table_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,16 +9,14 @@ # 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 the :class:`MappingListModel` model. -""" +"""Contains the :class:`MappingListModel` model.""" from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal from spinedb_api.export_mapping.export_mapping import ( ParameterDefaultValueIndexMapping, ParameterValueIndexMapping, - RelationshipClassObjectClassMapping, - RelationshipClassMapping, + DimensionMapping, + EntityClassMapping, ) from spinetoolbox.helpers import unique_name @@ -40,12 +39,12 @@ class MappingsTableModel(QAbstractTableModel): MAPPING_TYPE_ROLE = Qt.ItemDataRole.UserRole + 2 MAPPING_ROOT_ROLE = Qt.ItemDataRole.UserRole + 3 ALWAYS_EXPORT_HEADER_ROLE = Qt.ItemDataRole.UserRole + 4 - RELATIONSHIP_DIMENSIONS_ROLE = Qt.ItemDataRole.UserRole + 5 + ENTITY_DIMENSIONS_ROLE = Qt.ItemDataRole.UserRole + 5 USE_FIXED_TABLE_NAME_FLAG_ROLE = Qt.ItemDataRole.UserRole + 6 FIXED_TABLE_NAME_ROLE = Qt.ItemDataRole.UserRole + 7 PARAMETER_DIMENSIONS_ROLE = Qt.ItemDataRole.UserRole + 8 GROUP_FN_ROLE = Qt.ItemDataRole.UserRole + 9 - HIGHLIGHT_DIMENSION_ROLE = Qt.ItemDataRole.UserRole + 10 + HIGHLIGHT_POSITION_ROLE = Qt.ItemDataRole.UserRole + 10 def __init__(self, mappings=None, parent=None): """ @@ -116,8 +115,8 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): return spec.root if role == self.ALWAYS_EXPORT_HEADER_ROLE: return spec.always_export_header - if role == self.RELATIONSHIP_DIMENSIONS_ROLE: - return _instance_occurrences(spec.root, RelationshipClassObjectClassMapping) + if role == self.ENTITY_DIMENSIONS_ROLE: + return _instance_occurrences(spec.root, DimensionMapping) if role == self.USE_FIXED_TABLE_NAME_FLAG_ROLE: return spec.use_fixed_table_name_flag if role == self.FIXED_TABLE_NAME_ROLE: @@ -129,13 +128,11 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): return dimensions if role == self.GROUP_FN_ROLE: return spec.group_fn - if role == self.HIGHLIGHT_DIMENSION_ROLE: - highlighting_mapping = next( - (m for m in spec.root.flatten() if isinstance(m, RelationshipClassMapping)), None - ) + if role == self.HIGHLIGHT_POSITION_ROLE: + highlighting_mapping = next((m for m in spec.root.flatten() if isinstance(m, EntityClassMapping)), None) if highlighting_mapping is None: return None - return highlighting_mapping.highlight_dimension + return highlighting_mapping.highlight_position return None def flags(self, index): @@ -239,14 +236,12 @@ def setData(self, index, value, role=Qt.ItemDataRole.EditRole): elif role == self.GROUP_FN_ROLE: spec.group_fn = value self.dataChanged.emit(index, index, [self.GROUP_FN_ROLE]) - elif role == self.HIGHLIGHT_DIMENSION_ROLE: - highlighting_mapping = next( - (m for m in spec.root.flatten() if isinstance(m, RelationshipClassMapping)), None - ) + elif role == self.HIGHLIGHT_POSITION_ROLE: + highlighting_mapping = next((m for m in spec.root.flatten() if isinstance(m, EntityClassMapping)), None) if highlighting_mapping is None: return False - highlighting_mapping.highlight_dimension = value - self.dataChanged.emit(index, index, [self.HIGHLIGHT_DIMENSION_ROLE]) + highlighting_mapping.highlight_position = value + self.dataChanged.emit(index, index, [self.HIGHLIGHT_POSITION_ROLE]) return True return False diff --git a/spine_items/exporter/mvcmodels/mappings_table_proxy.py b/spine_items/exporter/mvcmodels/mappings_table_proxy.py index 368dc8e8..61aeb1ca 100644 --- a/spine_items/exporter/mvcmodels/mappings_table_proxy.py +++ b/spine_items/exporter/mvcmodels/mappings_table_proxy.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 the :class:`MappingsTableProxy` model. -""" +"""Contains the :class:`MappingsTableProxy` model.""" from itertools import takewhile from PySide6.QtCore import QSortFilterProxyModel diff --git a/spine_items/exporter/mvcmodels/preview_table_model.py b/spine_items/exporter/mvcmodels/preview_table_model.py index 1fc3d683..1d147489 100644 --- a/spine_items/exporter/mvcmodels/preview_table_model.py +++ b/spine_items/exporter/mvcmodels/preview_table_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 model for a single export preview table. -""" +"""Contains model for a single export preview table.""" from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt diff --git a/spine_items/exporter/mvcmodels/preview_tree_model.py b/spine_items/exporter/mvcmodels/preview_tree_model.py index 6d36cae8..62071f39 100644 --- a/spine_items/exporter/mvcmodels/preview_tree_model.py +++ b/spine_items/exporter/mvcmodels/preview_tree_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,13 +9,10 @@ # 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 model for export preview tables. -""" +"""Contains model for export preview tables.""" from itertools import takewhile from operator import methodcaller - from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt diff --git a/spine_items/exporter/output_channel.py b/spine_items/exporter/output_channel.py index 56a41343..007e432d 100644 --- a/spine_items/exporter/output_channel.py +++ b/spine_items/exporter/output_channel.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,13 +9,10 @@ # 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 :class:`OutputChannel` class. -""" +"""Contains :class:`OutputChannel` class.""" from contextlib import suppress from dataclasses import dataclass, InitVar - from spine_engine.utils.serialization import deserialize_path, serialize_path diff --git a/spine_items/exporter/preview_table_writer.py b/spine_items/exporter/preview_table_writer.py index fe9be0a2..2d25f377 100644 --- a/spine_items/exporter/preview_table_writer.py +++ b/spine_items/exporter/preview_table_writer.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 . ###################################################################################################################### -""" -Functions to write export preview tables. -""" +"""Functions to write export preview tables.""" import numpy from spinedb_api.spine_io.exporters.writer import Writer diff --git a/spine_items/exporter/specification.py b/spine_items/exporter/specification.py index fcc462b9..2a1907cb 100644 --- a/spine_items/exporter/specification.py +++ b/spine_items/exporter/specification.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains Exporter's specifications. - -""" +"""Contains Exporter's specifications.""" from dataclasses import dataclass from enum import Enum, unique from spine_engine.project_item.project_item_specification import ProjectItemSpecification @@ -21,9 +19,9 @@ ExportMapping, FixedValueMapping, from_dict as mapping_from_dict, - ObjectClassMapping, - ObjectGroupMapping, - ObjectGroupObjectMapping, + EntityClassMapping, + EntityGroupMapping, + EntityGroupEntityMapping, ParameterValueIndexMapping, ParameterDefaultValueIndexMapping, IndexNameMapping, @@ -36,22 +34,30 @@ @unique class MappingType(Enum): alternatives = "alternatives" - features = "features" - objects = "objects" - object_groups = "object_groups" - object_parameter_default_values = "object_default_parameter_values" - object_parameter_values = "object_parameter_values" + entities = "entities" + entity_groups = "entity_groups" + entity_parameter_default_values = "entity_parameter_default_values" + entity_parameter_values = "entity_parameter_values" + entity_dimension_parameter_default_values = "entity_dimension_parameter_default_values" + entity_dimension_parameter_values = "entity_dimension_parameter_values" parameter_value_lists = "parameter_value_lists" - relationships = "relationships" - relationship_parameter_default_values = "relationship_default_parameter_values" - relationship_parameter_values = "relationship_parameter_values" - relationship_object_parameter_default_values = "relationship_object_parameter_default_values" - relationship_object_parameter_values = "relationship_object_parameter_values" scenario_alternatives = "scenario_alternatives" scenarios = "scenarios" - tool_features = "tool_features" - tool_feature_methods = "tool_feature_methods" - tools = "tools" + + @classmethod + def from_legacy_type(cls, type_str): + type_str = { + "objects": "entities", + "object_groups": "entity_groups", + "object_default_parameter_values": "entity_parameter_default_values", + "object_parameter_values": "entity_parameter_values", + "relationships": "entities", + "relationship_default_parameter_values": "entity_parameter_default_values", + "relationship_parameter_values": "entity_parameter_values", + "relationship_object_parameter_default_values": "entity_dimension_parameter_default_values", + "relationship_object_parameter_values": "entity_dimension_parameter_values", + }.get(type_str, type_str) + return cls(type_str) @unique @@ -161,7 +167,7 @@ def from_dict(mapping_dict): MappingSpecification: deserialized specification """ root = mapping_from_dict(mapping_dict.pop("root")) - type_ = MappingType(mapping_dict.pop("type")) + type_ = MappingType.from_legacy_type(mapping_dict.pop("type")) return MappingSpecification(root=root, type=type_, **mapping_dict) @@ -176,7 +182,7 @@ def __init__(self, name="", description="", mapping_specifications=None, output_ mapping_specifications (dict, optional): mapping from export mapping name to ``MappingSpecification`` output_format (OutputFormat): output format """ - super().__init__(name, description, ItemInfo.item_type(), ItemInfo.item_category()) + super().__init__(name, description, ItemInfo.item_type()) if mapping_specifications is None: mapping_specifications = dict() self._mapping_specifications = mapping_specifications @@ -294,17 +300,17 @@ def from_dict(specification_dict): # Legacy: remove parameter value mappings from object group mappings, # they're not supported anymore. if spec_dict["type"] == "object_group_parameter_values": - spec_dict["type"] = MappingType.object_groups.value + spec_dict["type"] = MappingType.entity_groups.value keep_map_types = { FixedValueMapping.MAP_TYPE, - ObjectClassMapping.MAP_TYPE, - ObjectGroupMapping.MAP_TYPE, - ObjectGroupObjectMapping.MAP_TYPE, + EntityClassMapping.MAP_TYPE, + EntityGroupMapping.MAP_TYPE, + EntityGroupEntityMapping.MAP_TYPE, } spec_dict["mapping"] = [m for m in spec_dict["mapping"] if m["map_type"] in keep_map_types] mapping_specifications = { name: MappingSpecification( - MappingType(spec_dict["type"]), + MappingType.from_legacy_type(spec_dict["type"]), spec_dict.get("enabled", True), spec_dict.get("always_export_header", True), spec_dict.get("group_fn", legacy_group_fn_from_dict(spec_dict["mapping"])), diff --git a/spine_items/exporter/specification_factory.py b/spine_items/exporter/specification_factory.py index 9e7692c6..5e15076f 100644 --- a/spine_items/exporter/specification_factory.py +++ b/spine_items/exporter/specification_factory.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Exporter's specification factory. - -""" +"""Exporter's specification factory.""" from spine_engine.project_item.project_item_specification_factory import ProjectItemSpecificationFactory from .item_info import ItemInfo from .specification import Specification diff --git a/spine_items/exporter/ui/__init__.py b/spine_items/exporter/ui/__init__.py index 8095b663..046209e7 100644 --- a/spine_items/exporter/ui/__init__.py +++ b/spine_items/exporter/ui/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/exporter/ui/export_list_item.py b/spine_items/exporter/ui/export_list_item.py index 4654d8ba..55fe910b 100644 --- a/spine_items/exporter/ui/export_list_item.py +++ b/spine_items/exporter/ui/export_list_item.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/exporter/ui/exporter_properties.py b/spine_items/exporter/ui/exporter_properties.py index 18a9a17f..fcf3d0d6 100644 --- a/spine_items/exporter/ui/exporter_properties.py +++ b/spine_items/exporter/ui/exporter_properties.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/exporter/ui/specification_editor.py b/spine_items/exporter/ui/specification_editor.py index 87c1f1ba..ae168e90 100644 --- a/spine_items/exporter/ui/specification_editor.py +++ b/spine_items/exporter/ui/specification_editor.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -26,10 +27,11 @@ QImage, QKeySequence, QLinearGradient, QPainter, QPalette, QPixmap, QRadialGradient, QTransform) from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QComboBox, - QDockWidget, QFormLayout, QHBoxLayout, QHeaderView, - QLabel, QLineEdit, QMainWindow, QPushButton, - QSizePolicy, QSpacerItem, QSpinBox, QTableView, - QToolButton, QTreeView, QVBoxLayout, QWidget) + QFormLayout, QFrame, QHBoxLayout, QHeaderView, + QLabel, QLayout, QLineEdit, QMainWindow, + QPushButton, QSizePolicy, QSpacerItem, QSpinBox, + QSplitter, QTableView, QToolButton, QTreeView, + QVBoxLayout, QWidget) from spinetoolbox.widgets.custom_combobox import ElidedCombobox from spine_items import resources_icons_rc @@ -38,82 +40,189 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(1146, 801) + MainWindow.resize(1086, 780) MainWindow.setDockNestingEnabled(True) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") - MainWindow.setCentralWidget(self.centralwidget) - self.mappings_dock = QDockWidget(MainWindow) - self.mappings_dock.setObjectName(u"mappings_dock") - self.dockWidgetContents = QWidget() - self.dockWidgetContents.setObjectName(u"dockWidgetContents") - self.verticalLayout = QVBoxLayout(self.dockWidgetContents) - self.verticalLayout.setSpacing(3) - self.verticalLayout.setObjectName(u"verticalLayout") - self.verticalLayout.setContentsMargins(3, 3, 3, 3) - self.horizontalLayout_2 = QHBoxLayout() - self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") - self.add_mapping_button = QPushButton(self.dockWidgetContents) - self.add_mapping_button.setObjectName(u"add_mapping_button") + self.verticalLayout_10 = QVBoxLayout(self.centralwidget) + self.verticalLayout_10.setObjectName(u"verticalLayout_10") + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.horizontalLayout_3.setSizeConstraint(QLayout.SetDefaultConstraint) + self.label = QLabel(self.centralwidget) + self.label.setObjectName(u"label") + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) + self.label.setSizePolicy(sizePolicy) - self.horizontalLayout_2.addWidget(self.add_mapping_button) + self.horizontalLayout_3.addWidget(self.label) - self.remove_mapping_button = QPushButton(self.dockWidgetContents) - self.remove_mapping_button.setObjectName(u"remove_mapping_button") + self.export_format_combo_box = QComboBox(self.centralwidget) + self.export_format_combo_box.setObjectName(u"export_format_combo_box") + sizePolicy1 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + sizePolicy1.setHorizontalStretch(1) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.export_format_combo_box.sizePolicy().hasHeightForWidth()) + self.export_format_combo_box.setSizePolicy(sizePolicy1) - self.horizontalLayout_2.addWidget(self.remove_mapping_button) + self.horizontalLayout_3.addWidget(self.export_format_combo_box) - self.toggle_enabled_button = QPushButton(self.dockWidgetContents) - self.toggle_enabled_button.setObjectName(u"toggle_enabled_button") + self.live_preview_check_box = QCheckBox(self.centralwidget) + self.live_preview_check_box.setObjectName(u"live_preview_check_box") + self.live_preview_check_box.setChecked(False) - self.horizontalLayout_2.addWidget(self.toggle_enabled_button) + self.horizontalLayout_3.addWidget(self.live_preview_check_box) - self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.frame_preview = QFrame(self.centralwidget) + self.frame_preview.setObjectName(u"frame_preview") + self.frame_preview.setEnabled(False) + sizePolicy2 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.frame_preview.sizePolicy().hasHeightForWidth()) + self.frame_preview.setSizePolicy(sizePolicy2) + self.frame_preview.setFrameShape(QFrame.StyledPanel) + self.frame_preview.setFrameShadow(QFrame.Raised) + self.horizontalLayout = QHBoxLayout(self.frame_preview) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.horizontalLayout.setContentsMargins(3, 0, 3, 0) + self.label_9 = QLabel(self.frame_preview) + self.label_9.setObjectName(u"label_9") - self.horizontalLayout_2.addItem(self.horizontalSpacer_2) + self.horizontalLayout.addWidget(self.label_9) + + self.database_url_combo_box = ElidedCombobox(self.frame_preview) + self.database_url_combo_box.setObjectName(u"database_url_combo_box") + sizePolicy3 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + sizePolicy3.setHorizontalStretch(0) + sizePolicy3.setVerticalStretch(0) + sizePolicy3.setHeightForWidth(self.database_url_combo_box.sizePolicy().hasHeightForWidth()) + self.database_url_combo_box.setSizePolicy(sizePolicy3) + self.database_url_combo_box.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) + self.database_url_combo_box.setMinimumContentsLength(16) + + self.horizontalLayout.addWidget(self.database_url_combo_box) + + self.load_url_from_fs_button = QToolButton(self.frame_preview) + self.load_url_from_fs_button.setObjectName(u"load_url_from_fs_button") + icon = QIcon() + icon.addFile(u":/icons/folder-open-solid.svg", QSize(), QIcon.Normal, QIcon.Off) + self.load_url_from_fs_button.setIcon(icon) + self.horizontalLayout.addWidget(self.load_url_from_fs_button) - self.verticalLayout.addLayout(self.horizontalLayout_2) + self.label_3 = QLabel(self.frame_preview) + self.label_3.setObjectName(u"label_3") + + self.horizontalLayout.addWidget(self.label_3) - self.mappings_table = QTableView(self.dockWidgetContents) + self.max_preview_tables_spin_box = QSpinBox(self.frame_preview) + self.max_preview_tables_spin_box.setObjectName(u"max_preview_tables_spin_box") + self.max_preview_tables_spin_box.setMaximum(16777215) + self.max_preview_tables_spin_box.setSingleStep(10) + self.max_preview_tables_spin_box.setValue(20) + + self.horizontalLayout.addWidget(self.max_preview_tables_spin_box) + + self.label_2 = QLabel(self.frame_preview) + self.label_2.setObjectName(u"label_2") + + self.horizontalLayout.addWidget(self.label_2) + + self.max_preview_rows_spin_box = QSpinBox(self.frame_preview) + self.max_preview_rows_spin_box.setObjectName(u"max_preview_rows_spin_box") + self.max_preview_rows_spin_box.setMaximum(16777215) + self.max_preview_rows_spin_box.setSingleStep(10) + self.max_preview_rows_spin_box.setValue(20) + + self.horizontalLayout.addWidget(self.max_preview_rows_spin_box) + + + self.horizontalLayout_3.addWidget(self.frame_preview) + + + self.verticalLayout_10.addLayout(self.horizontalLayout_3) + + self.splitter_3 = QSplitter(self.centralwidget) + self.splitter_3.setObjectName(u"splitter_3") + self.splitter_3.setOrientation(Qt.Horizontal) + self.splitter_2 = QSplitter(self.splitter_3) + self.splitter_2.setObjectName(u"splitter_2") + self.splitter_2.setOrientation(Qt.Vertical) + self.mapping_list_layout_widget = QWidget(self.splitter_2) + self.mapping_list_layout_widget.setObjectName(u"mapping_list_layout_widget") + self.verticalLayout_9 = QVBoxLayout(self.mapping_list_layout_widget) + self.verticalLayout_9.setSpacing(0) + self.verticalLayout_9.setObjectName(u"verticalLayout_9") + self.frame = QFrame(self.mapping_list_layout_widget) + self.frame.setObjectName(u"frame") + self.frame.setFrameShape(QFrame.StyledPanel) + self.frame.setFrameShadow(QFrame.Raised) + self.verticalLayout_11 = QVBoxLayout(self.frame) + self.verticalLayout_11.setSpacing(0) + self.verticalLayout_11.setObjectName(u"verticalLayout_11") + self.verticalLayout_11.setContentsMargins(3, 3, 3, 3) + self.label_11 = QLabel(self.frame) + self.label_11.setObjectName(u"label_11") + + self.verticalLayout_11.addWidget(self.label_11) + + + self.verticalLayout_9.addWidget(self.frame) + + self.mappings_table = QTableView(self.mapping_list_layout_widget) self.mappings_table.setObjectName(u"mappings_table") self.mappings_table.setContextMenuPolicy(Qt.CustomContextMenu) self.mappings_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.mappings_table.setShowGrid(False) self.mappings_table.verticalHeader().setVisible(False) - self.verticalLayout.addWidget(self.mappings_table) + self.verticalLayout_9.addWidget(self.mappings_table) - self.horizontalLayout_6 = QHBoxLayout() - self.horizontalLayout_6.setObjectName(u"horizontalLayout_6") - self.horizontalSpacer_4 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.splitter_2.addWidget(self.mapping_list_layout_widget) + self.mapping_controls_layout_widget = QWidget(self.splitter_2) + self.mapping_controls_layout_widget.setObjectName(u"mapping_controls_layout_widget") + self.verticalLayout_8 = QVBoxLayout(self.mapping_controls_layout_widget) + self.verticalLayout_8.setObjectName(u"verticalLayout_8") + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.add_mapping_button = QPushButton(self.mapping_controls_layout_widget) + self.add_mapping_button.setObjectName(u"add_mapping_button") - self.horizontalLayout_6.addItem(self.horizontalSpacer_4) + self.horizontalLayout_2.addWidget(self.add_mapping_button) + + self.remove_mapping_button = QPushButton(self.mapping_controls_layout_widget) + self.remove_mapping_button.setObjectName(u"remove_mapping_button") + + self.horizontalLayout_2.addWidget(self.remove_mapping_button) + + self.toggle_enabled_button = QPushButton(self.mapping_controls_layout_widget) + self.toggle_enabled_button.setObjectName(u"toggle_enabled_button") - self.write_earlier_button = QPushButton(self.dockWidgetContents) + self.horizontalLayout_2.addWidget(self.toggle_enabled_button) + + self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.horizontalLayout_2.addItem(self.horizontalSpacer_2) + + self.write_earlier_button = QPushButton(self.mapping_controls_layout_widget) self.write_earlier_button.setObjectName(u"write_earlier_button") - self.horizontalLayout_6.addWidget(self.write_earlier_button) + self.horizontalLayout_2.addWidget(self.write_earlier_button) - self.write_later_button = QPushButton(self.dockWidgetContents) + self.write_later_button = QPushButton(self.mapping_controls_layout_widget) self.write_later_button.setObjectName(u"write_later_button") - self.horizontalLayout_6.addWidget(self.write_later_button) + self.horizontalLayout_2.addWidget(self.write_later_button) - self.verticalLayout.addLayout(self.horizontalLayout_6) + self.verticalLayout_8.addLayout(self.horizontalLayout_2) - self.mappings_dock.setWidget(self.dockWidgetContents) - MainWindow.addDockWidget(Qt.LeftDockWidgetArea, self.mappings_dock) - self.mapping_options_dock = QDockWidget(MainWindow) - self.mapping_options_dock.setObjectName(u"mapping_options_dock") - self.mapping_options_contents = QWidget() + self.mapping_options_contents = QFrame(self.mapping_controls_layout_widget) self.mapping_options_contents.setObjectName(u"mapping_options_contents") - self.verticalLayout_7 = QVBoxLayout(self.mapping_options_contents) - self.verticalLayout_7.setSpacing(3) - self.verticalLayout_7.setObjectName(u"verticalLayout_7") - self.verticalLayout_7.setContentsMargins(3, 3, 3, 3) - self.formLayout = QFormLayout() + self.formLayout = QFormLayout(self.mapping_options_contents) self.formLayout.setObjectName(u"formLayout") self.label_4 = QLabel(self.mapping_options_contents) self.label_4.setObjectName(u"label_4") @@ -128,30 +237,20 @@ def setupUi(self, MainWindow): self.item_type_combo_box.addItem("") self.item_type_combo_box.addItem("") self.item_type_combo_box.addItem("") - self.item_type_combo_box.addItem("") - self.item_type_combo_box.addItem("") - self.item_type_combo_box.addItem("") - self.item_type_combo_box.addItem("") - self.item_type_combo_box.addItem("") self.item_type_combo_box.setObjectName(u"item_type_combo_box") self.formLayout.setWidget(0, QFormLayout.FieldRole, self.item_type_combo_box) - self.always_export_header_check_box = QCheckBox(self.mapping_options_contents) - self.always_export_header_check_box.setObjectName(u"always_export_header_check_box") - - self.formLayout.setWidget(1, QFormLayout.SpanningRole, self.always_export_header_check_box) - self.label_8 = QLabel(self.mapping_options_contents) self.label_8.setObjectName(u"label_8") self.formLayout.setWidget(2, QFormLayout.LabelRole, self.label_8) - self.relationship_dimensions_spin_box = QSpinBox(self.mapping_options_contents) - self.relationship_dimensions_spin_box.setObjectName(u"relationship_dimensions_spin_box") - self.relationship_dimensions_spin_box.setMinimum(1) + self.entity_dimensions_spin_box = QSpinBox(self.mapping_options_contents) + self.entity_dimensions_spin_box.setObjectName(u"entity_dimensions_spin_box") + self.entity_dimensions_spin_box.setMinimum(0) - self.formLayout.setWidget(2, QFormLayout.FieldRole, self.relationship_dimensions_spin_box) + self.formLayout.setWidget(2, QFormLayout.FieldRole, self.entity_dimensions_spin_box) self.label_7 = QLabel(self.mapping_options_contents) self.label_7.setObjectName(u"label_7") @@ -190,225 +289,95 @@ def setupUi(self, MainWindow): self.fix_table_name_check_box = QCheckBox(self.mapping_options_contents) self.fix_table_name_check_box.setObjectName(u"fix_table_name_check_box") - self.formLayout.setWidget(6, QFormLayout.LabelRole, self.fix_table_name_check_box) + self.formLayout.setWidget(7, QFormLayout.LabelRole, self.fix_table_name_check_box) self.fix_table_name_line_edit = QLineEdit(self.mapping_options_contents) self.fix_table_name_line_edit.setObjectName(u"fix_table_name_line_edit") - self.formLayout.setWidget(6, QFormLayout.FieldRole, self.fix_table_name_line_edit) + self.formLayout.setWidget(7, QFormLayout.FieldRole, self.fix_table_name_line_edit) + + self.always_export_header_check_box = QCheckBox(self.mapping_options_contents) + self.always_export_header_check_box.setObjectName(u"always_export_header_check_box") + + self.formLayout.setWidget(10, QFormLayout.LabelRole, self.always_export_header_check_box) + + self.compact_button = QPushButton(self.mapping_options_contents) + self.compact_button.setObjectName(u"compact_button") + + self.formLayout.setWidget(10, QFormLayout.FieldRole, self.compact_button) self.label_6 = QLabel(self.mapping_options_contents) self.label_6.setObjectName(u"label_6") - self.formLayout.setWidget(7, QFormLayout.LabelRole, self.label_6) + self.formLayout.setWidget(6, QFormLayout.LabelRole, self.label_6) self.group_fn_combo_box = QComboBox(self.mapping_options_contents) self.group_fn_combo_box.setObjectName(u"group_fn_combo_box") - self.formLayout.setWidget(7, QFormLayout.FieldRole, self.group_fn_combo_box) + self.formLayout.setWidget(6, QFormLayout.FieldRole, self.group_fn_combo_box) - self.verticalLayout_7.addLayout(self.formLayout) + self.verticalLayout_8.addWidget(self.mapping_options_contents) - self.verticalSpacer_3 = QSpacerItem(20, 12, QSizePolicy.Minimum, QSizePolicy.Expanding) - - self.verticalLayout_7.addItem(self.verticalSpacer_3) - - self.mapping_options_dock.setWidget(self.mapping_options_contents) - MainWindow.addDockWidget(Qt.LeftDockWidgetArea, self.mapping_options_dock) - self.mapping_spec_dock = QDockWidget(MainWindow) - self.mapping_spec_dock.setObjectName(u"mapping_spec_dock") - self.mapping_spec_contents = QWidget() - self.mapping_spec_contents.setObjectName(u"mapping_spec_contents") - self.mapping_spec_contents.setEnabled(False) - self.verticalLayout_2 = QVBoxLayout(self.mapping_spec_contents) - self.verticalLayout_2.setSpacing(3) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.verticalLayout_2.setContentsMargins(3, 3, 3, 3) - self.mapping_table_view = QTableView(self.mapping_spec_contents) + self.mapping_table_view = QTableView(self.mapping_controls_layout_widget) self.mapping_table_view.setObjectName(u"mapping_table_view") self.mapping_table_view.setSelectionMode(QAbstractItemView.SingleSelection) self.mapping_table_view.horizontalHeader().setStretchLastSection(True) self.mapping_table_view.verticalHeader().setVisible(False) - self.verticalLayout_2.addWidget(self.mapping_table_view) - - self.horizontalLayout_5 = QHBoxLayout() - self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.verticalLayout_8.addWidget(self.mapping_table_view) - self.horizontalLayout_5.addItem(self.horizontalSpacer) - - self.compact_button = QPushButton(self.mapping_spec_contents) - self.compact_button.setObjectName(u"compact_button") - - self.horizontalLayout_5.addWidget(self.compact_button) - - - self.verticalLayout_2.addLayout(self.horizontalLayout_5) - - self.mapping_spec_dock.setWidget(self.mapping_spec_contents) - MainWindow.addDockWidget(Qt.LeftDockWidgetArea, self.mapping_spec_dock) - self.preview_tables_dock = QDockWidget(MainWindow) - self.preview_tables_dock.setObjectName(u"preview_tables_dock") - self.dockWidgetContents_5 = QWidget() - self.dockWidgetContents_5.setObjectName(u"dockWidgetContents_5") - self.verticalLayout_3 = QVBoxLayout(self.dockWidgetContents_5) - self.verticalLayout_3.setSpacing(3) - self.verticalLayout_3.setObjectName(u"verticalLayout_3") - self.verticalLayout_3.setContentsMargins(3, 3, 3, 3) - self.preview_tree_view = QTreeView(self.dockWidgetContents_5) + self.splitter_2.addWidget(self.mapping_controls_layout_widget) + self.splitter_3.addWidget(self.splitter_2) + self.splitter = QSplitter(self.splitter_3) + self.splitter.setObjectName(u"splitter") + self.splitter.setOrientation(Qt.Horizontal) + self.preview_tree_view = QTreeView(self.splitter) self.preview_tree_view.setObjectName(u"preview_tree_view") + sizePolicy4 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + sizePolicy4.setHorizontalStretch(1) + sizePolicy4.setVerticalStretch(0) + sizePolicy4.setHeightForWidth(self.preview_tree_view.sizePolicy().hasHeightForWidth()) + self.preview_tree_view.setSizePolicy(sizePolicy4) + self.splitter.addWidget(self.preview_tree_view) self.preview_tree_view.header().setVisible(False) - - self.verticalLayout_3.addWidget(self.preview_tree_view) - - self.preview_tables_dock.setWidget(self.dockWidgetContents_5) - MainWindow.addDockWidget(Qt.RightDockWidgetArea, self.preview_tables_dock) - self.preview_contents_dock = QDockWidget(MainWindow) - self.preview_contents_dock.setObjectName(u"preview_contents_dock") - self.dockWidgetContents_6 = QWidget() - self.dockWidgetContents_6.setObjectName(u"dockWidgetContents_6") - self.verticalLayout_4 = QVBoxLayout(self.dockWidgetContents_6) - self.verticalLayout_4.setSpacing(3) - self.verticalLayout_4.setObjectName(u"verticalLayout_4") - self.verticalLayout_4.setContentsMargins(3, 3, 3, 3) - self.preview_table_view = QTableView(self.dockWidgetContents_6) + self.preview_table_view = QTableView(self.splitter) self.preview_table_view.setObjectName(u"preview_table_view") + sizePolicy5 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + sizePolicy5.setHorizontalStretch(5) + sizePolicy5.setVerticalStretch(0) + sizePolicy5.setHeightForWidth(self.preview_table_view.sizePolicy().hasHeightForWidth()) + self.preview_table_view.setSizePolicy(sizePolicy5) + self.splitter.addWidget(self.preview_table_view) + self.splitter_3.addWidget(self.splitter) - self.verticalLayout_4.addWidget(self.preview_table_view) - - self.preview_contents_dock.setWidget(self.dockWidgetContents_6) - MainWindow.addDockWidget(Qt.RightDockWidgetArea, self.preview_contents_dock) - self.export_options_dock = QDockWidget(MainWindow) - self.export_options_dock.setObjectName(u"export_options_dock") - self.export_options_dock.setAllowedAreas(Qt.AllDockWidgetAreas) - self.dockWidgetContents_7 = QWidget() - self.dockWidgetContents_7.setObjectName(u"dockWidgetContents_7") - self.verticalLayout_5 = QVBoxLayout(self.dockWidgetContents_7) - self.verticalLayout_5.setSpacing(3) - self.verticalLayout_5.setObjectName(u"verticalLayout_5") - self.verticalLayout_5.setContentsMargins(3, 3, 3, 3) - self.horizontalLayout_4 = QHBoxLayout() - self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") - self.label = QLabel(self.dockWidgetContents_7) - self.label.setObjectName(u"label") - sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) - self.label.setSizePolicy(sizePolicy) - font = QFont() - font.setPointSize(10) - self.label.setFont(font) - - self.horizontalLayout_4.addWidget(self.label) - - self.export_format_combo_box = QComboBox(self.dockWidgetContents_7) - self.export_format_combo_box.setObjectName(u"export_format_combo_box") - sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) - sizePolicy1.setHorizontalStretch(1) - sizePolicy1.setVerticalStretch(0) - sizePolicy1.setHeightForWidth(self.export_format_combo_box.sizePolicy().hasHeightForWidth()) - self.export_format_combo_box.setSizePolicy(sizePolicy1) - - self.horizontalLayout_4.addWidget(self.export_format_combo_box) - - - self.verticalLayout_5.addLayout(self.horizontalLayout_4) - - self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) - - self.verticalLayout_5.addItem(self.verticalSpacer) - - self.export_options_dock.setWidget(self.dockWidgetContents_7) - MainWindow.addDockWidget(Qt.LeftDockWidgetArea, self.export_options_dock) - self.preview_controls_dock = QDockWidget(MainWindow) - self.preview_controls_dock.setObjectName(u"preview_controls_dock") - self.dockWidgetContents_4 = QWidget() - self.dockWidgetContents_4.setObjectName(u"dockWidgetContents_4") - self.verticalLayout_6 = QVBoxLayout(self.dockWidgetContents_4) - self.verticalLayout_6.setSpacing(3) - self.verticalLayout_6.setObjectName(u"verticalLayout_6") - self.verticalLayout_6.setContentsMargins(3, 3, 3, 3) - self.horizontalLayout_3 = QHBoxLayout() - self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") - self.label_9 = QLabel(self.dockWidgetContents_4) - self.label_9.setObjectName(u"label_9") - self.label_9.setFont(font) - - self.horizontalLayout_3.addWidget(self.label_9) - - self.database_url_combo_box = ElidedCombobox(self.dockWidgetContents_4) - self.database_url_combo_box.setObjectName(u"database_url_combo_box") - sizePolicy2 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - sizePolicy2.setHorizontalStretch(0) - sizePolicy2.setVerticalStretch(0) - sizePolicy2.setHeightForWidth(self.database_url_combo_box.sizePolicy().hasHeightForWidth()) - self.database_url_combo_box.setSizePolicy(sizePolicy2) - self.database_url_combo_box.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) - self.database_url_combo_box.setMinimumContentsLength(0) - - self.horizontalLayout_3.addWidget(self.database_url_combo_box) - - self.load_url_from_fs_button = QToolButton(self.dockWidgetContents_4) - self.load_url_from_fs_button.setObjectName(u"load_url_from_fs_button") - icon = QIcon() - icon.addFile(u":/icons/folder-open-solid.svg", QSize(), QIcon.Normal, QIcon.Off) - self.load_url_from_fs_button.setIcon(icon) - - self.horizontalLayout_3.addWidget(self.load_url_from_fs_button) - + self.verticalLayout_10.addWidget(self.splitter_3) - self.verticalLayout_6.addLayout(self.horizontalLayout_3) - - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.live_preview_check_box = QCheckBox(self.dockWidgetContents_4) - self.live_preview_check_box.setObjectName(u"live_preview_check_box") - self.live_preview_check_box.setChecked(False) - - self.horizontalLayout.addWidget(self.live_preview_check_box) - - self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) - - self.horizontalLayout.addItem(self.horizontalSpacer_3) - - self.label_3 = QLabel(self.dockWidgetContents_4) - self.label_3.setObjectName(u"label_3") - - self.horizontalLayout.addWidget(self.label_3) - - self.max_preview_tables_spin_box = QSpinBox(self.dockWidgetContents_4) - self.max_preview_tables_spin_box.setObjectName(u"max_preview_tables_spin_box") - self.max_preview_tables_spin_box.setMaximum(16777215) - self.max_preview_tables_spin_box.setSingleStep(10) - self.max_preview_tables_spin_box.setValue(20) - - self.horizontalLayout.addWidget(self.max_preview_tables_spin_box) - - self.label_2 = QLabel(self.dockWidgetContents_4) - self.label_2.setObjectName(u"label_2") - - self.horizontalLayout.addWidget(self.label_2) - - self.max_preview_rows_spin_box = QSpinBox(self.dockWidgetContents_4) - self.max_preview_rows_spin_box.setObjectName(u"max_preview_rows_spin_box") - self.max_preview_rows_spin_box.setMaximum(16777215) - self.max_preview_rows_spin_box.setSingleStep(10) - self.max_preview_rows_spin_box.setValue(20) - - self.horizontalLayout.addWidget(self.max_preview_rows_spin_box) - - - self.verticalLayout_6.addLayout(self.horizontalLayout) - - self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) - - self.verticalLayout_6.addItem(self.verticalSpacer_2) - - self.preview_controls_dock.setWidget(self.dockWidgetContents_4) - MainWindow.addDockWidget(Qt.RightDockWidgetArea, self.preview_controls_dock) + MainWindow.setCentralWidget(self.centralwidget) + QWidget.setTabOrder(self.export_format_combo_box, self.live_preview_check_box) + QWidget.setTabOrder(self.live_preview_check_box, self.database_url_combo_box) + QWidget.setTabOrder(self.database_url_combo_box, self.load_url_from_fs_button) + QWidget.setTabOrder(self.load_url_from_fs_button, self.max_preview_tables_spin_box) + QWidget.setTabOrder(self.max_preview_tables_spin_box, self.max_preview_rows_spin_box) + QWidget.setTabOrder(self.max_preview_rows_spin_box, self.mappings_table) + QWidget.setTabOrder(self.mappings_table, self.add_mapping_button) + QWidget.setTabOrder(self.add_mapping_button, self.remove_mapping_button) + QWidget.setTabOrder(self.remove_mapping_button, self.toggle_enabled_button) + QWidget.setTabOrder(self.toggle_enabled_button, self.write_earlier_button) + QWidget.setTabOrder(self.write_earlier_button, self.write_later_button) + QWidget.setTabOrder(self.write_later_button, self.item_type_combo_box) + QWidget.setTabOrder(self.item_type_combo_box, self.entity_dimensions_spin_box) + QWidget.setTabOrder(self.entity_dimensions_spin_box, self.highlight_dimension_spin_box) + QWidget.setTabOrder(self.highlight_dimension_spin_box, self.parameter_type_combo_box) + QWidget.setTabOrder(self.parameter_type_combo_box, self.parameter_dimensions_spin_box) + QWidget.setTabOrder(self.parameter_dimensions_spin_box, self.group_fn_combo_box) + QWidget.setTabOrder(self.group_fn_combo_box, self.fix_table_name_check_box) + QWidget.setTabOrder(self.fix_table_name_check_box, self.fix_table_name_line_edit) + QWidget.setTabOrder(self.fix_table_name_line_edit, self.always_export_header_check_box) + QWidget.setTabOrder(self.always_export_header_check_box, self.compact_button) + QWidget.setTabOrder(self.compact_button, self.mapping_table_view) + QWidget.setTabOrder(self.mapping_table_view, self.preview_tree_view) + QWidget.setTabOrder(self.preview_tree_view, self.preview_table_view) self.retranslateUi(MainWindow) @@ -417,7 +386,16 @@ def setupUi(self, MainWindow): def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) - self.mappings_dock.setWindowTitle(QCoreApplication.translate("MainWindow", u"Mappings", None)) + self.label.setText(QCoreApplication.translate("MainWindow", u"Export format:", None)) + self.live_preview_check_box.setText(QCoreApplication.translate("MainWindow", u"Live preview", None)) + self.label_9.setText(QCoreApplication.translate("MainWindow", u"Database url:", None)) +#if QT_CONFIG(tooltip) + self.load_url_from_fs_button.setToolTip(QCoreApplication.translate("MainWindow", u"

Browse file system

", None)) +#endif // QT_CONFIG(tooltip) + self.load_url_from_fs_button.setText(QCoreApplication.translate("MainWindow", u"...", None)) + self.label_3.setText(QCoreApplication.translate("MainWindow", u"Max. tables", None)) + self.label_2.setText(QCoreApplication.translate("MainWindow", u"Max. content rows:", None)) + self.label_11.setText(QCoreApplication.translate("MainWindow", u"Mappings", None)) self.add_mapping_button.setText(QCoreApplication.translate("MainWindow", u"Add", None)) self.remove_mapping_button.setText(QCoreApplication.translate("MainWindow", u"Remove", None)) #if QT_CONFIG(tooltip) @@ -432,28 +410,18 @@ def retranslateUi(self, MainWindow): self.write_later_button.setToolTip(QCoreApplication.translate("MainWindow", u"Deprioratize mapping.", None)) #endif // QT_CONFIG(tooltip) self.write_later_button.setText(QCoreApplication.translate("MainWindow", u"Write later", None)) - self.mapping_options_dock.setWindowTitle(QCoreApplication.translate("MainWindow", u"Mapping options", None)) - self.label_4.setText(QCoreApplication.translate("MainWindow", u"Item type:", None)) - self.item_type_combo_box.setItemText(0, QCoreApplication.translate("MainWindow", u"Object class", None)) - self.item_type_combo_box.setItemText(1, QCoreApplication.translate("MainWindow", u"Relationship class", None)) - self.item_type_combo_box.setItemText(2, QCoreApplication.translate("MainWindow", u"Relationship class with object parameter", None)) - self.item_type_combo_box.setItemText(3, QCoreApplication.translate("MainWindow", u"Object group", None)) - self.item_type_combo_box.setItemText(4, QCoreApplication.translate("MainWindow", u"Alternative", None)) - self.item_type_combo_box.setItemText(5, QCoreApplication.translate("MainWindow", u"Scenario", None)) - self.item_type_combo_box.setItemText(6, QCoreApplication.translate("MainWindow", u"Scenario alternative", None)) - self.item_type_combo_box.setItemText(7, QCoreApplication.translate("MainWindow", u"Parameter value list", None)) - self.item_type_combo_box.setItemText(8, QCoreApplication.translate("MainWindow", u"Feature", None)) - self.item_type_combo_box.setItemText(9, QCoreApplication.translate("MainWindow", u"Tool", None)) - self.item_type_combo_box.setItemText(10, QCoreApplication.translate("MainWindow", u"Tool feature", None)) - self.item_type_combo_box.setItemText(11, QCoreApplication.translate("MainWindow", u"Tool feature method", None)) - -#if QT_CONFIG(tooltip) - self.always_export_header_check_box.setToolTip(QCoreApplication.translate("MainWindow", u"Export header even when a table is otherwise empty.", None)) -#endif // QT_CONFIG(tooltip) - self.always_export_header_check_box.setText(QCoreApplication.translate("MainWindow", u"Always export header", None)) - self.label_8.setText(QCoreApplication.translate("MainWindow", u"Relationship dimensions:", None)) + self.label_4.setText(QCoreApplication.translate("MainWindow", u"Type:", None)) + self.item_type_combo_box.setItemText(0, QCoreApplication.translate("MainWindow", u"Entity class", None)) + self.item_type_combo_box.setItemText(1, QCoreApplication.translate("MainWindow", u"Entity class with dimension parameter", None)) + self.item_type_combo_box.setItemText(2, QCoreApplication.translate("MainWindow", u"Entity group", None)) + self.item_type_combo_box.setItemText(3, QCoreApplication.translate("MainWindow", u"Alternative", None)) + self.item_type_combo_box.setItemText(4, QCoreApplication.translate("MainWindow", u"Scenario", None)) + self.item_type_combo_box.setItemText(5, QCoreApplication.translate("MainWindow", u"Scenario alternative", None)) + self.item_type_combo_box.setItemText(6, QCoreApplication.translate("MainWindow", u"Parameter value list", None)) + + self.label_8.setText(QCoreApplication.translate("MainWindow", u"Entity dimensions:", None)) #if QT_CONFIG(tooltip) - self.relationship_dimensions_spin_box.setToolTip(QCoreApplication.translate("MainWindow", u"Number of expected relationship dimensions.", None)) + self.entity_dimensions_spin_box.setToolTip(QCoreApplication.translate("MainWindow", u"Number of expected relationship dimensions.", None)) #endif // QT_CONFIG(tooltip) self.label_7.setText(QCoreApplication.translate("MainWindow", u"Selected dimension:", None)) #if QT_CONFIG(tooltip) @@ -469,27 +437,17 @@ def retranslateUi(self, MainWindow): self.parameter_dimensions_spin_box.setToolTip(QCoreApplication.translate("MainWindow", u"Number of expected parameter value dimensions.", None)) #endif // QT_CONFIG(tooltip) self.fix_table_name_check_box.setText(QCoreApplication.translate("MainWindow", u"Fixed table name:", None)) - self.label_6.setText(QCoreApplication.translate("MainWindow", u"Group function:", None)) #if QT_CONFIG(tooltip) - self.group_fn_combo_box.setToolTip(QCoreApplication.translate("MainWindow", u"Group/aggregate data that ends up in the same cell in pivot tables.", None)) + self.always_export_header_check_box.setToolTip(QCoreApplication.translate("MainWindow", u"Export header even when a table is otherwise empty.", None)) #endif // QT_CONFIG(tooltip) - self.mapping_spec_dock.setWindowTitle(QCoreApplication.translate("MainWindow", u"Mapping specification", None)) + self.always_export_header_check_box.setText(QCoreApplication.translate("MainWindow", u"Always export header", None)) #if QT_CONFIG(tooltip) self.compact_button.setToolTip(QCoreApplication.translate("MainWindow", u"Compact mapping by removing empty columns and rows.", None)) #endif // QT_CONFIG(tooltip) self.compact_button.setText(QCoreApplication.translate("MainWindow", u"Compact", None)) - self.preview_tables_dock.setWindowTitle(QCoreApplication.translate("MainWindow", u"Preview tables", None)) - self.preview_contents_dock.setWindowTitle(QCoreApplication.translate("MainWindow", u"Preview contents", None)) - self.export_options_dock.setWindowTitle(QCoreApplication.translate("MainWindow", u"Export options", None)) - self.label.setText(QCoreApplication.translate("MainWindow", u"Format:", None)) - self.preview_controls_dock.setWindowTitle(QCoreApplication.translate("MainWindow", u"Preview controls", None)) - self.label_9.setText(QCoreApplication.translate("MainWindow", u"Database url:", None)) + self.label_6.setText(QCoreApplication.translate("MainWindow", u"Group function:", None)) #if QT_CONFIG(tooltip) - self.load_url_from_fs_button.setToolTip(QCoreApplication.translate("MainWindow", u"

Browse file system

", None)) + self.group_fn_combo_box.setToolTip(QCoreApplication.translate("MainWindow", u"Group/aggregate data that ends up in the same cell in pivot tables.", None)) #endif // QT_CONFIG(tooltip) - self.load_url_from_fs_button.setText(QCoreApplication.translate("MainWindow", u"...", None)) - self.live_preview_check_box.setText(QCoreApplication.translate("MainWindow", u"Live preview", None)) - self.label_3.setText(QCoreApplication.translate("MainWindow", u"Max. tables", None)) - self.label_2.setText(QCoreApplication.translate("MainWindow", u"Max. content rows:", None)) # retranslateUi diff --git a/spine_items/exporter/ui/specification_editor.ui b/spine_items/exporter/ui/specification_editor.ui index 6a9b143a..ac6f2e6c 100644 --- a/spine_items/exporter/ui/specification_editor.ui +++ b/spine_items/exporter/ui/specification_editor.ui @@ -2,6 +2,7 @@ + + Form + + + + 0 + 0 + 527 + 99 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + + + 6 + + + 6 + + + + + 6 + + + 6 + + + + + Basic Console + + + true + + + + + + + Jupyter Console + + + + + + + + + Qt::Vertical + + + + + + + 0 + + + + + 6 + + + 6 + + + + + Executable: + + + + + + + <html><head/><body><p>Julia executable for <span style=" font-weight:700;">Basic Console</span> execution. Leave empty to use the Julia in your PATH environment variable.</p></body></html> + + + Using Julia executable in system path + + + true + + + + + + + <html><head/><body><p>Pick a Julia executable using a file browser</p></body></html> + + + + :/icons/folder-open-solid.svg:/icons/folder-open-solid.svg + + + + + + + + + 6 + + + + + Project: + + + + + + + <html><head/><body><p>Julia environment/project directory</p></body></html> + + + Using Julia default project + + + true + + + + + + + <html><head/><body><p>Pick a Julia project using a file browser</p></body></html> + + + + :/icons/folder-open-solid.svg:/icons/folder-open-solid.svg + + + + + + + + + 6 + + + 6 + + + + + Kernel: + + + + + + + + 0 + 0 + + + + + 100 + 24 + + + + + 16777215 + 24 + + + + <html><head/><body><p>Select a Julia kernel for <span style=" font-weight:700;">Jupyter Console</span></p></body></html> + + + + + + + <html><head/><body><p>Refresh kernel specs list</p></body></html> + + + + + + + :/icons/sync.svg:/icons/sync.svg + + + Qt::ToolButtonIconOnly + + + + + + + + + + + + + radioButton_basic_console + radioButton_jupyter_console + lineEdit_executable + toolButton_browse_julia + lineEdit_julia_project + toolButton_browse_julia_project + comboBox_kernel_specs + toolButton_refresh_kernel_specs + + + + + + diff --git a/spine_items/tool/ui/julia_options.py b/spine_items/tool/ui/julia_options.py index cae2b61f..7c672b1a 100644 --- a/spine_items/tool/ui/julia_options.py +++ b/spine_items/tool/ui/julia_options.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/tool/ui/python_kernel_spec_options.py b/spine_items/tool/ui/python_kernel_spec_options.py index 3994a493..ee5f66cd 100644 --- a/spine_items/tool/ui/python_kernel_spec_options.py +++ b/spine_items/tool/ui/python_kernel_spec_options.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -49,15 +50,16 @@ def setupUi(self, Form): self.horizontalLayout_3 = QHBoxLayout() self.horizontalLayout_3.setSpacing(6) self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.horizontalLayout_3.setContentsMargins(-1, -1, 6, -1) self.verticalLayout = QVBoxLayout() self.verticalLayout.setSpacing(6) self.verticalLayout.setObjectName(u"verticalLayout") self.verticalLayout.setContentsMargins(6, -1, -1, -1) - self.radioButton_python_console = QRadioButton(Form) - self.radioButton_python_console.setObjectName(u"radioButton_python_console") - self.radioButton_python_console.setChecked(True) + self.radioButton_basic_console = QRadioButton(Form) + self.radioButton_basic_console.setObjectName(u"radioButton_basic_console") + self.radioButton_basic_console.setChecked(True) - self.verticalLayout.addWidget(self.radioButton_python_console) + self.verticalLayout.addWidget(self.radioButton_basic_console) self.radioButton_jupyter_console = QRadioButton(Form) self.radioButton_jupyter_console.setObjectName(u"radioButton_jupyter_console") @@ -76,7 +78,7 @@ def setupUi(self, Form): self.verticalLayout_2 = QVBoxLayout() self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.verticalLayout_2.setContentsMargins(-1, -1, 6, -1) + self.verticalLayout_2.setContentsMargins(-1, -1, 0, -1) self.horizontalLayout_2 = QHBoxLayout() self.horizontalLayout_2.setSpacing(6) self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") @@ -86,11 +88,11 @@ def setupUi(self, Form): self.horizontalLayout_2.addWidget(self.label_2) - self.lineEdit_python_path = QLineEdit(Form) - self.lineEdit_python_path.setObjectName(u"lineEdit_python_path") - self.lineEdit_python_path.setClearButtonEnabled(True) + self.lineEdit_executable = QLineEdit(Form) + self.lineEdit_executable.setObjectName(u"lineEdit_executable") + self.lineEdit_executable.setClearButtonEnabled(True) - self.horizontalLayout_2.addWidget(self.lineEdit_python_path) + self.horizontalLayout_2.addWidget(self.lineEdit_executable) self.toolButton_browse_python = QToolButton(Form) self.toolButton_browse_python.setObjectName(u"toolButton_browse_python") @@ -149,22 +151,22 @@ def setupUi(self, Form): # setupUi def retranslateUi(self, Form): - self.radioButton_python_console.setText(QCoreApplication.translate("Form", u"Basic Console", None)) + self.radioButton_basic_console.setText(QCoreApplication.translate("Form", u"Basic Console", None)) self.radioButton_jupyter_console.setText(QCoreApplication.translate("Form", u"Jupyter Console", None)) - self.label_2.setText(QCoreApplication.translate("Form", u"Interpreter", None)) + self.label_2.setText(QCoreApplication.translate("Form", u"Interpreter:", None)) #if QT_CONFIG(tooltip) - self.lineEdit_python_path.setToolTip(QCoreApplication.translate("Form", u"

Python interpreter for executing this Tool specification. Leave empty to select the Python that was used in launching Spine Toolbox.

", None)) + self.lineEdit_executable.setToolTip(QCoreApplication.translate("Form", u"

Python interpreter for Basic Console execution. Leave empty to use the Python that was used in launching Spine Toolbox.

", None)) #endif // QT_CONFIG(tooltip) - self.lineEdit_python_path.setPlaceholderText(QCoreApplication.translate("Form", u"Using current Python interpreter", None)) + self.lineEdit_executable.setPlaceholderText(QCoreApplication.translate("Form", u"Using current Python interpreter", None)) #if QT_CONFIG(tooltip) self.toolButton_browse_python.setToolTip(QCoreApplication.translate("Form", u"

Pick a Python interpreter using a file browser

", None)) #endif // QT_CONFIG(tooltip) - self.label.setText(QCoreApplication.translate("Form", u"Kernel spec", None)) + self.label.setText(QCoreApplication.translate("Form", u"Kernel:", None)) #if QT_CONFIG(tooltip) - self.comboBox_kernel_specs.setToolTip(QCoreApplication.translate("Form", u"

Select a Python Jupyter kernel spec for Jupyter Console.

Both Conda and Jupyter kernel specs are shown.

", None)) + self.comboBox_kernel_specs.setToolTip(QCoreApplication.translate("Form", u"

Select a Python kernel for Jupyter Console

", None)) #endif // QT_CONFIG(tooltip) #if QT_CONFIG(tooltip) - self.toolButton_refresh_kernel_specs.setToolTip(QCoreApplication.translate("Form", u"

Refresh the list of Jupyter and Conda kernel specs

", None)) + self.toolButton_refresh_kernel_specs.setToolTip(QCoreApplication.translate("Form", u"

Refresh kernel specs list

", None)) #endif // QT_CONFIG(tooltip) self.toolButton_refresh_kernel_specs.setText("") pass diff --git a/spine_items/tool/ui/python_kernel_spec_options.ui b/spine_items/tool/ui/python_kernel_spec_options.ui index c09645d6..9072e142 100644 --- a/spine_items/tool/ui/python_kernel_spec_options.ui +++ b/spine_items/tool/ui/python_kernel_spec_options.ui @@ -2,6 +2,7 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spine_items/ui/resources/item_icons/terminal-logo.svg b/spine_items/ui/resources/item_icons/terminal-logo.svg new file mode 100644 index 00000000..fbca8da0 --- /dev/null +++ b/spine_items/ui/resources/item_icons/terminal-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spine_items/ui/resources/resources_icons.qrc b/spine_items/ui/resources/resources_icons.qrc index 243c56fe..0b5236c4 100644 --- a/spine_items/ui/resources/resources_icons.qrc +++ b/spine_items/ui/resources/resources_icons.qrc @@ -1,12 +1,17 @@ + Spine_symbol.png + item_icons/gams-logo.svg + item_icons/terminal-logo.svg + item_icons/julia-logo.svg + item_icons/python-logo.svg + share.svg bolt-lightning.svg item_icons/satellite.svg broom.svg sync.svg file-regular.svg save.svg - Spine_symbol.png copy.svg datapkg.png double-at.svg diff --git a/spine_items/ui/resources/share.svg b/spine_items/ui/resources/share.svg new file mode 100644 index 00000000..e7e262b4 --- /dev/null +++ b/spine_items/ui/resources/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spine_items/ui/url_selector_dialog.py b/spine_items/ui/url_selector_dialog.py index 0ac46af5..f823560d 100644 --- a/spine_items/ui/url_selector_dialog.py +++ b/spine_items/ui/url_selector_dialog.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/ui/url_selector_widget.py b/spine_items/ui/url_selector_widget.py index b8c38383..a3ffdb37 100644 --- a/spine_items/ui/url_selector_widget.py +++ b/spine_items/ui/url_selector_widget.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/utils.py b/spine_items/utils.py index ce2c894d..2c106400 100644 --- a/spine_items/utils.py +++ b/spine_items/utils.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,19 +10,15 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains utilities shared between project items. - -""" +""" Contains utilities shared between project items. """ import os.path from contextlib import suppress - from sqlalchemy import create_engine from sqlalchemy.engine.url import URL, make_url import spinedb_api from spinedb_api.filters.scenario_filter import scenario_name_from_dict from spine_engine.utils.queue_logger import SuppressedMessage -from spinedb_api.helpers import remove_credentials_from_url +from spinedb_api.helpers import remove_credentials_from_url, SUPPORTED_DIALECTS, UNSUPPORTED_DIALECTS class URLError(Exception): @@ -40,39 +37,50 @@ def database_label(provider_name): return "db_url@" + provider_name -def convert_to_sqlalchemy_url(urllib_url, item_name="", logger=None): - """Returns a sqlalchemy url from the url or None if not valid.""" +def convert_to_sqlalchemy_url(url, item_name="", logger=None): + """Returns a sqlalchemy url from url dict or None if not valid. + + Args: + url (dict): URL to convert + item_name (str): project item name for logging + logger (LoggerInterface, optional): a logger + + Returns: + URL: SqlAlchemy URL + """ selections = f"{item_name} selections" if item_name else "selections" if logger is None: logger = _NoLogger() - if not urllib_url: - logger.msg_error.emit(f"No URL specified for {selections}. Please specify one and try again") + if not url: + logger.msg_error.emit(f"No URL specified for {selections}. Please specify one and try again.") return None try: - sa_url = _convert_url(urllib_url) - _validate_sa_url(sa_url, urllib_url["dialect"]) + sa_url = _convert_url(url) + _validate_sa_url(sa_url, url["dialect"]) return sa_url except URLError as error: logger.msg_error.emit(f"Unable to generate URL from {selections}: {error}") return None -def _convert_url(url_dict): +def _convert_url(url): """Converts URL dict to SqlAlchemy URL. Args: - url_dict (dict): URL dictionary + url (dict): URL dictionary Returns: URL: SqlAlchemy URL """ try: - url = {key: value for key, value in url_dict.items() if value} - dialect = url.pop("dialect") + url = {key: value for key, value in url.items() if value} + dialect = url.pop("dialect", None) with suppress(KeyError): del url["schema"] if not dialect: - raise URLError(f"invalid dialect {dialect}.") + raise URLError(f"missing dialect") + if dialect not in set(SUPPORTED_DIALECTS) | set(UNSUPPORTED_DIALECTS): + raise URLError(f"invalid dialect '{dialect}'") if dialect == "sqlite": database = url.get("database", "") if database: diff --git a/spine_items/view/__init__.py b/spine_items/view/__init__.py index fa8d7d1f..1ea034c2 100644 --- a/spine_items/view/__init__.py +++ b/spine_items/view/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -View plugin. - -""" +"""View plugin.""" diff --git a/spine_items/view/commands.py b/spine_items/view/commands.py index 7b36c255..89879fd5 100644 --- a/spine_items/view/commands.py +++ b/spine_items/view/commands.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,32 +10,34 @@ # this program. If not, see . ###################################################################################################################### -""" -Undo/redo commands for the View project item. - -""" +"""Undo/redo commands for the View project item.""" +from copy import deepcopy from spine_items.commands import SpineToolboxCommand class PinOrUnpinDBValuesCommand(SpineToolboxCommand): """Command to pin or unpin DB values.""" - def __init__(self, view, new_values, old_values): + def __init__(self, view_name, new_values, old_values, project): """ Args: - view (View): the View + view_name (str): View's name new_values (dict): mapping name to list of value identifiers old_values (dict): mapping name to list of value identifiers + project (SpineToolboxProject): project """ super().__init__() - self.view = view - self.new_values = new_values - self.old_values = old_values + self._view_name = view_name + self._new_values = deepcopy(new_values) + self._old_values = deepcopy(old_values) + self._project = project texts = ["pin" if values else "unpin" + " " + name for name, values in new_values.items()] - self.setText(f"{', '.join(texts)} in {view.name}") + self.setText(f"{', '.join(texts)} in {view_name}") def redo(self): - self.view.do_pin_db_values(self.new_values) + view = self._project.get_item(self._view_name) + view.do_pin_db_values(deepcopy(self._new_values)) def undo(self): - self.view.do_pin_db_values(self.old_values) + view = self._project.get_item(self._view_name) + view.do_pin_db_values(deepcopy(self._old_values)) diff --git a/spine_items/view/executable_item.py b/spine_items/view/executable_item.py index 7456e741..f4647835 100644 --- a/spine_items/view/executable_item.py +++ b/spine_items/view/executable_item.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains View's executable item as well as support utilities. - -""" +"""Contains View's executable item as well as support utilities.""" from spine_engine.project_item.executable_item_base import ExecutableItemBase from .item_info import ItemInfo diff --git a/spine_items/view/item_info.py b/spine_items/view/item_info.py index 509cd6eb..9a079942 100644 --- a/spine_items/view/item_info.py +++ b/spine_items/view/item_info.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,19 +10,11 @@ # this program. If not, see . ###################################################################################################################### -""" -View project item info. - -""" +"""View project item info.""" from spine_engine.project_item.project_item_info import ProjectItemInfo class ItemInfo(ProjectItemInfo): - @staticmethod - def item_category(): - """See base class.""" - return "Views" - @staticmethod def item_type(): """See base class.""" diff --git a/spine_items/view/ui/__init__.py b/spine_items/view/ui/__init__.py index 5b8c1b9e..12452616 100644 --- a/spine_items/view/ui/__init__.py +++ b/spine_items/view/ui/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/view/ui/view_properties.py b/spine_items/view/ui/view_properties.py index 28c7c063..ddb1bfe9 100644 --- a/spine_items/view/ui/view_properties.py +++ b/spine_items/view/ui/view_properties.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/spine_items/view/view.py b/spine_items/view/view.py index 6af86902..d93487b6 100644 --- a/spine_items/view/view.py +++ b/spine_items/view/view.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Module for view class. - -""" - +"""Module for view class.""" import os from PySide6.QtCore import Qt, Slot, Signal, QObject, QTimer from PySide6.QtGui import QStandardItem, QStandardItemModel, QIcon, QPixmap @@ -60,11 +57,6 @@ def item_type(): """See base class.""" return ItemInfo.item_type() - @staticmethod - def item_category(): - """See base class.""" - return ItemInfo.item_category() - @property def executable_class(self): return ExecutableItem @@ -100,7 +92,7 @@ def open_editor(self, checked=False): db_url_codenames = self._db_url_codenames(indexes) if not db_url_codenames: return - self._toolbox.db_mngr.open_db_editor(db_url_codenames) + self._toolbox.db_mngr.open_db_editor(db_url_codenames, reuse_existing_editor=True) @Slot(bool) def pin_values(self, checked=False): @@ -131,7 +123,7 @@ def reference_resource_label_from_url(self, url): @Slot(str, list) def _pin_db_values(self, name, values): self._toolbox.undo_stack.push( - PinOrUnpinDBValuesCommand(self, {name: values}, {name: self._pinned_values.get(name)}) + PinOrUnpinDBValuesCommand(self.name, {name: values}, {name: self._pinned_values.get(name)}, self._project) ) self._logger.msg.emit(f"{self.name}: Successfully added pin '{name}'") @@ -139,7 +131,10 @@ def unpin_selected_pinned_values(self): names = [index.data() for index in self._properties_ui.treeView_pinned_values.selectedIndexes()] self._toolbox.undo_stack.push( PinOrUnpinDBValuesCommand( - self, {name: None for name in names}, {name: self._pinned_values.get(name) for name in names} + self.name, + {name: None for name in names}, + {name: self._pinned_values.get(name) for name in names}, + self._project, ) ) @@ -151,7 +146,9 @@ def renamed_selected_pinned_value(self): return values = self._pinned_values.get(old_name) self._toolbox.undo_stack.push( - PinOrUnpinDBValuesCommand(self, {old_name: None, new_name: values}, {old_name: values, new_name: None}) + PinOrUnpinDBValuesCommand( + self.name, {old_name: None, new_name: values}, {old_name: values, new_name: None}, self._project + ) ) def do_pin_db_values(self, values_by_name): @@ -193,7 +190,7 @@ def add_to_plot(self, fetch_id, db_map, parameter_record): Args: fetch_id (Any): id given to database fetcher - db_map (DatabaseMappingBase): database map + db_map (DatabaseMapping): database map parameter_record (dict): parameter value's database data """ if not self._data_fetchers: @@ -233,6 +230,9 @@ def _plot_if_all_fetched(self): def _finalize_fetching(self): self._fetched_parameter_values.clear() + for fetcher in self._data_fetchers: + fetcher.set_obsolete(True) + fetcher.deleteLater() self._data_fetchers.clear() self._properties_ui.pushButton_plot_pinned.setEnabled(True) @@ -349,6 +349,12 @@ def item_dict(self): def from_dict(name, item_dict, toolbox, project): description, x, y = ProjectItem.parse_item_dict(item_dict) pinned_values = item_dict.get("pinned_values", dict()) + for values in pinned_values.values(): + for value in values: + pks = value[1] + for pk_key, pk in pks.items(): + if isinstance(pk, list): + pks[pk_key] = tuple(pk) return View(name, description, x, y, toolbox, project, pinned_values=pinned_values) diff --git a/spine_items/view/view_factory.py b/spine_items/view/view_factory.py index 9e4f9d77..8f09dde9 100644 --- a/spine_items/view/view_factory.py +++ b/spine_items/view/view_factory.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -The ViewFactory class. - -""" - +"""The ViewFactory class.""" from PySide6.QtGui import QColor from spinetoolbox.project_item.project_item_factory import ProjectItemFactory from .view import View diff --git a/spine_items/view/view_icon.py b/spine_items/view/view_icon.py index 1f181205..c15dbcf9 100644 --- a/spine_items/view/view_icon.py +++ b/spine_items/view/view_icon.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Module for view icon class. - -""" - +"""Module for view icon class.""" from spinetoolbox.project_item_icon import ProjectItemIcon diff --git a/spine_items/view/widgets/__init__.py b/spine_items/view/widgets/__init__.py index d969db1e..5755cef4 100644 --- a/spine_items/view/widgets/__init__.py +++ b/spine_items/view/widgets/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Widgets for the View project item. - -""" +"""Widgets for the View project item.""" diff --git a/spine_items/view/widgets/add_view_widget.py b/spine_items/view/widgets/add_view_widget.py index f43aa707..f335c585 100644 --- a/spine_items/view/widgets/add_view_widget.py +++ b/spine_items/view/widgets/add_view_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Widget shown to user when a new View is created. - -""" - +"""Widget shown to user when a new View is created.""" from spinetoolbox.widgets.add_project_item_widget import AddProjectItemWidget from ..item_info import ItemInfo from ..view import View diff --git a/spine_items/view/widgets/custom_menus.py b/spine_items/view/widgets/custom_menus.py index 9fa102b3..e69ec71e 100644 --- a/spine_items/view/widgets/custom_menus.py +++ b/spine_items/view/widgets/custom_menus.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Classes for custom context menus and pop-up menus. - -""" - +"""Classes for custom context menus and pop-up menus.""" from spinetoolbox.widgets.custom_menus import CustomContextMenu diff --git a/spine_items/view/widgets/view_properties_widget.py b/spine_items/view/widgets/view_properties_widget.py index af954d70..dc7262a7 100644 --- a/spine_items/view/widgets/view_properties_widget.py +++ b/spine_items/view/widgets/view_properties_widget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -View properties widget. - -""" - +"""View properties widget.""" from PySide6.QtCore import Slot, QPoint from spinetoolbox.widgets.properties_widget import PropertiesWidgetBase from .custom_menus import ViewRefsContextMenu, ViewSelectionsContextMenu diff --git a/spine_items/widgets.py b/spine_items/widgets.py index c8ab1a80..52ef18be 100644 --- a/spine_items/widgets.py +++ b/spine_items/widgets.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,16 +10,14 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains common & shared (Q)widgets. - -""" - +"""Contains common & shared (Q)widgets.""" import os from PySide6.QtCore import Qt, Signal, QUrl, QMimeData, Property, Slot from PySide6.QtWidgets import ( QApplication, QLineEdit, + QStyle, + QStyleOptionComboBox, QTreeView, QStyledItemDelegate, QWidget, @@ -604,3 +603,20 @@ def _browse_sqlite_file(self): self._app_settings, key, self, "Select an SQLite file", APPLICATION_PATH, filter_=filter_ ) return filepath if filepath else None + + +def combo_box_width(font_metric_widget, items): + """Returns section width. + + Args: + font_metric_widget (QWidget): Widget whose font metrics are used + items (Iterable of str): combo box items + + Returns: + int: width of a combo box containing the given items + """ + fm = font_metric_widget.fontMetrics() + style = QApplication.instance().style() + option = QStyleOptionComboBox() + rect = style.subControlRect(QStyle.ComplexControl.CC_ComboBox, option, QStyle.SubControl.SC_ComboBoxArrow) + return max(fm.horizontalAdvance(item) for item in items) + rect.width() diff --git a/tests/__init__.py b/tests/__init__.py index a6daa3ef..b99affe9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Init file for tests.project_items package. Intentionally empty. - -""" +"""Init file for tests.project_items package. Intentionally empty.""" diff --git a/tests/data_connection/__init__.py b/tests/data_connection/__init__.py index 3a5cfcaf..b26df8b5 100644 --- a/tests/data_connection/__init__.py +++ b/tests/data_connection/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Init file for tests.project_items.data_connection package. Intentionally empty. - -""" +"""Init file for tests.project_items.data_connection package. Intentionally empty.""" diff --git a/tests/data_connection/test_DataConnection.py b/tests/data_connection/test_DataConnection.py index 14b6bb66..2d067961 100644 --- a/tests/data_connection/test_DataConnection.py +++ b/tests/data_connection/test_DataConnection.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Data Connection project item. - -""" - +"""Unit tests for Data Connection project item.""" import os from tempfile import TemporaryDirectory from pathlib import Path @@ -23,11 +20,12 @@ from PySide6.QtCore import QItemSelectionModel from PySide6.QtWidgets import QApplication, QDialog, QMessageBox from PySide6.QtGui import Qt +from spinedb_api import create_new_spine_database from spinetoolbox.helpers import signal_waiter -from spine_items.data_connection.data_connection import DataConnection +from spine_items.data_connection.data_connection import _Role, DataConnection from spine_items.data_connection.data_connection_factory import DataConnectionFactory from spine_items.data_connection.item_info import ItemInfo -from ..mock_helpers import ( +from tests.mock_helpers import ( clean_up_toolbox, create_toolboxui_with_project, mock_finish_project_item_construction, @@ -40,9 +38,6 @@ class TestDataConnection(unittest.TestCase): def test_item_type(self): self.assertEqual(DataConnection.item_type(), ItemInfo.item_type()) - def test_item_category(self): - self.assertEqual(DataConnection.item_category(), ItemInfo.item_category()) - class TestDataConnectionWithProject(unittest.TestCase): def setUp(self): @@ -451,6 +446,210 @@ def test_item_dict(self): for k in a: self.assertTrue(k in d, f"Key '{k}' not in dict {d}") + def test_deserialization_with_remote_db_reference(self): + with mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.exec" + ) as url_selector_exec, mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.url_dict" + ) as url_selector_url_dict: + # Add nothing + url_selector_exec.return_value = QDialog.DialogCode.Accepted + url_selector_url_dict.return_value = { + "dialect": "mysql", + "host": "post.com", + "port": 3306, + "database": "db", + "username": "randy", + "password": "creamfraiche", + } + self._data_connection.show_add_db_reference_dialog() + item_dict = self._data_connection.item_dict() + self.assertEqual(len(item_dict["db_references"]), 1) + self.assertNotIn("username", item_dict["db_references"][0]) + self.assertNotIn("password", item_dict["db_references"][0]) + deserialized = DataConnection.from_dict("deserialized", item_dict, self._toolbox, self._toolbox.project()) + self.assertTrue(deserialized.has_db_references()) + self.assertEqual( + list(deserialized.db_reference_iter()), + [ + { + "dialect": "mysql", + "host": "post.com", + "port": 3306, + "database": "db", + "username": "randy", + "password": "creamfraiche", + } + ], + ) + + def test_deserialization_with_sqlite_db_reference_in_project_directory(self): + db_path = Path(self._temp_dir.name, "db.sqlite") + create_new_spine_database("sqlite:///" + str(db_path)) + with mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.exec" + ) as url_selector_exec, mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.url_dict" + ) as url_selector_url_dict: + # Add nothing + url_selector_exec.return_value = QDialog.DialogCode.Accepted + url_selector_url_dict.return_value = { + "dialect": "sqlite", + "host": None, + "port": None, + "database": str(db_path), + "username": None, + "password": None, + } + self._data_connection.show_add_db_reference_dialog() + item_dict = self._data_connection.item_dict() + self.assertEqual(len(item_dict["db_references"]), 1) + self.assertNotIn("username", item_dict["db_references"][0]) + self.assertNotIn("password", item_dict["db_references"][0]) + deserialized = DataConnection.from_dict("deserialized", item_dict, self._toolbox, self._toolbox.project()) + self.assertTrue(deserialized.has_db_references()) + self.assertEqual( + list(deserialized.db_reference_iter()), + [ + { + "dialect": "sqlite", + "host": None, + "port": None, + "database": str(db_path), + "username": None, + "password": None, + } + ], + ) + + def test_sqlite_db_reference_is_marked_missing_when_db_file_is_renamed(self): + db_path = Path(self._temp_dir.name, "db.sqlite") + create_new_spine_database("sqlite:///" + str(db_path)) + with mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.exec" + ) as url_selector_exec, mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.url_dict" + ) as url_selector_url_dict: + # Add nothing + url_selector_exec.return_value = QDialog.DialogCode.Accepted + url_selector_url_dict.return_value = { + "dialect": "sqlite", + "host": None, + "port": None, + "database": str(db_path), + "username": None, + "password": None, + } + self._data_connection.show_add_db_reference_dialog() + while self._data_connection._database_validator.is_busy(): + QApplication.processEvents() + self.assertEqual( + list(self._data_connection.db_reference_iter()), + [ + { + "dialect": "sqlite", + "host": None, + "port": None, + "database": str(db_path), + "username": None, + "password": None, + } + ], + ) + with signal_waiter(self._data_connection.file_system_watcher.file_renamed) as waiter: + db_path.rename(db_path.parent / "renamed.sqlite") + waiter.wait() + self.assertTrue(self._data_connection._db_ref_root.child(0, 0).data(_Role.MISSING)) + + def test_refreshing_missing_sqlite_reference_resurrects_it(self): + db_path = Path(self._temp_dir.name, "db.sqlite") + create_new_spine_database("sqlite:///" + str(db_path)) + with mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.exec" + ) as url_selector_exec, mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.url_dict" + ) as url_selector_url_dict: + # Add nothing + url_selector_exec.return_value = QDialog.DialogCode.Accepted + url_selector_url_dict.return_value = { + "dialect": "sqlite", + "host": None, + "port": None, + "database": str(db_path), + "username": None, + "password": None, + } + self._data_connection.show_add_db_reference_dialog() + while self._data_connection._database_validator.is_busy(): + QApplication.processEvents() + self.assertEqual( + list(self._data_connection.db_reference_iter()), + [ + { + "dialect": "sqlite", + "host": None, + "port": None, + "database": str(db_path), + "username": None, + "password": None, + } + ], + ) + with signal_waiter(self._data_connection.file_system_watcher.file_renamed) as waiter: + renamed_path = db_path.rename(db_path.parent / "renamed.sqlite") + waiter.wait() + self.assertTrue(self._data_connection._db_ref_root.child(0, 0).data(_Role.MISSING)) + with signal_waiter(self._data_connection.file_system_watcher.file_renamed) as waiter: + renamed_path.rename(db_path) + waiter.wait() + self.assertTrue(self._data_connection._db_ref_root.child(0, 0).data(_Role.MISSING)) + self._data_connection.restore_selections() + self._data_connection._connect_signals() + db_ref_root_index = self._ref_model.index(1, 0) + ref_index = self._ref_model.index(0, 0, db_ref_root_index) + self._data_connection._properties_ui.treeView_dc_references.selectionModel().select( + ref_index, QItemSelectionModel.Select + ) + self._data_connection.refresh_references() + while self._data_connection._database_validator.is_busy(): + QApplication.processEvents() + self.assertFalse(self._data_connection._db_ref_root.child(0, 0).data(_Role.MISSING)) + + def test_broken_sqlite_url_marks_the_reference_missing(self): + db_path = Path(self._temp_dir.name, "db.sqlite") + with mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.exec" + ) as url_selector_exec, mock.patch( + "spine_items.data_connection.data_connection.UrlSelectorDialog.url_dict" + ) as url_selector_url_dict: + # Add nothing + url_selector_exec.return_value = QDialog.DialogCode.Accepted + url_selector_url_dict.return_value = { + "dialect": "sqlite", + "host": None, + "port": None, + "database": str(db_path), + "username": None, + "password": None, + } + self._data_connection.show_add_db_reference_dialog() + while self._data_connection._database_validator.is_busy(): + QApplication.processEvents() + self.assertEqual( + list(self._data_connection.db_reference_iter()), + [ + { + "dialect": "sqlite", + "host": None, + "port": None, + "database": str(db_path), + "username": None, + "password": None, + } + ], + ) + self.assertTrue(self._data_connection._db_ref_root.child(0, 0).data(_Role.MISSING)) + def test_notify_destination(self): self._data_connection.logger.msg = MagicMock() self._data_connection.logger.msg_warning = MagicMock() @@ -484,7 +683,7 @@ def test_rename(self): self._data_connection.rename(expected_name, "") # Check name self.assertEqual(expected_name, self._data_connection.name) # item name - self.assertEqual(expected_name, self._data_connection.get_icon().name_item.text()) # name item on Design View + self.assertEqual(expected_name, self._data_connection.get_icon().name()) # Check data_dir self.assertEqual(expected_data_dir, self._data_connection.data_dir) # Check data dir # Check that file_system_watcher has one path (new data_dir) @@ -565,6 +764,7 @@ def setUp(self): self.project = create_mock_project(self._temp_dir.name) self.toolbox.project.return_value = self.project self.data_connection = factory.make_item("DC", item_dict, self.toolbox, self.project) + self.project.get_item.return_value = self.data_connection self._properties_tab = mock_finish_project_item_construction(factory, self.data_connection, self.toolbox) self.ref_model = self.data_connection.reference_model diff --git a/tests/data_connection/test_DataConnectionExecutable.py b/tests/data_connection/test_DataConnectionExecutable.py index ab5238a8..be5065e2 100644 --- a/tests/data_connection/test_DataConnectionExecutable.py +++ b/tests/data_connection/test_DataConnectionExecutable.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for DataConnectionExecutable. - -""" +"""Unit tests for DataConnectionExecutable.""" from multiprocessing import Lock import pathlib import tempfile diff --git a/tests/data_connection/test_ItemInfo.py b/tests/data_connection/test_ItemInfo.py index 81568f58..80db565b 100644 --- a/tests/data_connection/test_ItemInfo.py +++ b/tests/data_connection/test_ItemInfo.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Data Connection's ItemInfo class. - -""" +"""Unit tests for Data Connection's ItemInfo class.""" import unittest from spine_items.data_connection.item_info import ItemInfo @@ -21,9 +19,6 @@ class TestItemInfo(unittest.TestCase): def test_item_type(self): self.assertEqual(ItemInfo.item_type(), "Data Connection") - def test_item_category(self): - self.assertEqual(ItemInfo.item_category(), "Data Connections") - if __name__ == "__main__": unittest.main() diff --git a/tests/data_connection/test_data_connection_icon.py b/tests/data_connection/test_data_connection_icon.py new file mode 100644 index 00000000..3c43a313 --- /dev/null +++ b/tests/data_connection/test_data_connection_icon.py @@ -0,0 +1,45 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +"""Unit tests for DataConnectionIcon class.""" +import unittest +from tempfile import TemporaryDirectory +from PySide6.QtWidgets import QApplication +from tests.mock_helpers import create_toolboxui_with_project, clean_up_toolbox +from spine_items.data_connection.data_connection_factory import DataConnectionFactory + + +class TestDataConnectionIcon(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not QApplication.instance(): + QApplication() + + def setUp(self): + super().setUp() + self._temp_dir = TemporaryDirectory() + self._toolbox = create_toolboxui_with_project(self._temp_dir.name) + item_dict = {"type": "Data Connection", "description": "", "x": 0, "y": 0} + dc = DataConnectionFactory.make_item("DC", item_dict, self._toolbox, self._toolbox.project()) + self._toolbox.project().add_item(dc) + + def tearDown(self): + super().tearDown() + clean_up_toolbox(self._toolbox) + self._temp_dir.cleanup() + + def test_make_data_connection_icon(self): + icon = self._toolbox.project()._project_items["DC"].get_icon() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/data_connection/test_output_resources.py b/tests/data_connection/test_output_resources.py index b33525b9..a7dd5a81 100644 --- a/tests/data_connection/test_output_resources.py +++ b/tests/data_connection/test_output_resources.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,11 +9,11 @@ # 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 unit tests for ``output_resources`` module.""" from pathlib import PurePath import unittest from unittest.mock import MagicMock - from spine_engine.project_item.project_item_resource import url_resource from spine_items.data_connection.output_resources import scan_for_resources @@ -47,5 +48,5 @@ def test_credentials_do_not_show_in_url_resource_label(self): ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/data_store/__init__.py b/tests/data_store/__init__.py index 2749f759..0bf3e9a5 100644 --- a/tests/data_store/__init__.py +++ b/tests/data_store/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Init file for tests.project_items.data_store package. Intentionally empty. - -""" +"""Init file for tests.project_items.data_store package. Intentionally empty.""" diff --git a/tests/data_store/test_DataStoreExecutable.py b/tests/data_store/test_DataStoreExecutable.py index 2b6588c7..496f56ce 100644 --- a/tests/data_store/test_DataStoreExecutable.py +++ b/tests/data_store/test_DataStoreExecutable.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for DataStoreExecutable. - -""" +"""Unit tests for DataStoreExecutable.""" from multiprocessing import Lock from pathlib import Path from tempfile import TemporaryDirectory diff --git a/tests/data_store/test_ItemInfo.py b/tests/data_store/test_ItemInfo.py index f1779466..7cd04a49 100644 --- a/tests/data_store/test_ItemInfo.py +++ b/tests/data_store/test_ItemInfo.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Data Store's ItemInfo class. - -""" +"""Unit tests for Data Store's ItemInfo class.""" import unittest from spine_items.data_store.item_info import ItemInfo @@ -21,9 +19,6 @@ class TestItemInfo(unittest.TestCase): def test_item_type(self): self.assertEqual(ItemInfo.item_type(), "Data Store") - def test_item_category(self): - self.assertEqual(ItemInfo.item_category(), "Data Stores") - if __name__ == "__main__": unittest.main() diff --git a/tests/data_store/test_dataStore.py b/tests/data_store/test_dataStore.py index c78cc737..e635bc87 100644 --- a/tests/data_store/test_dataStore.py +++ b/tests/data_store/test_dataStore.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for DataStore class. - -""" +"""Unit tests for DataStore class.""" from pathlib import Path from tempfile import TemporaryDirectory import unittest @@ -29,7 +27,12 @@ from spine_items.data_store.item_info import ItemInfo from spine_items.utils import convert_to_sqlalchemy_url, database_label from spinetoolbox.helpers import signal_waiter -from ..mock_helpers import mock_finish_project_item_construction, create_mock_project, create_mock_toolbox +from ..mock_helpers import ( + mock_finish_project_item_construction, + create_mock_project, + create_mock_toolbox, + create_toolboxui_with_project, +) class TestDataStore(unittest.TestCase): @@ -37,9 +40,108 @@ def test_item_type(self): """Tests that the item type is correct.""" self.assertEqual(DataStore.item_type(), ItemInfo.item_type()) - def test_item_category(self): - """Tests that the item category is correct.""" - self.assertEqual(DataStore.item_category(), ItemInfo.item_category()) + +class TestDataStoreWithToolbox(unittest.TestCase): + @classmethod + def setUpClass(cls): + """Overridden method. Runs once before all tests in this class.""" + try: + cls.app = QApplication().processEvents() + except RuntimeError: + pass + logging.basicConfig( + stream=sys.stderr, + level=logging.DEBUG, + format="%(asctime)s %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + def setUp(self): + """Set up.""" + self._temp_dir = TemporaryDirectory() + self._toolbox = create_toolboxui_with_project(self._temp_dir.name) + factory = DataStoreFactory() + item_dict = {"type": "Data Store", "description": "", "x": 0, "y": 0, "url": None} + self._project = self._toolbox.project() + with mock.patch("spine_items.data_store.data_store.QMenu"): + self.ds = factory.make_item("DS", item_dict, self._toolbox, self._project) + self._project.add_item(self.ds) + self._properties_widget = mock_finish_project_item_construction(factory, self.ds, self._toolbox) + self.ds_properties_ui = self.ds._properties_ui + + def tearDown(self): + """Overridden method. Runs after each test. + Use this to free resources after a test if needed. + """ + ds_db_path = os.path.join(self.ds.data_dir, "DS.sqlite") + temp_db_path = os.path.join(self.ds.data_dir, "temp_db.sqlite") + self.ds.tear_down() + if os.path.exists(ds_db_path): + try: + os.remove(ds_db_path) + except OSError as os_e: + logging.error("Failed to remove %s. Error: %s", ds_db_path, os_e) + if os.path.exists(temp_db_path): + try: + os.remove(temp_db_path) + except OSError as os_e: + logging.error("Failed to remove %s. Error: %s", temp_db_path, os_e) + self._temp_dir.cleanup() + + def create_temp_db(self): + """Let's create a real db to more easily test complicated stuff (such as opening a tree view).""" + temp_db_path = os.path.join(self.ds.data_dir, "temp_db.sqlite") + sqlite_url = "sqlite:///" + temp_db_path + create_new_spine_database(sqlite_url) + return temp_db_path + + def test_rename(self): + """Tests renaming a Data Store with an existing sqlite db in it's data_dir.""" + temp_path = self.create_temp_db() + url = dict(dialect="sqlite", database=temp_path) + self.ds._url = self.ds.parse_url(url) + self.ds.activate() + # Check that DS is connected to an existing DS.sqlite file that is in data_dir + url = self.ds_properties_ui.url_selector_widget.url_dict() + self.assertEqual(url["dialect"], "sqlite") + self.assertEqual(url["database"], os.path.join(self.ds.data_dir, "temp_db.sqlite")) # data_dir before rename + self.assertTrue(os.path.exists(url["database"])) + expected_name = "ABC" + expected_short_name = "abc" + expected_data_dir = os.path.join(self._project.items_dir, expected_short_name) + self.ds.rename(expected_name, "") # Do rename + # Check name + self.assertEqual(expected_name, self.ds.name) # item name + self.assertEqual(expected_name, self.ds.get_icon().name()) # name item on Design View + # Check data_dir and logs_dir + self.assertEqual(expected_data_dir, self.ds.data_dir) # Check data dir + # Check that the database path in properties has been updated + expected_db_path = os.path.join(expected_data_dir, "temp_db.sqlite") + url = self.ds_properties_ui.url_selector_widget.url_dict() + self.assertEqual(url["database"], expected_db_path) + # Check that the db file has actually been moved + self.assertTrue(os.path.exists(url["database"])) + + def test_dirty_db_notification(self): + """Tests renaming a Data Store with an existing sqlite db in it's data_dir.""" + temp_path = self.create_temp_db() + url = dict(dialect="sqlite", database=temp_path) + self.ds._url = self.ds.parse_url(url) + self.ds.activate() + # Test that there are no notifications + self.ds._check_notifications() + self.assertEqual([], self.ds.get_icon().exclamation_icon._notifications) + # Check that there is a warning about uncommitted changes + db_map = self.ds.get_db_map_for_ds() + self._toolbox.db_mngr.add_entity_classes({db_map: [{"name": "my_object_class"}]}) + self.ds._check_notifications() + self.assertEqual( + [f"{self.ds.name} has uncommitted changes"], self.ds.get_icon().exclamation_icon._notifications + ) + # Check that the warning disappears after committing the changes + self._toolbox.db_mngr.commit_session("Added entity classes", db_map) + self.ds._check_notifications() + self.assertEqual([], self.ds.get_icon().exclamation_icon._notifications) # noinspection PyUnusedLocal @@ -68,6 +170,7 @@ def setUp(self): self.toolbox.project.return_value = self.project with mock.patch("spine_items.data_store.data_store.QMenu"): self.ds = factory.make_item("DS", item_dict, self.toolbox, self.project) + self.project.get_item.return_value = self.ds self._properties_widget = mock_finish_project_item_construction(factory, self.ds, self.toolbox) self.ds_properties_ui = self.ds._properties_ui @@ -252,7 +355,7 @@ def test_open_db_editor1(self): self.ds_properties_ui.pushButton_ds_open_editor.click() sa_url = convert_to_sqlalchemy_url(self.ds.url(), "DS", logger=None) self.assertIsNotNone(sa_url) - self.toolbox.db_mngr.open_db_editor.assert_called_with({sa_url: 'DS'}) + self.toolbox.db_mngr.open_db_editor.assert_called_with({sa_url: "DS"}, True) def test_open_db_editor2(self): """Test that selecting the 'sqlite' dialect, typing the path to an existing db file, @@ -270,7 +373,7 @@ def test_open_db_editor2(self): self.ds_properties_ui.pushButton_ds_open_editor.click() sa_url = convert_to_sqlalchemy_url(self.ds.url(), "DS", logger=None) self.assertIsNotNone(sa_url) - self.toolbox.db_mngr.open_db_editor.assert_called_with({sa_url: 'DS'}) + self.toolbox.db_mngr.open_db_editor.assert_called_with({sa_url: "DS"}, True) def test_notify_destination(self): self.ds.logger.msg = mock.MagicMock() @@ -299,33 +402,6 @@ def test_notify_destination(self): "View and a Data Store has not been implemented yet." ) - def test_rename(self): - """Tests renaming a Data Store with an existing sqlite db in it's data_dir.""" - temp_path = self.create_temp_db() - url = dict(dialect="sqlite", database=temp_path) - self.ds._url = self.ds.parse_url(url) - self.ds.activate() - # Check that DS is connected to an existing DS.sqlite file that is in data_dir - url = self.ds_properties_ui.url_selector_widget.url_dict() - self.assertEqual(url["dialect"], "sqlite") - self.assertEqual(url["database"], os.path.join(self.ds.data_dir, "temp_db.sqlite")) # data_dir before rename - self.assertTrue(os.path.exists(url["database"])) - expected_name = "ABC" - expected_short_name = "abc" - expected_data_dir = os.path.join(self.project.items_dir, expected_short_name) - self.ds.rename(expected_name, "") # Do rename - # Check name - self.assertEqual(expected_name, self.ds.name) # item name - self.assertEqual(expected_name, self.ds.get_icon().name_item.text()) # name item on Design View - # Check data_dir and logs_dir - self.assertEqual(expected_data_dir, self.ds.data_dir) # Check data dir - # Check that the database path in properties has been updated - expected_db_path = os.path.join(expected_data_dir, "temp_db.sqlite") - url = self.ds_properties_ui.url_selector_widget.url_dict() - self.assertEqual(url["database"], expected_db_path) - # Check that the db file has actually been moved - self.assertTrue(os.path.exists(url["database"])) - def test_do_update_url_uses_filterable_resources_when_replacing_them(self): database_1 = os.path.join(self._temp_dir.name, "db1.sqlite") Path(database_1).touch() diff --git a/tests/data_store/test_data_store_icon.py b/tests/data_store/test_data_store_icon.py new file mode 100644 index 00000000..90d568c2 --- /dev/null +++ b/tests/data_store/test_data_store_icon.py @@ -0,0 +1,51 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +"""Unit tests for DataStoreIcon class.""" +import unittest +from unittest import mock +from tempfile import TemporaryDirectory +from PySide6.QtCore import QEvent +from PySide6.QtWidgets import QApplication, QGraphicsSceneMouseEvent +from tests.mock_helpers import create_toolboxui_with_project, clean_up_toolbox +from spine_items.data_store.data_store_factory import DataStoreFactory + + +class TestDataStoreIcon(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not QApplication.instance(): + QApplication() + + def setUp(self): + super().setUp() + self._temp_dir = TemporaryDirectory() + self._toolbox = create_toolboxui_with_project(self._temp_dir.name) + item_dict = {"type": "Data Store", "description": "", "x": 0, "y": 0, "url": None} + ds = DataStoreFactory.make_item("DS", item_dict, self._toolbox, self._toolbox.project()) + self._toolbox.project().add_item(ds) + + def tearDown(self): + super().tearDown() + clean_up_toolbox(self._toolbox) + self._temp_dir.cleanup() + + def test_mouse_double_click_event(self): + icon = self._toolbox.project()._project_items["DS"].get_icon() + with mock.patch("spine_items.data_store.data_store.DataStore._open_spine_db_editor") as mock_open_db_editor: + mock_open_db_editor.return_value = True + icon.mouseDoubleClickEvent(QGraphicsSceneMouseEvent(QEvent.Type.GraphicsSceneMouseDoubleClick)) + mock_open_db_editor.assert_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/data_transformer/__init__.py b/tests/data_transformer/__init__.py index a8012e4a..ff615f31 100644 --- a/tests/data_transformer/__init__.py +++ b/tests/data_transformer/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # 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) diff --git a/tests/data_transformer/mvcmodels/__init__.py b/tests/data_transformer/mvcmodels/__init__.py index f92c66b4..e5f7399f 100644 --- a/tests/data_transformer/mvcmodels/__init__.py +++ b/tests/data_transformer/mvcmodels/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,7 +9,5 @@ # 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 . ###################################################################################################################### -""" -Unit tests for Data Transformer's MVC models. -""" +"""Unit tests for Data Transformer's MVC models.""" diff --git a/tests/data_transformer/mvcmodels/test_class_renames_table_model.py b/tests/data_transformer/mvcmodels/test_class_renames_table_model.py index 30f814e0..8f8f0203 100644 --- a/tests/data_transformer/mvcmodels/test_class_renames_table_model.py +++ b/tests/data_transformer/mvcmodels/test_class_renames_table_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,10 +9,8 @@ # 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 . ###################################################################################################################### -""" -Unit tests for :class:`ClassRenamesTableModel`. -""" +"""Unit tests for :class:`ClassRenamesTableModel`.""" import unittest from PySide6.QtCore import Qt from PySide6.QtGui import QUndoStack @@ -63,5 +62,5 @@ def test_setData(self): self.assertEqual(model.renaming_settings(), {"a": "B"}) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/data_transformer/test_DataTransformer.py b/tests/data_transformer/test_DataTransformer.py index 31ad17da..4b038bae 100644 --- a/tests/data_transformer/test_DataTransformer.py +++ b/tests/data_transformer/test_DataTransformer.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains unit tests for :class:`DataTransformer`. - -""" +"""Contains unit tests for :class:`DataTransformer`.""" import os.path from tempfile import TemporaryDirectory import unittest @@ -23,7 +21,6 @@ from spine_items.data_transformer.data_transformer import DataTransformer from spine_items.data_transformer.data_transformer_factory import DataTransformerFactory from spine_items.data_transformer.data_transformer_specification import DataTransformerSpecification -from spine_items.data_transformer.executable_item import ExecutableItem from spine_items.data_transformer.filter_config_path import filter_config_path from spine_items.data_transformer.item_info import ItemInfo from spine_items.data_transformer.settings import EntityClassRenamingSettings @@ -53,9 +50,6 @@ def setUpClass(cls): def test_item_type(self): self.assertEqual(DataTransformer.item_type(), ItemInfo.item_type()) - def test_item_category(self): - self.assertEqual(DataTransformer.item_category(), ItemInfo.item_category()) - def test_item_dict(self): """Tests Item dictionary creation.""" d = self.transformer.item_dict() @@ -109,7 +103,7 @@ def test_rename(self): expected_short_name = "abc" self.transformer.rename(expected_name, "") self.assertEqual(expected_name, self.transformer.name) - self.assertEqual(expected_name, self.transformer.get_icon().name_item.text()) + self.assertEqual(expected_name, self.transformer.get_icon().name()) expected_data_dir = os.path.join(self.project.items_dir, expected_short_name) self.assertEqual(expected_data_dir, self.transformer.data_dir) diff --git a/tests/data_transformer/test_DataTransformerExecutable.py b/tests/data_transformer/test_DataTransformerExecutable.py index 5d1d1b50..746eb600 100644 --- a/tests/data_transformer/test_DataTransformerExecutable.py +++ b/tests/data_transformer/test_DataTransformerExecutable.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains unit tests for :class:`DataTransformerExecutable`. - -""" +"""Contains unit tests for :class:`DataTransformerExecutable`.""" from multiprocessing import Lock from tempfile import TemporaryDirectory import unittest @@ -76,5 +74,5 @@ def test_skip_execution_with_specification(self): self.assertEqual(transformer.output_resources(ExecutionDirection.FORWARD), [expected_resource]) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/data_transformer/test_DataTransformerSpecification.py b/tests/data_transformer/test_DataTransformerSpecification.py index ad8bbbcc..8a3a4ffe 100644 --- a/tests/data_transformer/test_DataTransformerSpecification.py +++ b/tests/data_transformer/test_DataTransformerSpecification.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Data transformer's :class:`DataTransformerSpecification`. - -""" +"""Unit tests for Data transformer's :class:`DataTransformerSpecification`.""" import unittest from spine_items.data_transformer.data_transformer_specification import DataTransformerSpecification from spine_items.data_transformer.settings import EntityClassRenamingSettings diff --git a/tests/data_transformer/test_ItemInfo.py b/tests/data_transformer/test_ItemInfo.py index 0bf7c615..25058150 100644 --- a/tests/data_transformer/test_ItemInfo.py +++ b/tests/data_transformer/test_ItemInfo.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Data transformer's :class:`ItemInfo`. - -""" +"""Unit tests for Data transformer's :class:`ItemInfo`.""" import unittest from spine_items.data_transformer.item_info import ItemInfo @@ -21,9 +19,6 @@ class TestItemInfo(unittest.TestCase): def test_item_type(self): self.assertEqual(ItemInfo.item_type(), "Data Transformer") - def test_item_category(self): - self.assertEqual(ItemInfo.item_category(), "Manipulators") - if __name__ == "__main__": unittest.main() diff --git a/tests/data_transformer/test_data_transformer_icon.py b/tests/data_transformer/test_data_transformer_icon.py new file mode 100644 index 00000000..ef84c256 --- /dev/null +++ b/tests/data_transformer/test_data_transformer_icon.py @@ -0,0 +1,53 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +"""Unit tests for DataTransformerIcon class.""" +import unittest +from unittest import mock +from tempfile import TemporaryDirectory +from PySide6.QtCore import QEvent +from PySide6.QtWidgets import QApplication, QGraphicsSceneMouseEvent +from tests.mock_helpers import create_toolboxui_with_project, clean_up_toolbox +from spine_items.data_transformer.data_transformer_factory import DataTransformerFactory + + +class TestDataTransformerIcon(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not QApplication.instance(): + QApplication() + + def setUp(self): + super().setUp() + self._temp_dir = TemporaryDirectory() + self._toolbox = create_toolboxui_with_project(self._temp_dir.name) + item_dict = {"type": "Data Transformer", "description": "", "x": 0, "y": 0, "specification": None} + dt = DataTransformerFactory.make_item("DT", item_dict, self._toolbox, self._toolbox.project()) + self._toolbox.project().add_item(dt) + + def tearDown(self): + super().tearDown() + clean_up_toolbox(self._toolbox) + self._temp_dir.cleanup() + + def test_mouse_double_click_event(self): + icon = self._toolbox.project()._project_items["DT"].get_icon() + with mock.patch( + "spine_items.data_transformer.data_transformer.DataTransformer.show_specification_window" + ) as mock_show_spec_window: + mock_show_spec_window.return_value = True + icon.mouseDoubleClickEvent(QGraphicsSceneMouseEvent(QEvent.Type.GraphicsSceneMouseDoubleClick)) + mock_show_spec_window.assert_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/exporter/__init__.py b/tests/exporter/__init__.py index 8095b663..046209e7 100644 --- a/tests/exporter/__init__.py +++ b/tests/exporter/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/tests/exporter/mvcmodels/__init__.py b/tests/exporter/mvcmodels/__init__.py index 8095b663..046209e7 100644 --- a/tests/exporter/mvcmodels/__init__.py +++ b/tests/exporter/mvcmodels/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/tests/exporter/mvcmodels/test_mapping_editor_table_model.py b/tests/exporter/mvcmodels/test_mapping_editor_table_model.py index 051e782a..edd0d780 100644 --- a/tests/exporter/mvcmodels/test_mapping_editor_table_model.py +++ b/tests/exporter/mvcmodels/test_mapping_editor_table_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,16 +9,14 @@ # 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 . ###################################################################################################################### -""" -Unit tests for export mapping setup table. -""" +"""Unit tests for export mapping setup table.""" import unittest from unittest.mock import MagicMock from PySide6.QtWidgets import QApplication from PySide6.QtGui import QUndoStack from spinedb_api.mapping import Position -from spinedb_api.export_mapping import object_export +from spinedb_api.export_mapping import entity_export from spine_items.exporter.mvcmodels.mapping_editor_table_model import MappingEditorTableModel @@ -32,41 +31,41 @@ def setUp(self): def test_columnCount(self): model = MappingEditorTableModel( - "mapping", object_export(Position.hidden, Position.hidden), self._undo_stack, MagicMock() + "mapping", entity_export(Position.hidden, Position.hidden), self._undo_stack, MagicMock() ) self.assertEqual(model.rowCount(), 2) def test_rowCount(self): - mapping_root = object_export(Position.hidden, Position.hidden) + mapping_root = entity_export(Position.hidden, Position.hidden) model = MappingEditorTableModel("mapping", mapping_root, self._undo_stack, MagicMock()) self.assertEqual(model.rowCount(), mapping_root.count_mappings()) def test_data(self): - model = MappingEditorTableModel("mapping", object_export(1, 2), self._undo_stack, MagicMock()) + model = MappingEditorTableModel("mapping", entity_export(1, 2), self._undo_stack, MagicMock()) self.assertEqual(model.rowCount(), 2) - self.assertEqual(model.index(0, 0).data(), "Object classes") + self.assertEqual(model.index(0, 0).data(), "Entity classes") self.assertEqual(model.index(0, 1).data(), "2") - self.assertEqual(model.index(1, 0).data(), "Objects") + self.assertEqual(model.index(1, 0).data(), "Entities") self.assertEqual(model.index(1, 1).data(), "3") def test_setData_column_number(self): model = MappingEditorTableModel( - "mapping", object_export(Position.hidden, Position.hidden), self._undo_stack, MagicMock() + "mapping", entity_export(Position.hidden, Position.hidden), self._undo_stack, MagicMock() ) self.assertTrue(model.setData(model.index(0, 1), "23")) self.assertEqual(model.index(0, 1).data(), "23") def test_setData_prevents_duplicate_table_name_positions(self): - model = MappingEditorTableModel("mapping", object_export(Position.table_name, 0), self._undo_stack, MagicMock()) + model = MappingEditorTableModel("mapping", entity_export(Position.table_name, 0), self._undo_stack, MagicMock()) self.assertEqual(model.rowCount(), 2) - self.assertEqual(model.index(0, 0).data(), "Object classes") + self.assertEqual(model.index(0, 0).data(), "Entity classes") self.assertEqual(model.index(0, 1).data(), "table name") - self.assertEqual(model.index(1, 0).data(), "Objects") + self.assertEqual(model.index(1, 0).data(), "Entities") self.assertEqual(model.index(1, 1).data(), "1") model.setData(model.index(1, 1), "table name") - self.assertEqual(model.index(0, 0).data(), "Object classes") + self.assertEqual(model.index(0, 0).data(), "Entity classes") self.assertEqual(model.index(0, 1).data(), "1") - self.assertEqual(model.index(1, 0).data(), "Objects") + self.assertEqual(model.index(1, 0).data(), "Entities") self.assertEqual(model.index(1, 1).data(), "table name") diff --git a/tests/exporter/test_Exporter.py b/tests/exporter/test_Exporter.py index 677e223f..d1e9d82d 100644 --- a/tests/exporter/test_Exporter.py +++ b/tests/exporter/test_Exporter.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Exporter project item. - -""" +"""Unit tests for Exporter project item.""" import os.path from tempfile import TemporaryDirectory import unittest @@ -26,7 +24,7 @@ from spine_items.exporter.item_info import ItemInfo from spine_items.exporter.specification import OutputFormat, Specification from spine_items.utils import database_label -from spinedb_api import DatabaseMapping +from spinedb_api import create_new_spine_database from spinetoolbox.project_item.logging_connection import LoggingConnection from ..mock_helpers import ( clean_up_toolbox, @@ -69,9 +67,6 @@ def tearDown(self): def test_item_type(self): self.assertEqual(Exporter.item_type(), ItemInfo.item_type()) - def test_item_category(self): - self.assertEqual(Exporter.item_category(), ItemInfo.item_category()) - def test_serialization(self): item_dict = self._exporter.item_dict() deserialized = Exporter.from_dict("new exporter", item_dict, self._toolbox, self._project) @@ -86,7 +81,7 @@ def test_notify_destination(self): source_item.item_type = MagicMock(return_value="Data Connection") self._exporter.notify_destination(source_item) self._exporter.logger.msg_warning.emit.assert_called_with( - 'Link established. Interaction between a Data Connection and a Exporter has not been implemented yet.' + "Link established. Interaction between a Data Connection and a Exporter has not been implemented yet." ) source_item.item_type = MagicMock(return_value="Data Store") self._exporter.notify_destination(source_item) @@ -101,12 +96,12 @@ def test_notify_destination(self): source_item.item_type = MagicMock(return_value="Exporter") self._exporter.notify_destination(source_item) self._exporter.logger.msg_warning.emit.assert_called_with( - 'Link established. Interaction between a Exporter and a Exporter has not been implemented yet.' + "Link established. Interaction between a Exporter and a Exporter has not been implemented yet." ) source_item.item_type = MagicMock(return_value="Importer") self._exporter.notify_destination(source_item) self._exporter.logger.msg_warning.emit.assert_called_with( - 'Link established. Interaction between a Importer and a Exporter has not been implemented yet.' + "Link established. Interaction between a Importer and a Exporter has not been implemented yet." ) source_item.item_type = MagicMock(return_value="Tool") self._exporter.notify_destination(source_item) @@ -126,7 +121,7 @@ def test_rename(self): expected_short_name = shorten(expected_name) self.assertTrue(self._exporter.rename(expected_name, "")) self.assertEqual(expected_name, self._exporter.name) - self.assertEqual(expected_name, self._exporter.get_icon().name_item.text()) + self.assertEqual(expected_name, self._exporter.get_icon().name()) expected_data_dir = os.path.join(self._project.items_dir, expected_short_name) self.assertEqual(expected_data_dir, self._exporter.data_dir) @@ -173,8 +168,7 @@ def test_notifications_when_specification_is_set(self): def test_notifications_when_output_file_name_extension_mismatches_with_specification_output_format(self): project = self._toolbox.project() url = "sqlite:///" + os.path.join(self._temp_dir.name, "db.sqlite") - db_map = DatabaseMapping(url, create=True) - db_map.connection.close() + create_new_spine_database(url) url_dict = {"dialect": "sqlite", "database": os.path.join(self._temp_dir.name, "db.sqlite")} data_store = DataStore("Dummy data store", "", 0.0, 0.0, self._toolbox, project, url_dict) project.add_item(data_store) diff --git a/tests/exporter/test_ItemInfo.py b/tests/exporter/test_ItemInfo.py index e34dfa2a..d068741b 100644 --- a/tests/exporter/test_ItemInfo.py +++ b/tests/exporter/test_ItemInfo.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Exporter's :class:`ItemInfo`. - -""" +"""Unit tests for Exporter's :class:`ItemInfo`.""" import unittest from spine_items.exporter.item_info import ItemInfo @@ -21,9 +19,6 @@ class TestItemInfo(unittest.TestCase): def test_item_type(self): self.assertEqual(ItemInfo.item_type(), "Exporter") - def test_item_category(self): - self.assertEqual(ItemInfo.item_category(), "Exporters") - if __name__ == "__main__": unittest.main() diff --git a/tests/exporter/test_do_work.py b/tests/exporter/test_do_work.py index 92477504..8f601def 100644 --- a/tests/exporter/test_do_work.py +++ b/tests/exporter/test_do_work.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,21 +9,18 @@ # 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 . ###################################################################################################################### -""" -Unit tests for Exporter's :func:`do_work` function. -""" +"""Unit tests for Exporter's :func:`do_work` function.""" from csv import reader import os.path import sqlite3 from tempfile import TemporaryDirectory import unittest from unittest.mock import MagicMock - from spinedb_api import DatabaseMapping, import_object_classes, import_objects from spinedb_api.export_mapping.export_mapping import FixedValueMapping from spinedb_api.export_mapping.group_functions import NoGroup -from spinedb_api.export_mapping import object_export +from spinedb_api.export_mapping import entity_export from spine_items.exporter.do_work import do_work from spine_items.exporter.specification import OutputFormat, Specification, MappingSpecification, MappingType from spinedb_api.mapping import Position @@ -37,17 +35,16 @@ def setUpClass(cls): cls._temp_dir = TemporaryDirectory() db_file = os.path.join(cls._temp_dir.name, "test_db.sqlite") cls._url = "sqlite:///" + db_file - db_map = DatabaseMapping(cls._url, create=True) - try: + with DatabaseMapping(cls._url, create=True) as db_map: import_object_classes(db_map, ("oc1", "oc2")) import_objects(db_map, (("oc1", "o11"), ("oc1", "o12"), ("oc2", "o21"), ("oc2", "o22"), ("oc2", "o23"))) db_map.commit_session("Add test data.") - finally: - db_map.connection.close() def test_export_database(self): - root_mapping = object_export(class_position=0, object_position=1) - mapping_specification = MappingSpecification(MappingType.objects, True, True, NoGroup.NAME, False, root_mapping) + root_mapping = entity_export(entity_class_position=0, entity_position=1) + mapping_specification = MappingSpecification( + MappingType.entities, True, True, NoGroup.NAME, False, root_mapping + ) specification = Specification("name", "description", {"mapping": mapping_specification}) databases = {self._url: "test_export_database.csv"} logger = MagicMock() @@ -63,12 +60,14 @@ def test_export_database(self): self.assertEqual(table, expected) def test_export_to_output_database(self): - object_root = object_export(class_position=0, object_position=1) + object_root = entity_export(entity_class_position=0, entity_position=1) object_root.header = "object_class" object_root.child.header = "object" root_mapping = FixedValueMapping(Position.table_name, "data_table") root_mapping.child = object_root - mapping_specification = MappingSpecification(MappingType.objects, True, True, NoGroup.NAME, False, root_mapping) + mapping_specification = MappingSpecification( + MappingType.entities, True, True, NoGroup.NAME, False, root_mapping + ) specification = Specification("name", "description", {"mapping": mapping_specification}, OutputFormat.SQL) databases = {self._url: "output label"} out_path = os.path.join(self._temp_dir.name, "out_database.sqlite") diff --git a/tests/exporter/test_executable_item.py b/tests/exporter/test_executable_item.py index b8148141..47815d02 100644 --- a/tests/exporter/test_executable_item.py +++ b/tests/exporter/test_executable_item.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,14 +9,13 @@ # 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 . ###################################################################################################################### + """Unit tests for Exporter's ``executable_item`` module.""" import unittest from unittest import mock from pathlib import Path from tempfile import TemporaryDirectory - from PySide6.QtWidgets import QApplication - from spine_engine.project_item.project_item_resource import database_resource from spine_items.exporter.executable_item import ExecutableItem from spine_items.exporter.exporter import Exporter @@ -73,5 +73,5 @@ def test_from_dict(self): self.assertEqual(executable.name, exporter.name) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/exporter/test_exporter_icon.py b/tests/exporter/test_exporter_icon.py new file mode 100644 index 00000000..e160e6c9 --- /dev/null +++ b/tests/exporter/test_exporter_icon.py @@ -0,0 +1,51 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +"""Unit tests for ExporterIcon class.""" +import unittest +from unittest import mock +from tempfile import TemporaryDirectory +from PySide6.QtCore import QEvent +from PySide6.QtWidgets import QApplication, QGraphicsSceneMouseEvent +from tests.mock_helpers import create_toolboxui_with_project, clean_up_toolbox +from spine_items.exporter.exporter_factory import ExporterFactory + + +class TestExporterIcon(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not QApplication.instance(): + QApplication() + + def setUp(self): + super().setUp() + self._temp_dir = TemporaryDirectory() + self._toolbox = create_toolboxui_with_project(self._temp_dir.name) + item_dict = {"type": "Exporter", "description": "", "x": 0, "y": 0, "specification": None} + exp = ExporterFactory.make_item("E", item_dict, self._toolbox, self._toolbox.project()) + self._toolbox.project().add_item(exp) + + def tearDown(self): + super().tearDown() + clean_up_toolbox(self._toolbox) + self._temp_dir.cleanup() + + def test_mouse_double_click_event(self): + icon = self._toolbox.project()._project_items["E"].get_icon() + with mock.patch("spine_items.exporter.exporter.Exporter.show_specification_window") as mock_show_spec_window: + mock_show_spec_window.return_value = True + icon.mouseDoubleClickEvent(QGraphicsSceneMouseEvent(QEvent.Type.GraphicsSceneMouseDoubleClick)) + mock_show_spec_window.assert_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/exporter/test_output_channel.py b/tests/exporter/test_output_channel.py index 5399f8b9..0eef1167 100644 --- a/tests/exporter/test_output_channel.py +++ b/tests/exporter/test_output_channel.py @@ -12,7 +12,6 @@ """Unit tests for the ``output_channel`` module.""" import unittest from pathlib import Path - from spine_items.exporter.output_channel import OutputChannel @@ -79,5 +78,5 @@ def test_database_paths_serialized_as_relative_when_inside_project_dir(self): ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/exporter/test_specification.py b/tests/exporter/test_specification.py index 244b19b3..5df9ca25 100644 --- a/tests/exporter/test_specification.py +++ b/tests/exporter/test_specification.py @@ -12,21 +12,21 @@ """Unit tests for the ''specification'' module""" import unittest from spine_items.exporter.specification import MappingSpecification, MappingType, OutputFormat, Specification -from spinedb_api.export_mapping import object_export +from spinedb_api.export_mapping import entity_export from spinedb_api.mapping import Position class TestSpecification(unittest.TestCase): def test_is_exporting_multiple_files_is_false_when_exporting_single_file(self): - mapping_root = object_export(0, 1) - mapping_specification = MappingSpecification(MappingType.objects, True, False, "", False, mapping_root) + mapping_root = entity_export(0, 1) + mapping_specification = MappingSpecification(MappingType.entities, True, False, "", False, mapping_root) mapping_specifications = {"Only mapping": mapping_specification} specification = Specification(mapping_specifications=mapping_specifications, output_format=OutputFormat.CSV) self.assertFalse(specification.is_exporting_multiple_files()) def test_is_exporting_multiple_files_is_false_when_exporting_csv_with_named_tables(self): - mapping_root = object_export(Position.table_name, 0) - mapping_specification = MappingSpecification(MappingType.objects, True, False, "", False, mapping_root) + mapping_root = entity_export(Position.table_name, 0) + mapping_specification = MappingSpecification(MappingType.entities, True, False, "", False, mapping_root) mapping_specifications = {"Only mapping": mapping_specification} specification = Specification(mapping_specifications=mapping_specifications, output_format=OutputFormat.CSV) self.assertTrue(specification.is_exporting_multiple_files()) @@ -65,5 +65,5 @@ def test_default_format(self): self.assertEqual(OutputFormat.default(), OutputFormat.CSV) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/exporter/test_utils.py b/tests/exporter/test_utils.py index a29ea516..b3648921 100644 --- a/tests/exporter/test_utils.py +++ b/tests/exporter/test_utils.py @@ -8,10 +8,10 @@ # 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 unit tests for Exporter's ``utils`` module.""" import os.path import unittest - from spine_engine.project_item.project_item_resource import url_resource from spine_items.exporter.output_channel import OutputChannel from spine_items.exporter.utils import output_database_resources @@ -31,5 +31,5 @@ def test_creates_url_resources_for_channels_with_urls(self): self.assertEqual(resources, [url_resource(item_name, expected_url, "out database")]) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/exporter/widgets/test_preview_updater.py b/tests/exporter/widgets/test_preview_updater.py index 96e68632..4f8c79ca 100644 --- a/tests/exporter/widgets/test_preview_updater.py +++ b/tests/exporter/widgets/test_preview_updater.py @@ -8,12 +8,11 @@ # 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 . ###################################################################################################################### + """Unit tests for the ``preview_updater`` module.""" import unittest from unittest import mock - from PySide6.QtWidgets import QApplication, QComboBox, QWidget - from spine_items.exporter.mvcmodels.full_url_list_model import FullUrlListModel from spine_items.exporter.widgets.preview_updater import PreviewUpdater @@ -49,5 +48,5 @@ def test_deleting_url_model_does_not_break_tear_down(self): preview_updater.tear_down() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/exporter/widgets/test_specification_editor_window.py b/tests/exporter/widgets/test_specification_editor_window.py index 40d556ce..49ba1420 100644 --- a/tests/exporter/widgets/test_specification_editor_window.py +++ b/tests/exporter/widgets/test_specification_editor_window.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,16 +9,18 @@ # 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 . ###################################################################################################################### + """Unit tests for the ``specification_editor_window`` module.""" +import json +import pathlib from tempfile import TemporaryDirectory import unittest from unittest import mock - from PySide6.QtWidgets import QApplication - from spine_items.exporter.specification import MappingSpecification, MappingType, OutputFormat, Specification from spine_items.exporter.widgets.specification_editor_window import SpecificationEditorWindow -from spinedb_api.export_mapping.export_mapping import FixedValueMapping, ObjectClassMapping +from spinedb_api.export_mapping.export_mapping import FixedValueMapping, EntityClassMapping +from spinedb_api.export_mapping.export_mapping import from_dict as mappings_from_dict from spinedb_api.mapping import Position, unflatten from ...mock_helpers import clean_up_toolbox, create_toolboxui_with_project @@ -56,9 +59,9 @@ def test_mapping_in_table_name_position_disables_fixed_table_name_widgets(self): self.assertEqual(editor._ui.fix_table_name_line_edit.text(), "") def test_mapping_with_fixed_table_enables_the_check_box_and_fills_the_table_name_field(self): - flattened_mappings = [FixedValueMapping(Position.table_name, "nice table name"), ObjectClassMapping(0)] + flattened_mappings = [FixedValueMapping(Position.table_name, "nice table name"), EntityClassMapping(0)] mapping_specification = MappingSpecification( - MappingType.objects, True, True, "", True, unflatten(flattened_mappings) + MappingType.entities, True, True, "", True, unflatten(flattened_mappings) ) specification = Specification("spec name", mapping_specifications={"my mappings": mapping_specification}) editor = SpecificationEditorWindow(self._toolbox, specification) @@ -68,9 +71,9 @@ def test_mapping_with_fixed_table_enables_the_check_box_and_fills_the_table_name self.assertEqual(editor._ui.fix_table_name_line_edit.text(), "nice table name") def test_duplicate_specification(self): - flattened_mappings = [FixedValueMapping(Position.table_name, "nice table name"), ObjectClassMapping(0)] + flattened_mappings = [FixedValueMapping(Position.table_name, "nice table name"), EntityClassMapping(0)] mapping_specification = MappingSpecification( - MappingType.objects, True, True, "", True, unflatten(flattened_mappings) + MappingType.entities, True, True, "", True, unflatten(flattened_mappings) ) specification = Specification("spec name", mapping_specifications={"my mappings": mapping_specification}) editor = SpecificationEditorWindow(self._toolbox, specification) @@ -94,6 +97,53 @@ def test_duplicate_specification(self): ) self.assertEqual(show_duplicate.call_args.kwargs, {}) + def test_forced_decrease_of_selected_dimension_by_entity_dimensions_is_stored_properly(self): + mapping_dicts = [ + {"map_type": "EntityClass", "position": 0, "highlight_position": 1}, + {"map_type": "Dimension", "position": "hidden"}, + {"map_type": "Dimension", "position": "hidden"}, + {"map_type": "ParameterDefinition", "position": 3}, + {"map_type": "ParameterValueList", "position": "hidden", "ignorable": True}, + {"map_type": "Entity", "position": "hidden"}, + {"map_type": "Element", "position": "hidden"}, + {"map_type": "Element", "position": "hidden"}, + {"map_type": "Alternative", "position": 4}, + {"map_type": "ParameterValueType", "position": "hidden"}, + {"map_type": "ParameterValue", "position": 5}, + ] + unflattened_mappings = mappings_from_dict(mapping_dicts) + mapping_specification = MappingSpecification( + MappingType.entity_dimension_parameter_values, True, True, "", True, unflattened_mappings + ) + specification = Specification("spec name", mapping_specifications={"my mappings": mapping_specification}) + specification_path = pathlib.Path(self._temp_dir.name) / "my spec.json" + specification.definition_file_path = str(specification_path) + self._toolbox.project().add_specification(specification, save_to_disk=False) + editor = SpecificationEditorWindow(self._toolbox, specification) + self.assertEqual(editor._ui.highlight_dimension_spin_box.value(), 2) + self.assertEqual(editor._ui.entity_dimensions_spin_box.value(), 2) + editor._spec_toolbar._line_edit_name.setText("my spec name") + editor._spec_toolbar._line_edit_name.editingFinished.emit() + editor._ui.entity_dimensions_spin_box.setValue(1) + self.assertEqual(editor._ui.entity_dimensions_spin_box.value(), 1) + self.assertEqual(editor._ui.highlight_dimension_spin_box.value(), 1) + editor.spec_toolbar().save_action.trigger() + with open(specification_path) as specification_file: + loaded_specification = Specification.from_dict(json.load(specification_file)) + self.assertEqual(loaded_specification.name, "my spec name") + expected_dicts = [ + {"map_type": "EntityClass", "position": 0, "highlight_position": 0}, + {"map_type": "Dimension", "position": "hidden"}, + {"map_type": "ParameterDefinition", "position": 3}, + {"map_type": "ParameterValueList", "position": "hidden", "ignorable": True}, + {"map_type": "Entity", "position": "hidden"}, + {"map_type": "Element", "position": "hidden"}, + {"map_type": "Alternative", "position": 4}, + {"map_type": "ParameterValueType", "position": "hidden"}, + {"map_type": "ParameterValue", "position": 5}, + ] + self.assertEqual(loaded_specification.mapping_specifications()["my mappings"].to_dict()["root"], expected_dicts) + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/importer/__init__.py b/tests/importer/__init__.py index 13a51d0e..ee5fd752 100644 --- a/tests/importer/__init__.py +++ b/tests/importer/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Init file for tests.project_items.importers package. Intentionally empty. - -""" +"""Init file for tests.project_items.importers package. Intentionally empty.""" diff --git a/tests/importer/mvcmodels/__init__.py b/tests/importer/mvcmodels/__init__.py index 75a6c2f7..547cfe61 100644 --- a/tests/importer/mvcmodels/__init__.py +++ b/tests/importer/mvcmodels/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # 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) @@ -8,6 +9,5 @@ # 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 unit tests for Import editor's MVC models. -""" + +"""Contains unit tests for Import editor's MVC models.""" diff --git a/tests/importer/mvcmodels/test_SourceDataTableModel.py b/tests/importer/mvcmodels/test_SourceDataTableModel.py index 24c8ad6c..f8468364 100644 --- a/tests/importer/mvcmodels/test_SourceDataTableModel.py +++ b/tests/importer/mvcmodels/test_SourceDataTableModel.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # 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) @@ -8,13 +9,11 @@ # 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 unit tests for Import editor's SourceDataTableModel. -""" + +"""Contains unit tests for Import editor's SourceDataTableModel.""" import unittest from unittest.mock import MagicMock from PySide6.QtCore import Qt - from spinedb_api.import_mapping.type_conversion import value_to_convert_spec from spinedb_api.import_mapping.import_mapping_compat import import_mapping_from_dict from spine_items.importer.mvcmodels.mappings_model_roles import Role @@ -31,13 +30,13 @@ def tearDown(self): def test_column_type_checking(self): self._model.reset_model([["1", "0h", "2018-01-01 00:00"], ["2", "1h", "2018-01-01 00:00"]]) - self._model.set_type(0, value_to_convert_spec('float')) + self._model.set_type(0, value_to_convert_spec("float")) self.assertEqual(self._model._column_type_errors, {}) self.assertEqual(self._model._row_type_errors, {}) - self._model.set_type(1, value_to_convert_spec('duration')) + self._model.set_type(1, value_to_convert_spec("duration")) self.assertEqual(self._model._column_type_errors, {}) self.assertEqual(self._model._row_type_errors, {}) - self._model.set_type(2, value_to_convert_spec('datetime')) + self._model.set_type(2, value_to_convert_spec("datetime")) self.assertEqual(self._model._column_type_errors, {}) self.assertEqual(self._model._row_type_errors, {}) @@ -45,19 +44,19 @@ def test_row_type_checking(self): self._model.reset_model( [["1", "1", "1.1"], ["2h", "1h", "2h"], ["2018-01-01 00:00", "2018-01-01 00:00", "2018-01-01 00:00"]] ) - self._model.set_type(0, value_to_convert_spec('float'), orientation=Qt.Orientation.Vertical) + self._model.set_type(0, value_to_convert_spec("float"), orientation=Qt.Orientation.Vertical) self.assertEqual(self._model._column_type_errors, {}) self.assertEqual(self._model._row_type_errors, {}) - self._model.set_type(1, value_to_convert_spec('duration'), orientation=Qt.Orientation.Vertical) + self._model.set_type(1, value_to_convert_spec("duration"), orientation=Qt.Orientation.Vertical) self.assertEqual(self._model._column_type_errors, {}) self.assertEqual(self._model._row_type_errors, {}) - self._model.set_type(2, value_to_convert_spec('datetime'), orientation=Qt.Orientation.Vertical) + self._model.set_type(2, value_to_convert_spec("datetime"), orientation=Qt.Orientation.Vertical) self.assertEqual(self._model._column_type_errors, {}) self.assertEqual(self._model._row_type_errors, {}) def test_column_type_checking_produces_error(self): self._model.reset_model([["Not a valid number", "2.4"], ["1", "3"]]) - self._model.set_type(0, value_to_convert_spec('float')) + self._model.set_type(0, value_to_convert_spec("float")) error_index = (0, 0) self.assertEqual(len(self._model._column_type_errors), 1) self.assertEqual(self._model._row_type_errors, {}) @@ -81,7 +80,7 @@ def test_column_type_checking_produces_error(self): def test_row_type_checking_produces_error(self): self._model.reset_model([["1", "2.4"], ["Not a valid number", "3"]]) - self._model.set_type(1, value_to_convert_spec('float'), orientation=Qt.Orientation.Vertical) + self._model.set_type(1, value_to_convert_spec("float"), orientation=Qt.Orientation.Vertical) error_index = (1, 0) self.assertEqual(len(self._model._row_type_errors), 1) self.assertEqual(self._model._column_type_errors, {}) @@ -106,7 +105,7 @@ def test_mapping_column_colors(self): mappings_model = MappingsModel(undo_stack, None) list_index = self._add_mapping(mappings_model, {"map_type": "ObjectClass", "name": 0}) self._model.set_mapping_list_index(list_index) - entity_class_color = self._find_color(list_index, "Object class names") + entity_class_color = self._find_color(list_index, "Entity class names") self.assertEqual( self._model.data(self._model.index(0, 0), role=Qt.ItemDataRole.BackgroundRole), entity_class_color ) @@ -116,7 +115,7 @@ def test_mapping_column_colors(self): # row not showing color if the start reading row is specified list_index = self._add_mapping(mappings_model, {"map_type": "ObjectClass", "name": 0, "read_start_row": 1}) self._model.set_mapping_list_index(list_index) - entity_class_color = self._find_color(list_index, "Object class names") + entity_class_color = self._find_color(list_index, "Entity class names") self.assertEqual(self._model.data(self._model.index(0, 0), role=Qt.ItemDataRole.BackgroundRole), None) self.assertEqual( self._model.data(self._model.index(1, 0), role=Qt.ItemDataRole.BackgroundRole), entity_class_color @@ -143,8 +142,8 @@ def test_mapping_pivoted_colors(self): mappings_model, {"map_type": "ObjectClass", "object": {"map_type": "row", "value_reference": 0}} ) self._model.set_mapping_list_index(list_index) - entity_color = self._find_color(list_index, "Object names") - metadata_color = self._find_color(list_index, "Object metadata") + entity_color = self._find_color(list_index, "Entity names") + metadata_color = self._find_color(list_index, "Entity metadata") self.assertEqual(self._model.data(self._model.index(0, 0), role=Qt.ItemDataRole.BackgroundRole), entity_color) self.assertEqual(self._model.data(self._model.index(0, 1), role=Qt.ItemDataRole.BackgroundRole), entity_color) self.assertEqual(self._model.data(self._model.index(1, 0), role=Qt.ItemDataRole.BackgroundRole), metadata_color) @@ -155,7 +154,7 @@ def test_mapping_pivoted_colors(self): {"map_type": "ObjectClass", "object": {"map_type": "row", "value_reference": 0}, "skip_columns": [0]}, ) self._model.set_mapping_list_index(list_index) - entity_color = self._find_color(list_index, "Object names") + entity_color = self._find_color(list_index, "Entity names") self.assertEqual(self._model.data(self._model.index(0, 0), role=Qt.ItemDataRole.BackgroundRole), None) self.assertEqual(self._model.data(self._model.index(0, 1), role=Qt.ItemDataRole.BackgroundRole), entity_color) self.assertEqual(self._model.data(self._model.index(1, 0), role=Qt.ItemDataRole.BackgroundRole), None) @@ -170,9 +169,9 @@ def test_mapping_column_and_pivot_colors(self): mappings_model, {"map_type": "ObjectClass", "name": 0, "object": {"map_type": "row", "value_reference": 0}} ) self._model.set_mapping_list_index(list_index) - entity_class_color = self._find_color(list_index, "Object class names") - entity_color = self._find_color(list_index, "Object names") - metadata_color = self._find_color(list_index, "Object metadata") + entity_class_color = self._find_color(list_index, "Entity class names") + entity_color = self._find_color(list_index, "Entity names") + metadata_color = self._find_color(list_index, "Entity metadata") self.assertEqual(self._model.data(self._model.index(0, 0), role=Qt.ItemDataRole.BackgroundRole), None) self.assertEqual(self._model.data(self._model.index(0, 1), role=Qt.ItemDataRole.BackgroundRole), entity_color) self.assertEqual( @@ -203,7 +202,7 @@ def test_mapping_column_and_pivot_colors_with_value_mapping_position_set_to_rand ) self._model.set_mapping_list_index(list_index) # no color showing where row and column mapping intersect - entity_color = self._find_color(list_index, "Object names") + entity_color = self._find_color(list_index, "Entity names") parameter_definition_color = self._find_color(list_index, "Parameter names") alternative_color = self._find_color(list_index, "Alternative names") index_color = self._find_color(list_index, "Parameter indexes") @@ -244,7 +243,7 @@ def test_mapping_column_and_pivot_colors_with_value_mapping_position_set_to_colu ) self._model.set_mapping_list_index(list_index) # no color showing where row and column mapping intersect - entity_color = self._find_color(list_index, "Object names") + entity_color = self._find_color(list_index, "Entity names") parameter_definition_color = self._find_color(list_index, "Parameter names") alternative_color = self._find_color(list_index, "Alternative names") index_color = self._find_color(list_index, "Parameter indexes") @@ -277,5 +276,5 @@ def _add_mapping(mappings_model, mapping_dict): return mappings_model.index(0, 0, table_index) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/importer/mvcmodels/test_mappings_model.py b/tests/importer/mvcmodels/test_mappings_model.py index 36caad64..efd839df 100644 --- a/tests/importer/mvcmodels/test_mappings_model.py +++ b/tests/importer/mvcmodels/test_mappings_model.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # 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) @@ -8,17 +9,15 @@ # 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 unit tests for Import editor's :class:`MappingsModel`. -""" + +"""Contains unit tests for Import editor's :class:`MappingsModel`.""" import unittest from PySide6.QtCore import QObject, Qt from PySide6.QtWidgets import QApplication from PySide6.QtGui import QUndoStack - from spine_items.importer.mvcmodels.mappings_model_roles import Role -from spinetoolbox.helpers import signal_waiter from spine_items.importer.mvcmodels.mappings_model import MappingsModel +from spinetoolbox.helpers import signal_waiter from spinedb_api import import_mapping_from_dict @@ -41,7 +40,7 @@ def test_empty_model_has_Select_All_item(self): self.assertEqual(model.rowCount(), 1) self.assertEqual(model.columnCount(), 1) index = model.index(0, 0) - self.assertEqual(index.data(), "Select All") + self.assertEqual(index.data(), "Select all") def test_set_time_series_repeat_flag(self): model = MappingsModel(self._undo_stack, self._model_parent) @@ -64,6 +63,76 @@ def test_set_time_series_repeat_flag(self): waiter.wait() self.assertTrue(flattened_mappings.value_mapping().options["repeat"]) + def test_change_mappings_type(self): + model = MappingsModel(self._undo_stack, self._model_parent) + model.restore( + { + "table_mappings": { + "Sheet1": [ + { + "Mapping 1": { + "mapping": [ + {"map_type": "EntityClass", "position": "hidden", "value": "Object"}, + {"map_type": "Entity", "position": 0}, + {"map_type": "EntityMetadata", "position": "hidden"}, + {"map_type": "ParameterDefinition", "position": "hidden", "value": "size"}, + {"map_type": "Alternative", "position": "hidden", "value": "Base"}, + {"map_type": "ParameterValueMetadata", "position": "hidden"}, + {"map_type": "ParameterValue", "position": 1}, + ] + } + } + ] + }, + "selected_tables": ["Sheet1"], + "table_options": {"Sheet1": {}}, + "table_types": {"Sheet1": {"0": "string", "1": "float"}}, + "table_default_column_type": {}, + "table_row_types": {}, + "source_type": "ExcelConnector", + } + ) + self.assertEqual(model.index(0, 0).data(), "Select all") + table_index = model.index(1, 0) + self.assertEqual(table_index.data(), "Sheet1") + list_index = model.index(0, 0, table_index) + self.assertEqual(list_index.data(), "Mapping 1") + expected = [ + ["Entity class names", "Constant", "Object", ""], + ["Entity names", "Column", 1, ""], + ["Entity metadata", "None", None, ""], + ["Parameter names", "Constant", "size", ""], + ["Alternative names", "Constant", "Base", ""], + ["Parameter value metadata", "None", None, ""], + ["Parameter values", "Column", 2, ""], + ] + rows = model.rowCount(list_index) + self.assertEqual(rows, len(expected)) + for row in range(model.rowCount(list_index)): + expected_row = expected[row] + columns = model.columnCount(list_index) + self.assertEqual(columns, len(expected_row)) + for column in range(columns): + with self.subTest(row=row, column=column): + index = model.index(row, column, list_index) + self.assertEqual(index.data(), expected_row[column]) + model.set_mappings_type(1, 0, "Entity group") + expected = [ + ["Entity class names", "None", None, ""], + ["Group names", "None", None, ""], + ["Member names", "None", None, ""], + ] + rows = model.rowCount(list_index) + self.assertEqual(rows, len(expected)) + for row in range(model.rowCount(list_index)): + expected_row = expected[row] + columns = model.columnCount(list_index) + self.assertEqual(columns, len(expected_row)) + for column in range(columns): + with self.subTest(row=row, column=column): + index = model.index(row, column, list_index) + self.assertEqual(index.data(), expected_row[column]) + class TestTableList(unittest.TestCase): @classmethod @@ -117,7 +186,7 @@ def test_remove_tables_not_in_source_and_specification(self): ) self._model.remove_tables_not_in_source_and_specification() self.assertEqual(self._model.rowCount(), 3) - expected_names = ["Select All", "table that is only in source", "table that is in source and specification"] + expected_names = ["Select all", "table that is only in source", "table that is in source and specification"] for row, expected_name in zip(range(self._model.rowCount()), expected_names): item = self._model.index(row, 0).data(Role.ITEM) self.assertEqual(item.name, expected_name) @@ -140,15 +209,13 @@ def test_cross_check_source_table_names(self): def test_empty_model_has_select_all_source_table_item(self): self.assertEqual(self._model.rowCount(), 1) index = self._model.index(0, 0) - self.assertEqual(index.data(), "Select All") + self.assertEqual(index.data(), "Select all") self.assertIsNone(index.data(Qt.ItemDataRole.ForegroundRole)) - self.assertFalse(index.data(Qt.ItemDataRole.CheckStateRole).value) + self.assertEqual(index.data(Qt.ItemDataRole.CheckStateRole), Qt.CheckState.Checked) self.assertIsNone(index.data(Qt.ItemDataRole.FontRole)) self.assertIsNone(index.data(Qt.ItemDataRole.ToolTipRole)) flags = self._model.flags(index) - self.assertEqual( - flags, Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsUserCheckable - ) + self.assertEqual(flags, Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable) def test_empty_row(self): self._model.add_empty_row() @@ -173,10 +240,11 @@ def test_turn_empty_row_into_non_real_table(self): index = self._model.index(1, 0) self.assertEqual(index.data(), "my shiny table (new)") self.assertIsNone(index.data(Qt.ItemDataRole.ForegroundRole)) - self.assertTrue(index.data(Qt.ItemDataRole.CheckStateRole).value) + self.assertEqual(index.data(Qt.ItemDataRole.CheckStateRole), Qt.CheckState.Checked) self.assertIsNone(index.data(Qt.ItemDataRole.FontRole)) self.assertEqual( - index.data(Qt.ItemDataRole.ToolTipRole), "Table's mappings haven't been saved with the specification yet." + index.data(Qt.ItemDataRole.ToolTipRole), + "Table's mappings haven't been saved with the specification yet.", ) flags = self._model.flags(index) self.assertEqual( @@ -189,9 +257,24 @@ def test_turn_empty_row_into_non_real_table(self): index = self._model.index(2, 0) self._assert_empty_row(index) + def test_when_empty_row_turns_into_non_real_table_its_check_status_equals_select_all(self): + self._model.add_empty_row() + index = self._model.index(0, 0) + self.assertTrue(self._model.setData(index, Qt.CheckState.Unchecked.value, Qt.ItemDataRole.CheckStateRole)) + empty_row_index = self._model.index(1, 0) + with signal_waiter(self._model.dataChanged, timeout=1.0) as waiter: + self._model.setData(empty_row_index, "my shiny table") + waiter.wait() + self.assertEqual(self._model.rowCount(), 3) + index = self._model.index(1, 0) + self.assertEqual(index.data(), "my shiny table (new)") + self.assertEqual(index.data(Qt.ItemDataRole.CheckStateRole), Qt.CheckState.Unchecked) + index = self._model.index(2, 0) + self._assert_empty_row(index) + def _assert_empty_row(self, index): self.assertEqual(index.data(), "") - self.assertFalse(index.data(Qt.ItemDataRole.CheckStateRole).value) + self.assertEqual(index.data(Qt.ItemDataRole.CheckStateRole), Qt.CheckState.Unchecked) self.assertIsNone(index.data(Qt.ItemDataRole.ForegroundRole)) self.assertTrue(index.data(Qt.ItemDataRole.FontRole).italic()) flags = self._model.flags(index) @@ -240,11 +323,11 @@ def test_data_when_mapping_object_class_without_objects_or_parameters(self): self.assertEqual(self._model.rowCount(self._list_index), 3) self.assertEqual(self._model.columnCount(self._list_index), 4) index = self._model.index(0, 0, self._list_index) - self.assertEqual(index.data(), "Object class names") + self.assertEqual(index.data(), "Entity class names") index = self._model.index(1, 0, self._list_index) - self.assertEqual(index.data(), "Object names") + self.assertEqual(index.data(), "Entity names") index = self._model.index(2, 0, self._list_index) - self.assertEqual(index.data(), "Object metadata") + self.assertEqual(index.data(), "Entity metadata") index = self._model.index(0, 1, self._list_index) self.assertEqual(index.data(), "None") index = self._model.index(1, 1, self._list_index) @@ -270,7 +353,7 @@ def test_data_when_mapping_object_class_without_objects_or_parameters(self): def test_data_when_mapping_invalid_object_class_with_parameters(self): indexed_parameter_mapping_dict = { "map_type": "parameter", - "name": {'map_type': None}, + "name": {"map_type": None}, "parameter_type": "map", "value": {"map_type": None}, "extra_dimensions": [{"map_type": None}], @@ -287,11 +370,11 @@ def test_data_when_mapping_invalid_object_class_with_parameters(self): self.assertEqual(self._model.rowCount(self._list_index), 9) self.assertEqual(self._model.columnCount(self._list_index), 4) index = self._model.index(0, 0, self._list_index) - self.assertEqual(index.data(), "Object class names") + self.assertEqual(index.data(), "Entity class names") index = self._model.index(1, 0, self._list_index) - self.assertEqual(index.data(), "Object names") + self.assertEqual(index.data(), "Entity names") index = self._model.index(2, 0, self._list_index) - self.assertEqual(index.data(), "Object metadata") + self.assertEqual(index.data(), "Entity metadata") index = self._model.index(3, 0, self._list_index) self.assertEqual(index.data(), "Parameter names") index = self._model.index(4, 0, self._list_index) @@ -327,11 +410,11 @@ def test_data_when_mapping_valid_object_class_with_pivoted_parameters(self): self.assertEqual(self._model.rowCount(self._list_index), 8) self.assertEqual(self._model.columnCount(self._list_index), 4) index = self._model.index(0, 0, self._list_index) - self.assertEqual(index.data(), "Object class names") + self.assertEqual(index.data(), "Entity class names") index = self._model.index(1, 0, self._list_index) - self.assertEqual(index.data(), "Object names") + self.assertEqual(index.data(), "Entity names") index = self._model.index(2, 0, self._list_index) - self.assertEqual(index.data(), "Object metadata") + self.assertEqual(index.data(), "Entity metadata") index = self._model.index(3, 0, self._list_index) self.assertEqual(index.data(), "Parameter names") index = self._model.index(4, 0, self._list_index) @@ -396,7 +479,7 @@ def test_data_when_mapping_valid_object_class_with_pivoted_parameters(self): def test_data_when_mapping_valid_object_class_with_parameters(self): indexed_parameter_mapping_dict = { "map_type": "parameter", - "name": {'map_type': 'column', 'reference': 99}, + "name": {"map_type": "column", "reference": 99}, "parameter_type": "map", "value": {"reference": 23, "map_type": "column"}, "extra_dimensions": [{"reference": "fifth column", "map_type": "column"}], @@ -415,11 +498,11 @@ def test_data_when_mapping_valid_object_class_with_parameters(self): self.assertEqual(self._model.rowCount(self._list_index), 9) self.assertEqual(self._model.columnCount(self._list_index), 4) index = self._model.index(0, 0, self._list_index) - self.assertEqual(index.data(), "Object class names") + self.assertEqual(index.data(), "Entity class names") index = self._model.index(1, 0, self._list_index) - self.assertEqual(index.data(), "Object names") + self.assertEqual(index.data(), "Entity names") index = self._model.index(2, 0, self._list_index) - self.assertEqual(index.data(), "Object metadata") + self.assertEqual(index.data(), "Entity metadata") index = self._model.index(3, 0, self._list_index) self.assertEqual(index.data(), "Parameter names") index = self._model.index(4, 0, self._list_index) @@ -492,7 +575,7 @@ def test_data_when_mapping_valid_object_class_with_parameters(self): def test_data_when_valid_object_class_with_nested_map(self): indexed_parameter_mapping_dict = { "map_type": "parameter", - "name": {'map_type': 'column', 'reference': 99}, + "name": {"map_type": "column", "reference": 99}, "parameter_type": "map", "value": {"reference": 23, "map_type": "column"}, "extra_dimensions": [ @@ -514,11 +597,11 @@ def test_data_when_valid_object_class_with_nested_map(self): self.assertEqual(self._model.rowCount(self._list_index), 11) self.assertEqual(self._model.columnCount(self._list_index), 4) index = self._model.index(0, 0, self._list_index) - self.assertEqual(index.data(), "Object class names") + self.assertEqual(index.data(), "Entity class names") index = self._model.index(1, 0, self._list_index) - self.assertEqual(index.data(), "Object names") + self.assertEqual(index.data(), "Entity names") index = self._model.index(2, 0, self._list_index) - self.assertEqual(index.data(), "Object metadata") + self.assertEqual(index.data(), "Entity metadata") index = self._model.index(3, 0, self._list_index) self.assertEqual(index.data(), "Parameter names") index = self._model.index(4, 0, self._list_index) @@ -612,13 +695,13 @@ def test_data_when_mapping_relationship_class_without_objects_or_parameters(self self.assertEqual(self._model.rowCount(self._list_index), 4) self.assertEqual(self._model.columnCount(self._list_index), 4) index = self._model.index(0, 0, self._list_index) - self.assertEqual(index.data(), "Relationship class names") + self.assertEqual(index.data(), "Entity class names") index = self._model.index(1, 0, self._list_index) - self.assertEqual(index.data(), "Object class names") + self.assertEqual(index.data(), "Dimension names") index = self._model.index(2, 0, self._list_index) - self.assertEqual(index.data(), "Object names") + self.assertEqual(index.data(), "Element names") index = self._model.index(3, 0, self._list_index) - self.assertEqual(index.data(), "Relationship metadata") + self.assertEqual(index.data(), "Entity metadata") for row in range(4): index = self._model.index(row, 1, self._list_index) self.assertEqual(index.data(), "None") @@ -631,7 +714,7 @@ def test_data_when_mapping_relationship_class_without_objects_or_parameters(self def test_data_when_mapping_invalid_relationship_class_with_parameters(self): indexed_parameter_mapping_dict = { "map_type": "parameter", - "name": {'map_type': None}, + "name": {"map_type": None}, "parameter_type": "map", "value": {"map_type": None}, "extra_dimensions": [{"map_type": None}], @@ -649,13 +732,13 @@ def test_data_when_mapping_invalid_relationship_class_with_parameters(self): self.assertEqual(self._model.rowCount(self._list_index), 10) self.assertEqual(self._model.columnCount(self._list_index), 4) index = self._model.index(0, 0, self._list_index) - self.assertEqual(index.data(), "Relationship class names") + self.assertEqual(index.data(), "Entity class names") index = self._model.index(1, 0, self._list_index) - self.assertEqual(index.data(), "Object class names") + self.assertEqual(index.data(), "Dimension names") index = self._model.index(2, 0, self._list_index) - self.assertEqual(index.data(), "Object names") + self.assertEqual(index.data(), "Element names") index = self._model.index(3, 0, self._list_index) - self.assertEqual(index.data(), "Relationship metadata") + self.assertEqual(index.data(), "Entity metadata") index = self._model.index(4, 0, self._list_index) self.assertEqual(index.data(), "Parameter names") index = self._model.index(5, 0, self._list_index) @@ -680,7 +763,7 @@ def test_data_when_mapping_invalid_relationship_class_with_parameters(self): def test_data_when_mapping_multidimensional_relationship_class_with_parameters(self): indexed_parameter_mapping_dict = { "map_type": "parameter", - "name": {'map_type': 'column', 'reference': 99}, + "name": {"map_type": "column", "reference": 99}, "parameter_type": "map", "value": {"reference": 23, "map_type": "column"}, "extra_dimensions": [{"reference": "fifth column", "map_type": "column"}], @@ -703,17 +786,17 @@ def test_data_when_mapping_multidimensional_relationship_class_with_parameters(s self.assertEqual(self._model.rowCount(self._list_index), 12) self.assertEqual(self._model.columnCount(self._list_index), 4) index = self._model.index(0, 0, self._list_index) - self.assertEqual(index.data(), "Relationship class names") + self.assertEqual(index.data(), "Entity class names") index = self._model.index(1, 0, self._list_index) - self.assertEqual(index.data(), "Object class names 1") + self.assertEqual(index.data(), "Dimension names 1") index = self._model.index(2, 0, self._list_index) - self.assertEqual(index.data(), "Object class names 2") + self.assertEqual(index.data(), "Dimension names 2") index = self._model.index(3, 0, self._list_index) - self.assertEqual(index.data(), "Object names 1") + self.assertEqual(index.data(), "Element names 1") index = self._model.index(4, 0, self._list_index) - self.assertEqual(index.data(), "Object names 2") + self.assertEqual(index.data(), "Element names 2") index = self._model.index(5, 0, self._list_index) - self.assertEqual(index.data(), "Relationship metadata") + self.assertEqual(index.data(), "Entity metadata") index = self._model.index(6, 0, self._list_index) self.assertEqual(index.data(), "Parameter names") index = self._model.index(7, 0, self._list_index) @@ -813,5 +896,5 @@ def test_data_when_mapping_object_class_with_regular_expression(self): self.assertEqual(self._model.index(2, 3, self._list_index).data(), "choose_me") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/importer/test_Importer.py b/tests/importer/test_Importer.py index e0f72cc1..f1b16636 100644 --- a/tests/importer/test_Importer.py +++ b/tests/importer/test_Importer.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Importer project item. - -""" +"""Unit tests for Importer project item.""" import collections import os from tempfile import TemporaryDirectory @@ -33,7 +31,7 @@ def setUp(self): """Set up.""" self.toolbox = create_mock_toolbox() mock_spec_model = self.toolbox.specification_model = MagicMock() - Specification = collections.namedtuple('Specification', 'name mapping item_type') + Specification = collections.namedtuple("Specification", "name mapping item_type") mock_spec_model.find_specification.side_effect = lambda x: Specification( name=x, mapping={}, item_type="Importer" ) @@ -64,9 +62,6 @@ def setUpClass(cls): def test_item_type(self): self.assertEqual(Importer.item_type(), ItemInfo.item_type()) - def test_item_category(self): - self.assertEqual(Importer.item_category(), ItemInfo.item_category()) - def test_item_dict(self): """Tests Item dictionary creation.""" specification = ImporterSpecification("import specification", {}) @@ -138,7 +133,7 @@ def test_rename(self): self.importer.rename(expected_name, "") # Check name self.assertEqual(expected_name, self.importer.name) # item name - self.assertEqual(expected_name, self.importer.get_icon().name_item.text()) # name item on Design View + self.assertEqual(expected_name, self.importer.get_icon().name()) # name item on Design View # Check data_dir expected_data_dir = os.path.join(self.project.items_dir, expected_short_name) self.assertEqual(expected_data_dir, self.importer.data_dir) # Check data dir diff --git a/tests/importer/test_ImporterExecutable.py b/tests/importer/test_ImporterExecutable.py index b6ff9058..dc98c3c9 100644 --- a/tests/importer/test_ImporterExecutable.py +++ b/tests/importer/test_ImporterExecutable.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for ImporterExecutable. - -""" +"""Unit tests for ImporterExecutable.""" from multiprocessing import Lock from pathlib import Path from tempfile import TemporaryDirectory @@ -78,12 +76,12 @@ def test_from_dict(self): self.assertEqual("Importer", item.item_type()) def test_stop_execution(self): - executable = ExecutableItem("name", {}, [], "", True, 'merge', self._temp_dir.name, mock.MagicMock()) + executable = ExecutableItem("name", {}, [], "", True, "merge", self._temp_dir.name, mock.MagicMock()) executable.stop_execution() self.assertIsNone(executable._process) def test_execute_simplest_case(self): - executable = ExecutableItem("name", {}, [], "", True, 'merge', self._temp_dir.name, mock.MagicMock()) + executable = ExecutableItem("name", {}, [], "", True, "merge", self._temp_dir.name, mock.MagicMock()) self.assertTrue(executable.execute([], [], Lock())) # Check that _process is None after execution self.assertIsNone(executable._process) @@ -99,7 +97,7 @@ def test_execute_import_small_file(self): logger = mock.MagicMock() logger.__reduce__ = lambda _: (mock.MagicMock, ()) executable = ExecutableItem( - "name", mapping, [str(data_file)], gams_path, True, 'merge', self._temp_dir.name, logger + "name", mapping, [str(data_file)], gams_path, True, "merge", self._temp_dir.name, logger ) database_resources = [database_resource("provider", database_url)] file_resources = [file_resource("provider", str(data_file))] @@ -109,14 +107,13 @@ def test_execute_import_small_file(self): self.assertTrue(executable.execute(file_resources, database_resources, Lock())) # Check that _process is None after execution self.assertIsNone(executable._process) - database_map = DatabaseMapping(database_url) - class_list = database_map.object_class_list().all() - self.assertEqual(len(class_list), 1) - self.assertEqual(class_list[0].name, "class") - object_list = database_map.object_list(class_id=class_list[0].id).all() - self.assertEqual(len(object_list), 1) - self.assertEqual(object_list[0].name, "entity") - database_map.connection.close() + with DatabaseMapping(database_url) as database_map: + class_list = database_map.query(database_map.entity_class_sq).all() + self.assertEqual(len(class_list), 1) + self.assertEqual(class_list[0].name, "class") + entity_list = database_map.query(database_map.entity_sq).filter_by(class_id=class_list[0].id).all() + self.assertEqual(len(entity_list), 1) + self.assertEqual(entity_list[0].name, "entity") def test_execute_skip_deselected_file(self): data_file = Path(self._temp_dir.name, "data.dat") @@ -125,16 +122,15 @@ def test_execute_skip_deselected_file(self): database_url = "sqlite:///" + str(database_path) create_new_spine_database(database_url) gams_path = "" - executable = ExecutableItem("name", {}, [], gams_path, True, 'merge', self._temp_dir.name, mock.MagicMock()) + executable = ExecutableItem("name", {}, [], gams_path, True, "merge", self._temp_dir.name, mock.MagicMock()) database_resources = [database_resource("provider", database_url)] file_resources = [file_resource("provider", str(data_file))] self.assertTrue(executable.execute(file_resources, database_resources, Lock())) # Check that _process is None after execution self.assertIsNone(executable._process) - database_map = DatabaseMapping(database_url) - class_list = database_map.object_class_list().all() - self.assertEqual(len(class_list), 0) - database_map.connection.close() + with DatabaseMapping(database_url) as database_map: + class_list = database_map.query(database_map.entity_class_sq).all() + self.assertEqual(len(class_list), 0) @staticmethod def _write_simple_data(file_name): diff --git a/tests/importer/test_ItemInfo.py b/tests/importer/test_ItemInfo.py index 652a3291..95aa2478 100644 --- a/tests/importer/test_ItemInfo.py +++ b/tests/importer/test_ItemInfo.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Importer's ItemInfo class. - -""" +"""Unit tests for Importer's ItemInfo class.""" import unittest from spine_items.importer.item_info import ItemInfo @@ -21,9 +19,6 @@ class TestItemInfo(unittest.TestCase): def test_item_type(self): self.assertEqual(ItemInfo.item_type(), "Importer") - def test_item_category(self): - self.assertEqual(ItemInfo.item_category(), "Importers") - if __name__ == "__main__": unittest.main() diff --git a/tests/importer/test_flattened_mappings.py b/tests/importer/test_flattened_mappings.py index 1ac01b5a..773824bc 100644 --- a/tests/importer/test_flattened_mappings.py +++ b/tests/importer/test_flattened_mappings.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,6 +9,7 @@ # 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 . ###################################################################################################################### + """Unit tests for ``flattened_mappings`` module.""" import unittest from spinedb_api import import_mapping_from_dict @@ -32,10 +34,10 @@ def test_increasing_relationship_dimensions_keeps_import_objects_flags_consisten ] root_mapping = import_mapping_from_dict(mapping_dicts) flattened_mappings = FlattenedMappings(root_mapping) - self.assertTrue(flattened_mappings.import_objects()) + self.assertTrue(flattened_mappings.import_entities()) flattened_mappings.set_dimension_count(2) - self.assertTrue(flattened_mappings.import_objects()) + self.assertTrue(flattened_mappings.import_entities()) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/importer/test_importer_icon.py b/tests/importer/test_importer_icon.py new file mode 100644 index 00000000..e92232c7 --- /dev/null +++ b/tests/importer/test_importer_icon.py @@ -0,0 +1,51 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +"""Unit tests for ImporterIcon class.""" +import unittest +from unittest import mock +from tempfile import TemporaryDirectory +from PySide6.QtCore import QEvent +from PySide6.QtWidgets import QApplication, QGraphicsSceneMouseEvent +from tests.mock_helpers import create_toolboxui_with_project, clean_up_toolbox +from spine_items.importer.importer_factory import ImporterFactory + + +class TestImporterIcon(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not QApplication.instance(): + QApplication() + + def setUp(self): + super().setUp() + self._temp_dir = TemporaryDirectory() + self._toolbox = create_toolboxui_with_project(self._temp_dir.name) + item_dict = {"type": "Importer", "description": "", "x": 0, "y": 0, "specification": None} + exp = ImporterFactory.make_item("I", item_dict, self._toolbox, self._toolbox.project()) + self._toolbox.project().add_item(exp) + + def tearDown(self): + super().tearDown() + clean_up_toolbox(self._toolbox) + self._temp_dir.cleanup() + + def test_mouse_double_click_event(self): + icon = self._toolbox.project()._project_items["I"].get_icon() + with mock.patch("spine_items.importer.importer.Importer.open_import_editor") as mock_open_import_editor: + mock_open_import_editor.return_value = True + icon.mouseDoubleClickEvent(QGraphicsSceneMouseEvent(QEvent.Type.GraphicsSceneMouseDoubleClick)) + mock_open_import_editor.assert_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/importer/widgets/__init__.py b/tests/importer/widgets/__init__.py index bfda140b..512ed971 100644 --- a/tests/importer/widgets/__init__.py +++ b/tests/importer/widgets/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Import editor's widgets. - -""" +"""Unit tests for Import editor's widgets.""" diff --git a/tests/importer/widgets/test_ImportPreview_Window.py b/tests/importer/widgets/test_ImportPreview_Window.py index 659e168f..0b69e514 100644 --- a/tests/importer/widgets/test_ImportPreview_Window.py +++ b/tests/importer/widgets/test_ImportPreview_Window.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # 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) @@ -9,16 +10,13 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains unit tests for the ImportEditorWindow class. - -""" - +""" Contains unit tests for the ImportEditorWindow class. """ import unittest from unittest import mock from PySide6.QtCore import QSettings from PySide6.QtWidgets import QApplication, QWidget from spine_items.importer.widgets.import_editor_window import ImportEditorWindow +from spinedb_api.spine_io.importers.csv_reader import CSVConnector class TestImportEditorWindow(unittest.TestCase): @@ -31,6 +29,7 @@ def test_closeEvent(self): spec = mock.NonCallableMagicMock() spec.name = "spec_name" spec.description = "spec_desc" + spec.mapping = {"source_type": CSVConnector.__name__} toolbox = QWidget() toolbox.qsettings = mock.MagicMock(return_value=QSettings(toolbox)) toolbox.restore_and_activate = mock.MagicMock() @@ -40,15 +39,23 @@ def test_closeEvent(self): widget._app_settings.beginGroup.assert_called_once_with("mappingPreviewWindow") widget._app_settings.endGroup.assert_called_once_with() qsettings_save_calls = widget._app_settings.setValue.call_args_list - self.assertEqual(len(qsettings_save_calls), 5) + self.assertEqual(len(qsettings_save_calls), 8) saved_dict = {saved[0][0]: saved[0][1] for saved in qsettings_save_calls} - self.assertIn("windowSize", saved_dict) - self.assertIn("windowPosition", saved_dict) - self.assertIn("windowState", saved_dict) - self.assertIn("windowMaximized", saved_dict) - self.assertIn("n_screens", saved_dict) + self.assertEqual(len(saved_dict), 8) + for key in ( + "windowSize", + "windowPosition", + "windowState", + "windowMaximized", + "n_screens", + "splitter_source_listState", + "splitter_source_data_mappingsState", + "splitterState", + ): + with self.subTest(key=key): + self.assertIn(key, saved_dict) toolbox.deleteLater() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/importer/widgets/test_IntegerSequenceDateTimeConvertSpecDialog.py b/tests/importer/widgets/test_IntegerSequenceDateTimeConvertSpecDialog.py index e0888551..0cc5d82f 100644 --- a/tests/importer/widgets/test_IntegerSequenceDateTimeConvertSpecDialog.py +++ b/tests/importer/widgets/test_IntegerSequenceDateTimeConvertSpecDialog.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains unit tests for :class:`IntegerSequenceDateTimeConvertSpecDialog`. - -""" +"""Contains unit tests for :class:`IntegerSequenceDateTimeConvertSpecDialog`.""" import unittest from PySide6.QtCore import Qt from PySide6.QtWidgets import QApplication @@ -35,5 +33,5 @@ def test_restore_previous_spec(self): self.assertEqual(widget.duration.text(), "5h") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/importer/widgets/test_OptionsWidget.py b/tests/importer/widgets/test_OptionsWidget.py index 61f831ef..1860aead 100644 --- a/tests/importer/widgets/test_OptionsWidget.py +++ b/tests/importer/widgets/test_OptionsWidget.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # 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) @@ -9,13 +10,10 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains unit tests for the OptionsWidget class. - -""" +"""Contains unit tests for the OptionsWidget class.""" import unittest from unittest.mock import MagicMock -from PySide6.QtWidgets import QApplication, QCheckBox, QComboBox, QLineEdit, QSpinBox +from PySide6.QtWidgets import QApplication from PySide6.QtGui import QUndoStack from spine_items.importer.widgets.options_widget import OptionsWidget @@ -36,15 +34,12 @@ def test_spin_box_change_signalling(self): connector.connection.OPTIONS = option_template widget = OptionsWidget(self._undo_stack) widget.set_connector(connector) - layout = widget.form_layout - checked = {"number": False} - for item in (layout.itemAt(i).widget() for i in range(layout.count())): - if isinstance(item, QSpinBox): - item.setValue(23) - connector.update_options.assert_called_once_with({"number": 23}) - checked["number"] = True - break - self.assertTrue(all(checked.values())) + changed_options = {} + widget.options_changed.connect(lambda o: changed_options.update(o)) + spin_box = widget.cellWidget(0, 0).layout().itemAt(1).widget() + spin_box.setValue(23) + connector.update_options.assert_called_once_with({"number": 23}) + self.assertEqual(changed_options, {"number": 23}) def test_line_edit_change_signalling(self): option_template = {"text": {"label": "text value", "type": str, "default": ""}} @@ -53,15 +48,12 @@ def test_line_edit_change_signalling(self): connector.connection.OPTIONS = option_template widget = OptionsWidget(self._undo_stack) widget.set_connector(connector) - layout = widget.form_layout - checked = False - for item in (layout.itemAt(i).widget() for i in range(layout.count())): - if isinstance(item, QLineEdit): - item.setText("the text has been set") - connector.update_options.assert_called_once_with({"text": "the text has been set"}) - checked = True - break - self.assertTrue(checked) + changed_options = {} + widget.options_changed.connect(lambda o: changed_options.update(o)) + line_edit = widget.cellWidget(0, 0).layout().itemAt(1).widget() + line_edit.setText("the text has been set") + connector.update_options.assert_called_once_with({"text": "the text has been set"}) + self.assertEqual(changed_options, {"text": "the text has been set"}) def test_combo_box_change_signalling(self): option_template = { @@ -72,15 +64,12 @@ def test_combo_box_change_signalling(self): connector.connection.OPTIONS = option_template widget = OptionsWidget(self._undo_stack) widget.set_connector(connector) - layout = widget.form_layout - checked = False - for item in (layout.itemAt(i).widget() for i in range(layout.count())): - if isinstance(item, QComboBox): - item.setCurrentText("choice b") - connector.update_options.assert_called_once_with({"choice": "choice b"}) - checked = True - break - self.assertTrue(checked) + changed_options = {} + widget.options_changed.connect(lambda o: changed_options.update(o)) + combo_box = widget.cellWidget(0, 0).layout().itemAt(1).widget() + combo_box.setCurrentText("choice b") + connector.update_options.assert_called_once_with({"choice": "choice b"}) + self.assertEqual(changed_options, {"choice": "choice b"}) def test_check_box_change_signalling(self): option_template = {"yesno": {"label": "check me", "type": bool, "default": True}} @@ -89,15 +78,12 @@ def test_check_box_change_signalling(self): connector.connection.OPTIONS = option_template widget = OptionsWidget(self._undo_stack) widget.set_connector(connector) - layout = widget.form_layout - checked = False - for item in (layout.itemAt(i).widget() for i in range(layout.count())): - if isinstance(item, QCheckBox): - item.setChecked(False) - connector.update_options.assert_called_once_with({"yesno": False}) - checked = True - break - self.assertTrue(checked) + changed_options = {} + widget.options_changed.connect(lambda o: changed_options.update(o)) + check_box = widget.cellWidget(0, 0).layout().itemAt(1).widget() + check_box.setChecked(False) + connector.update_options.assert_called_once_with({"yesno": False}) + self.assertEqual(changed_options, {"yesno": False}) def test_set_connector_makes_copy_of_connections_base_options(self): option_template = {"yesno": {"label": "check me", "type": bool, "default": True}} @@ -111,5 +97,5 @@ def test_set_connector_makes_copy_of_connections_base_options(self): ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/importer/widgets/test_import_editor_window.py b/tests/importer/widgets/test_import_editor_window.py new file mode 100644 index 00000000..c2c2f1ee --- /dev/null +++ b/tests/importer/widgets/test_import_editor_window.py @@ -0,0 +1,60 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +"""Unit tests for the ``import_editor_window`` module.""" +from tempfile import TemporaryDirectory +import unittest +from unittest import mock +from PySide6.QtWidgets import QApplication, QDialog +from spine_items.importer.widgets.import_editor_window import ImportEditorWindow +from spinedb_api.spine_io.importers.sqlalchemy_connector import SqlAlchemyConnector +from tests.mock_helpers import clean_up_toolbox, create_toolboxui_with_project + + +class TestIsUrl(unittest.TestCase): + def test_url_is_url(self): + self.assertTrue(ImportEditorWindow._is_url("sqlite:///C:\\data\\db.sqlite")) + self.assertTrue(ImportEditorWindow._is_url("postgresql+psycopg://spuser:s3cr3t@server.com/db")) + + def test_non_url_is_not_url(self): + self.assertFalse(ImportEditorWindow._is_url("C:\\path\\to\\file")) + self.assertFalse(ImportEditorWindow._is_url("/unix/style/path")) + + +class TestImportEditorWindow(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not QApplication.instance(): + QApplication() + + def setUp(self): + self._temp_dir = TemporaryDirectory() + self._toolbox = create_toolboxui_with_project(self._temp_dir.name) + + def tearDown(self): + clean_up_toolbox(self._toolbox) + self._temp_dir.cleanup() + + def test_get_connector_selects_sql_alchemy_connector_when_source_is_url(self): + with mock.patch("spine_items.importer.widgets.import_editor_window.QDialog.exec") as exec_dialog: + exec_dialog.return_value = QDialog.DialogCode.Accepted + editor = ImportEditorWindow(self._toolbox, None) + exec_dialog.assert_called_once() + exec_dialog.reset_mock() + connector = editor._get_connector("mysql://server.com/db") + exec_dialog.assert_called_once() + editor.close() + self.assertIs(connector, SqlAlchemyConnector) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/importer/widgets/test_import_mapping_options.py b/tests/importer/widgets/test_import_mapping_options.py index 6e4356a0..160ec737 100644 --- a/tests/importer/widgets/test_import_mapping_options.py +++ b/tests/importer/widgets/test_import_mapping_options.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # 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) @@ -97,5 +98,5 @@ def parent_widget(): parent.deleteLater() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/importer/widgets/test_import_sources.py b/tests/importer/widgets/test_import_sources.py index dcd332b1..d0ddd938 100644 --- a/tests/importer/widgets/test_import_sources.py +++ b/tests/importer/widgets/test_import_sources.py @@ -8,14 +8,13 @@ # 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 unit tests for the ``import_sources`` module.""" import unittest from unittest import mock - from PySide6.QtCore import QModelIndex from PySide6.QtGui import QUndoStack from PySide6.QtWidgets import QApplication, QListWidget, QWidget - from spine_items.importer.connection_manager import ConnectionManager from spine_items.importer.mvcmodels.mappings_model import MappingsModel from spine_items.importer.widgets.import_sources import ImportSources @@ -230,5 +229,5 @@ def get_data_iterator(self, table, options, max_rows=-1): return self._data[slice(begin, end)], header -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/merger/__init__.py b/tests/merger/__init__.py index 4575e6fa..2f2f7684 100644 --- a/tests/merger/__init__.py +++ b/tests/merger/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Init file for tests.project_items.merger package. Intentionally empty. - -""" +"""Init file for tests.project_items.merger package. Intentionally empty.""" diff --git a/tests/merger/test_MergerExecutable.py b/tests/merger/test_MergerExecutable.py index 28d9ae84..5df030d4 100644 --- a/tests/merger/test_MergerExecutable.py +++ b/tests/merger/test_MergerExecutable.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for MergerExecutable. - -""" +"""Unit tests for MergerExecutable.""" from multiprocessing import Lock from pathlib import Path from tempfile import TemporaryDirectory @@ -24,8 +22,6 @@ from spine_engine.project_item.connection import Connection from spine_engine.spine_engine import SpineEngine, SpineEngineState from spine_items.merger.executable_item import ExecutableItem -from spine_items.data_store.executable_item import ExecutableItem as DSExecutableItem -from spine_items.utils import convert_to_sqlalchemy_url class TestMergerExecutable(unittest.TestCase): @@ -63,22 +59,17 @@ def test_execute_merge_two_dbs(self): db1_path = Path(self._temp_dir.name, "db1.sqlite") db1_url = "sqlite:///" + str(db1_path) # Add some data to db1 - db1_map = DatabaseMapping(db1_url, create=True) - import_functions.import_object_classes(db1_map, ["a"]) - import_functions.import_objects(db1_map, [("a", "a_1")]) - # Commit to db1 - db1_map.commit_session("Add an object class 'a' and an object for unit tests.") + with DatabaseMapping(db1_url, create=True) as db1_map: + import_functions.import_entity_classes(db1_map, [("a",)]) + import_functions.import_entities(db1_map, [("a", "a_1")]) + db1_map.commit_session("Add an object class 'a' and an object for unit tests.") db2_path = Path(self._temp_dir.name, "db2.sqlite") db2_url = "sqlite:///" + str(db2_path) # Add some data to db2 - db2_map = DatabaseMapping(db2_url, create=True) - import_functions.import_object_classes(db2_map, ["b"]) - import_functions.import_objects(db2_map, [("b", "b_1")]) - # Commit to db2 - db2_map.commit_session("Add an object class 'b' and an object for unit tests.") - # Close connections - db1_map.connection.close() - db2_map.connection.close() + with DatabaseMapping(db2_url, create=True) as db2_map: + import_functions.import_entity_classes(db2_map, [("b",)]) + import_functions.import_entities(db2_map, [("b", "b_1")]) + db2_map.commit_session("Add an object class 'b' and an object for unit tests.") # Make an empty output db db3_path = Path(self._temp_dir.name, "db3.sqlite") db3_url = "sqlite:///" + str(db3_path) @@ -93,37 +84,31 @@ def test_execute_merge_two_dbs(self): r.metadata["db_server_manager_queue"] = mngr_queue self.assertTrue(executable.execute(input_db_resources, output_db_resources, Lock())) # Check output db - output_db_map = DatabaseMapping(db3_url) - class_list = output_db_map.object_class_list().all() - self.assertEqual(len(class_list), 2) - self.assertEqual(class_list[0].name, "a") - self.assertEqual(class_list[1].name, "b") - object_list_a = output_db_map.object_list(class_id=class_list[0].id).all() - self.assertEqual(len(object_list_a), 1) - self.assertEqual(object_list_a[0].name, "a_1") - object_list_b = output_db_map.object_list(class_id=class_list[1].id).all() - self.assertEqual(len(object_list_b), 1) - self.assertEqual(object_list_b[0].name, "b_1") - output_db_map.connection.close() + with DatabaseMapping(db3_url) as output_db_map: + class_list = output_db_map.query(output_db_map.entity_class_sq).all() + self.assertEqual(len(class_list), 2) + self.assertEqual(class_list[0].name, "a") + self.assertEqual(class_list[1].name, "b") + entity_list_a = output_db_map.query(output_db_map.entity_sq).filter_by(class_id=class_list[0].id).all() + self.assertEqual(len(entity_list_a), 1) + self.assertEqual(entity_list_a[0].name, "a_1") + entity_list_b = output_db_map.query(output_db_map.entity_sq).filter_by(class_id=class_list[1].id).all() + self.assertEqual(len(entity_list_b), 1) + self.assertEqual(entity_list_b[0].name, "b_1") def test_write_order(self): db1_path = Path(self._temp_dir.name, "db1.sqlite") db1_url = "sqlite:///" + str(db1_path) # Add some data to db1 - db1_map = DatabaseMapping(db1_url, create=True) - import_functions.import_data(db1_map, object_classes=["fish"]) - # Commit to db1 - db1_map.commit_session("Add test data.") + with DatabaseMapping(db1_url, create=True) as db1_map: + import_functions.import_data(db1_map, entity_classes=[("fish",)]) + db1_map.commit_session("Add test data.") db2_path = Path(self._temp_dir.name, "db2.sqlite") db2_url = "sqlite:///" + str(db2_path) # Add some data to db2 - db2_map = DatabaseMapping(db2_url, create=True) - import_functions.import_data(db2_map, object_classes=["cat"]) - # Commit to db2 - db2_map.commit_session("Add test data.") - # Close connections - db1_map.connection.close() - db2_map.connection.close() + with DatabaseMapping(db2_url, create=True) as db2_map: + import_functions.import_data(db2_map, entity_classes=[("cat",)]) + db2_map.commit_session("Add test data.") # Make an empty output db db3_path = Path(self._temp_dir.name, "db3.sqlite") db3_url = "sqlite:///" + str(db3_path) @@ -189,12 +174,11 @@ def test_write_order(self): create_new_spine_database(db3_url) engine.run() self.assertEqual(engine.state(), SpineEngineState.COMPLETED) - db3_map = DatabaseMapping(db3_url, create=True) - commits = db3_map.query(db3_map.commit_sq).all() + with DatabaseMapping(db3_url, create=True) as db3_map: + commits = db3_map.query(db3_map.commit_sq).all() merger1_idx = next(iter(k for k, commit in enumerate(commits) if db1_url in commit.comment)) merger2_idx = next(iter(k for k, commit in enumerate(commits) if db2_url in commit.comment)) self.assertTrue(merger2_idx < merger1_idx) - db3_map.connection.close() if __name__ == "__main__": diff --git a/tests/merger/test_merger_icon.py b/tests/merger/test_merger_icon.py new file mode 100644 index 00000000..2493067e --- /dev/null +++ b/tests/merger/test_merger_icon.py @@ -0,0 +1,47 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +"""Unit tests for MergerIcon class.""" +import unittest +from tempfile import TemporaryDirectory +from PySide6.QtWidgets import QApplication +from tests.mock_helpers import create_toolboxui_with_project, clean_up_toolbox +from spine_items.merger.merger_factory import MergerFactory + + +class TestMergerIcon(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not QApplication.instance(): + QApplication() + + def setUp(self): + super().setUp() + self._temp_dir = TemporaryDirectory() + self._toolbox = create_toolboxui_with_project(self._temp_dir.name) + item_dict = {"type": "Merger", "description": "", "x": 0, "y": 0} + merger = MergerFactory.make_item("M", item_dict, self._toolbox, self._toolbox.project()) + self._toolbox.project().add_item(merger) + + def tearDown(self): + super().tearDown() + clean_up_toolbox(self._toolbox) + self._temp_dir.cleanup() + + def test_animation(self): + icon = self._toolbox.project()._project_items["M"].get_icon() + icon.start_animation() + icon.stop_animation() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/mock_helpers.py b/tests/mock_helpers.py index 5197e3bb..f0fe7922 100644 --- a/tests/mock_helpers.py +++ b/tests/mock_helpers.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,15 +10,11 @@ # this program. If not, see . ###################################################################################################################### -""" -Classes and functions that can be shared among unit test modules. - -""" +"""Classes and functions that can be shared among unit test modules.""" import os.path from unittest.mock import MagicMock, patch from PySide6.QtGui import QStandardItemModel from PySide6.QtWidgets import QApplication, QWidget - from spinetoolbox.ui_main import ToolboxUI @@ -29,6 +26,21 @@ def test_push_vars(self): return True +class MockQSettings: + """Class for replacing an argument where e.g. class constructor requires an instance of QSettings. + For example all ToolSpecification classes require a QSettings instance.""" + + # noinspection PyMethodMayBeStatic, PyPep8Naming + def value(self, key, defaultValue=""): + """Returns the default value""" + return defaultValue + + # noinspection PyPep8Naming + def setValue(self, key, value): + """Returns without modifying anything.""" + return + + def create_mock_toolbox(): mock_toolbox = MagicMock() mock_toolbox.msg = MagicMock() @@ -40,6 +52,12 @@ def create_mock_toolbox(): return mock_toolbox +def create_mock_toolbox_with_mock_qsettings(): + mock_toolbox = create_mock_toolbox() + mock_toolbox.qsettings().value.side_effect = qsettings_value_side_effect + return mock_toolbox + + def create_mock_project(project_dir): mock_project = MagicMock() mock_project.project_dir = project_dir @@ -57,6 +75,21 @@ def mock_finish_project_item_construction(factory, project_item, mock_toolbox): return properties_widget +# noinspection PyMethodMayBeStatic, PyPep8Naming,SpellCheckingInspection +def qsettings_value_side_effect(key, defaultValue="0"): + """Returns the default value. + + Args: + key (str): Key to read + defaultValue (Any): Default value if key is missing + + Returns: + Any: settings value + """ + # Tip: add return values for specific keys here as needed. + return defaultValue + + def create_toolboxui(): """Returns ToolboxUI, where QSettings among others has been mocked.""" with patch("spinetoolbox.plugin_manager.PluginManager.load_installed_plugins"), patch( @@ -107,18 +140,3 @@ def clean_up_toolbox(toolbox): # Delete undo stack explicitly to prevent emitting certain signals well after ToolboxUI has been destroyed. toolbox.undo_stack.deleteLater() toolbox.deleteLater() - - -# noinspection PyMethodMayBeStatic, PyPep8Naming,SpellCheckingInspection -def qsettings_value_side_effect(key, defaultValue="0"): - """Returns Toolbox app settings values. - - Args: - key (str): Key to read - defaultValue (Any): Default value if key is missing - - Returns: - Any: settings value - """ - # Tip: add return values for specific keys here as needed. - return defaultValue diff --git a/tests/test_database_validation.py b/tests/test_database_validation.py index b308a336..778502a1 100644 --- a/tests/test_database_validation.py +++ b/tests/test_database_validation.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,11 +9,11 @@ # 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 . ###################################################################################################################### + """Unit tests for the ``database_validation`` module.""" from pathlib import Path from tempfile import TemporaryDirectory import unittest - from PySide6.QtCore import Slot from PySide6.QtWidgets import QApplication from sqlalchemy.engine.url import make_url @@ -42,6 +43,22 @@ def test_successful_validation_of_sqlite_database(self): validator.deleteLater() self.assertTrue(listener.is_success) + def test_successful_validation_of_sqlite_database_with_str_url(self): + with TemporaryDirectory() as temp_dir: + url = "sqlite:///" + str(Path(temp_dir, "db.sqlite")) + create_new_spine_database(url) + listener = _Listener() + validator = DatabaseConnectionValidator() + try: + sa_url = make_url(url) + validator.validate_url("sqlite", sa_url, listener.failure, listener.success) + while not listener.is_done: + QApplication.processEvents() + finally: + validator.wait_for_finish() + validator.deleteLater() + self.assertTrue(listener.is_success) + def test_validation_failure_due_to_missing_sqlite_file(self): with TemporaryDirectory() as temp_dir: url = "sqlite:///" + str(Path(temp_dir, "db.sqlite")) @@ -84,5 +101,5 @@ def success(self): self._is_done = True -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_models.py b/tests/test_models.py index 3b34fc98..c1a8e867 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for the ``models`` module. - -""" +"""Unit tests for the ``models`` module.""" from pathlib import Path import unittest from PySide6.QtCore import Qt diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..6c54b0e5 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,187 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +""" Unit tests for the ``utils`` module. """ +import sys +import unittest +from unittest import mock +from spine_items.utils import convert_to_sqlalchemy_url, convert_url_to_safe_string, database_label + + +class TestDatabaseLabel(unittest.TestCase): + def test_provider_name_is_appended_to_label(self): + self.assertEqual(database_label("Item Name"), "db_url@Item Name") + + +class TestConvertToSqlAlchemyUrl(unittest.TestCase): + def test_sqlite_url_conversion_succeeds(self): + database_path = r"h:\files\database.sqlite" if sys.platform == "win32" else "/home/data/database.sqlite" + url = { + "dialect": "sqlite", + "database": database_path, + } + sa_url = convert_to_sqlalchemy_url(url) + self.assertEqual(sa_url.database, database_path) + self.assertIsNone(sa_url.username) + self.assertIsNone(sa_url.password) + self.assertIsNone(sa_url.port) + self.assertIsNone(sa_url.host) + self.assertEqual(sa_url.drivername, r"sqlite") + self.assertEqual(str(sa_url), r"sqlite:///" + database_path) + + def test_remote_url_conversion_succeeds(self): + url = { + "dialect": "mysql", + "host": "example.com", + "port": 5432, + "database": "trade_nuc", + "username": "superman", + "password": "s3cr?t", + } + sa_url = convert_to_sqlalchemy_url(url) + self.assertEqual(sa_url.database, "trade_nuc") + self.assertEqual(sa_url.password, "s3cr?t") + self.assertEqual(sa_url.username, "superman") + self.assertEqual(sa_url.port, 5432) + self.assertEqual(sa_url.host, "example.com") + self.assertEqual(sa_url.drivername, r"mysql+pymysql") + + def test_remote_url_conversion_succeeds_with_schema(self): + url = { + "dialect": "mysql", + "host": "example.com", + "port": 5432, + "database": "trade_nuc", + "schema": "myschema", + "username": "superman", + "password": "s3cr?t", + } + sa_url = convert_to_sqlalchemy_url(url) + self.assertEqual(sa_url.database, "trade_nuc") + self.assertEqual(sa_url.password, "s3cr?t") + self.assertEqual(sa_url.username, "superman") + self.assertEqual(sa_url.port, 5432) + self.assertEqual(sa_url.host, "example.com") + self.assertEqual(sa_url.drivername, r"mysql+pymysql") + + def test_item_name_and_logger(self): + url = {} + logger = mock.MagicMock() + self.assertIsNone(convert_to_sqlalchemy_url(url, "Item Name", logger)) + logger.msg_error.emit.assert_called_once_with( + "No URL specified for Item Name selections. Please specify one and try again." + ) + + def test_missing_dialect_is_caught(self): + url = { + "dialect": "", + "database": r"h:\files\database.sqlite", + } + logger = mock.MagicMock() + self.assertIsNone(convert_to_sqlalchemy_url(url, "Item Name", logger)) + logger.msg_error.emit.assert_called_once_with( + "Unable to generate URL from Item Name selections: missing dialect" + ) + + def test_missing_host_is_caught(self): + url = { + "dialect": "mysql", + "host": "", + "port": 5432, + "database": "trade_nuc", + "username": "superman", + "password": "s3cr?t", + } + logger = mock.MagicMock() + self.assertIsNone(convert_to_sqlalchemy_url(url, "Item Name", logger)) + logger.msg_error.emit.assert_called_once_with( + "Unable to generate URL from Item Name selections: missing host" + ) + + def test_missing_port_is_caught(self): + url = { + "dialect": "mysql", + "host": "example.com", + "port": None, + "database": "trade_nuc", + "username": "superman", + "password": "s3cr?t", + } + logger = mock.MagicMock() + self.assertIsNone(convert_to_sqlalchemy_url(url, "Item Name", logger)) + logger.msg_error.emit.assert_called_once_with( + "Unable to generate URL from Item Name selections: missing port" + ) + + def test_missing_username_is_caught(self): + url = { + "dialect": "mysql", + "host": "example.com", + "port": 5432, + "database": "trade_nuc", + "username": None, + "password": "s3cr?t", + } + logger = mock.MagicMock() + self.assertIsNone(convert_to_sqlalchemy_url(url, "Item Name", logger)) + logger.msg_error.emit.assert_called_once_with( + "Unable to generate URL from Item Name selections: missing username" + ) + + def test_missing_password_is_caught(self): + url = { + "dialect": "mysql", + "host": "example.com", + "port": 5432, + "database": "trade_nuc", + "username": "superman", + "password": None, + } + logger = mock.MagicMock() + self.assertIsNone(convert_to_sqlalchemy_url(url, "Item Name", logger)) + logger.msg_error.emit.assert_called_once_with( + "Unable to generate URL from Item Name selections: missing password" + ) + + +class TestConvertUrlToSafeString(unittest.TestCase): + def test_removes_username_and_password(self): + url = { + "dialect": "mysql", + "host": "example.com", + "port": 5432, + "database": "trade_nuc", + "username": "superman", + "password": "s3cr?t", + } + self.assertEqual(convert_url_to_safe_string(url), "mysql+pymysql://example.com:5432/trade_nuc") + + def test_works_without_credentials_in_url(self): + url = { + "dialect": "mysql", + "host": "example.com", + "port": 5432, + "database": "trade_nuc", + } + self.assertEqual(convert_url_to_safe_string(url), "mysql+pymysql://example.com:5432/trade_nuc") + + def test_works_with_sqlite_url(self): + database_path = r"c:\files\database.sqlite" if sys.platform == "win32" else "/dir/data/database.sqlite" + url = { + "dialect": "sqlite", + "database": database_path, + } + self.assertEqual(convert_url_to_safe_string(url), r"sqlite:///" + database_path) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tool/__init__.py b/tests/tool/__init__.py index 5782752b..9de53913 100644 --- a/tests/tool/__init__.py +++ b/tests/tool/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Init file for tests.project_items.tool package. Intentionally empty. - -""" +"""Init file for tests.project_items.tool package. Intentionally empty.""" diff --git a/tests/tool/dummy_julia_kernel/kernel.json b/tests/tool/dummy_julia_kernel/kernel.json new file mode 100644 index 00000000..29894cd3 --- /dev/null +++ b/tests/tool/dummy_julia_kernel/kernel.json @@ -0,0 +1,14 @@ +{ + "display_name": "julia 1.8.5", + "argv": [ + "/path/to/somejulia", + "-i", + "--color=yes", + "--project=/path/to/someotherjuliaproject", + "/path/to/IJulia/6TIq1/src/kernel.jl", + "{connection_file}" + ], + "language": "julia", + "env": {}, + "interrupt_mode": "message" +} diff --git a/tests/tool/test_ItemInfo.py b/tests/tool/test_ItemInfo.py index 626a4b77..f2563d45 100644 --- a/tests/tool/test_ItemInfo.py +++ b/tests/tool/test_ItemInfo.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Tool's ItemInfo class. - -""" +"""Unit tests for Tool's ItemInfo class.""" import unittest from spine_items.tool.item_info import ItemInfo @@ -21,9 +19,6 @@ class TestItemInfo(unittest.TestCase): def test_item_type(self): self.assertEqual(ItemInfo.item_type(), "Tool") - def test_item_category(self): - self.assertEqual(ItemInfo.item_category(), "Tools") - if __name__ == "__main__": unittest.main() diff --git a/tests/tool/test_Tool.py b/tests/tool/test_Tool.py index f3229816..6f178a3e 100644 --- a/tests/tool/test_Tool.py +++ b/tests/tool/test_Tool.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for Tool project item. - -""" - +"""Unit tests for Tool project item.""" from tempfile import TemporaryDirectory import unittest from unittest import mock @@ -79,9 +76,6 @@ def setUpClass(cls): def test_item_type(self): self.assertEqual(Tool.item_type(), ItemInfo.item_type()) - def test_item_category(self): - self.assertEqual(Tool.item_category(), ItemInfo.item_category()) - def test_item_dict(self): """Tests Item dictionary creation.""" tool = self._add_tool() @@ -135,7 +129,7 @@ def test_rename(self): tool.rename(expected_name, "") # Check name self.assertEqual(expected_name, tool.name) # item name - self.assertEqual(expected_name, tool.get_icon().name_item.text()) # name item on Design View + self.assertEqual(expected_name, tool.get_icon().name()) # name item on Design View # Check data_dir expected_data_dir = os.path.join(self.project.items_dir, expected_short_name) self.assertEqual(expected_data_dir, tool.data_dir) # Check data dir @@ -209,7 +203,7 @@ def test_find_input_files(self): ProjectItemResource("Exporter", "file", "fifth", url="file:///" + url5, metadata={}, filterable=False) ) result = tool._find_input_files(resources) - expected = {'input2.csv': [expected_urls["url5"]], 'input1.csv': [expected_urls["url3"]]} + expected = {"input2.csv": [expected_urls["url5"]], "input1.csv": [expected_urls["url3"]]} self.assertEqual(expected, result) resources.append( ProjectItemResource("Exporter", "file", "sixth", url="file:///" + url6, metadata={}, filterable=False) @@ -218,7 +212,7 @@ def test_find_input_files(self): result = tool._find_input_files(resources) expected = { os.path.join(self._temp_dir.name, "input3.csv"): [expected_urls["url6"]], - 'input2.csv': [expected_urls["url5"]], + "input2.csv": [expected_urls["url5"]], } self.assertEqual(expected, result) @@ -230,6 +224,7 @@ def _add_tool(self, item_dict=None): self._properties_widget = mock_finish_project_item_construction(factory, tool, self.toolbox) # Set model for tool combo box tool._properties_ui.comboBox_tool.setModel(self.model) + self.project.get_item.return_value = tool return tool def _assert_is_simple_exec_tool(self, tool): diff --git a/tests/tool/test_ToolExecutable.py b/tests/tool/test_ToolExecutable.py index 7dbad7d6..7f4d544d 100644 --- a/tests/tool/test_ToolExecutable.py +++ b/tests/tool/test_ToolExecutable.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for ToolExecutable item. - -""" +"""Unit tests for ToolExecutable item.""" from multiprocessing import Lock import sys import pathlib @@ -152,7 +150,7 @@ def test_execute_archives_output_files(self): app_settings = _MockSettings() logger = mock.MagicMock() tool_specification = PythonTool( - "Python tool", "Python", str(script_dir), script_files, app_settings, None, logger, outputfiles=output_files + "Python tool", "Python", str(script_dir), script_files, app_settings, logger, outputfiles=output_files ) work_dir = pathlib.Path(self._temp_dir.name, "work") work_dir.mkdir() @@ -194,18 +192,18 @@ def test_execute_logs_messages(self): with open(logs[0], "r") as f: lines = f.readlines() expected_lines = [ - '### Spine execution log file\n', - '### Item name: Logs stuff\n', - '### Filter id: \n', - '### Part: 1\n', - '\n', - '# Running python script.py\n', - 'hello\n', - 'Traceback (most recent call last):\n', + "### Spine execution log file\n", + "### Item name: Logs stuff\n", + "### Filter id: \n", + "### Part: 1\n", + "\n", + "# Running python script.py\n", + "hello\n", + "Traceback (most recent call last):\n", ' File "", line 3, in \n', ' File "script.py", line 2, in \n', " raise ValueError('foo')\n", - 'ValueError: foo\n', + "ValueError: foo\n", ] self.assertCountEqual(lines, expected_lines) kill_persistent_processes() @@ -395,7 +393,7 @@ class _MockSettings: def value(key, defaultValue=None): return { "appSettings/pythonPath": sys.executable, - "appSettings/useEmbeddedPython": "0", # Don't use embedded Python + "appSettings/usePythonKernel": "0", # Don't use Jupyter Console "appSettings/workDir": "some_work_dir", }.get(key, defaultValue) diff --git a/tests/tool/test_tool_icon.py b/tests/tool/test_tool_icon.py new file mode 100644 index 00000000..fc06f1ee --- /dev/null +++ b/tests/tool/test_tool_icon.py @@ -0,0 +1,56 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +"""Unit tests for ToolIcon class.""" +import unittest +from unittest import mock +from tempfile import TemporaryDirectory +from PySide6.QtCore import QEvent +from PySide6.QtWidgets import QApplication, QGraphicsSceneMouseEvent +from tests.mock_helpers import create_toolboxui_with_project, clean_up_toolbox +from spine_items.tool.tool_factory import ToolFactory + + +class TestToolIcon(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not QApplication.instance(): + QApplication() + + def setUp(self): + super().setUp() + self._temp_dir = TemporaryDirectory() + self._toolbox = create_toolboxui_with_project(self._temp_dir.name) + item_dict = {"type": "Tool", "description": "", "x": 0, "y": 0, "specification": None} + t = ToolFactory.make_item("T", item_dict, self._toolbox, self._toolbox.project()) + self._toolbox.project().add_item(t) + + def tearDown(self): + super().tearDown() + clean_up_toolbox(self._toolbox) + self._temp_dir.cleanup() + + def test_mouse_double_click_event(self): + icon = self._toolbox.project()._project_items["T"].get_icon() + with mock.patch("spine_items.tool.tool.Tool.show_specification_window") as mock_show_spec_window: + mock_show_spec_window.return_value = True + icon.mouseDoubleClickEvent(QGraphicsSceneMouseEvent(QEvent.Type.GraphicsSceneMouseDoubleClick)) + mock_show_spec_window.assert_called() + + def test_animation(self): + icon = self._toolbox.project()._project_items["T"].get_icon() + icon.start_animation() + icon.stop_animation() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tool/test_tool_instance.py b/tests/tool/test_tool_instance.py index 50e0865e..e1c6b330 100644 --- a/tests/tool/test_tool_instance.py +++ b/tests/tool/test_tool_instance.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # 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) @@ -9,71 +10,434 @@ # this program. If not, see . ###################################################################################################################### -""" -Contains unit tests for the tool_instance module. - -""" +"""Contains unit tests for the tool_instance module.""" import sys import unittest from unittest import mock - -from spine_engine.execution_managers.persistent_execution_manager import kill_persistent_processes -from spine_items.tool.tool_specifications import PythonTool +from tempfile import TemporaryDirectory +from pathlib import Path +from spine_items.tool.tool_specifications import PythonTool, JuliaTool, GAMSTool, ExecutableTool +from tests.mock_helpers import MockQSettings -class TestPythonToolInstance(unittest.TestCase): - @classmethod - def tearDownClass(cls): - kill_persistent_processes() +class TestToolInstance(unittest.TestCase): + def test_python_prepare_with_jupyter_console(self): + # No cmd line args + instance = self._make_python_tool_instance(True) + with mock.patch("spine_items.tool.tool_instance.KernelExecutionManager") as mock_manager: + instance.prepare([]) + mock_manager.assert_called_once() + self.assertEqual(mock_manager.call_args[0][1], "some_kernel") # kernel_name + self.assertEqual(mock_manager.call_args[0][2], ["%cd -q path/", '%run "main.py"']) # commands + # With tool cmd line args + instance = self._make_python_tool_instance(True) + with mock.patch("spine_items.tool.tool_instance.KernelExecutionManager") as mock_manager: + instance.prepare(["arg1", "arg2"]) + mock_manager.assert_called_once() + self.assertEqual(mock_manager.call_args[0][1], "some_kernel") + self.assertEqual(mock_manager.call_args[0][2], ["%cd -q path/", '%run "main.py" "arg1" "arg2"']) + # With tool and tool spec cmd line args + instance = self._make_python_tool_instance(True, ["arg3"]) + with mock.patch("spine_items.tool.tool_instance.KernelExecutionManager") as mock_manager: + instance.prepare(["arg1", "arg2"]) + mock_manager.assert_called_once() + self.assertEqual(mock_manager.call_args[0][1], "some_kernel") + self.assertEqual(mock_manager.call_args[0][2], ["%cd -q path/", '%run "main.py" "arg3" "arg1" "arg2"']) - def test_prepare_with_cmd_line_arguments_in_jupyter_kernel(self): - instance = self._make_tool_instance(True) - with mock.patch("spine_items.tool.tool_instance.KernelExecutionManager"): + def test_python_prepare_with_basic_console(self): + # No cmd line args + instance = self._make_python_tool_instance(False) + with mock.patch("spine_items.tool.tool_instance.PythonPersistentExecutionManager") as mock_manager: + mock_manager.return_value = True + instance.prepare([]) + mock_manager.assert_called_once() + self.assertEqual([sys.executable], mock_manager.call_args[0][1]) # args + self.assertEqual(5, len(mock_manager.call_args[0][2])) # commands + self.assertEqual("python main.py", mock_manager.call_args[0][3]) # alias + # With tool cmd line args + instance = self._make_python_tool_instance(False) + with mock.patch("spine_items.tool.tool_instance.PythonPersistentExecutionManager") as mock_manager: + mock_manager.return_value = True + instance.prepare(["arg1", "arg2"]) + mock_manager.assert_called_once() + self.assertEqual([sys.executable], mock_manager.call_args[0][1]) # args + self.assertEqual(5, len(mock_manager.call_args[0][2])) # commands + self.assertEqual("python main.py arg1 arg2", mock_manager.call_args[0][3]) # alias + # With tool and tool spec cmd line args + instance = self._make_python_tool_instance(False, ["arg3"]) + with mock.patch("spine_items.tool.tool_instance.PythonPersistentExecutionManager") as mock_manager: + mock_manager.return_value = True instance.prepare(["arg1", "arg2"]) - self.assertEqual(instance.args, ['%cd -q path/', '%run "main.py" "arg1" "arg2"']) + mock_manager.assert_called_once() + self.assertEqual([sys.executable], mock_manager.call_args[0][1]) # args + self.assertEqual(5, len(mock_manager.call_args[0][2])) # commands + self.assertEqual("python main.py arg3 arg1 arg2", mock_manager.call_args[0][3]) # alias - def test_prepare_with_empty_cmd_line_arguments_in_jupyter_kernel(self): - instance = self._make_tool_instance(True) - with mock.patch("spine_items.tool.tool_instance.KernelExecutionManager"): + def test_julia_prepare_with_jupyter_console(self): + # No cmd line args + instance = self._make_julia_tool_instance(True) + with mock.patch("spine_items.tool.tool_instance.KernelExecutionManager") as mock_kem, mock.patch( + "os.path.isfile" + ) as mock_isfile, mock.patch("spine_items.tool.utils.find_kernel_specs") as mock_find_kernel_specs: + mock_find_kernel_specs.return_value = {"some_julia_kernel": Path(__file__).parent / "dummy_julia_kernel"} + mock_isfile.return_value = False instance.prepare([]) - self.assertEqual(instance.args, ['%cd -q path/', '%run "main.py"']) + mock_isfile.assert_called() + mock_kem.assert_called_once() + mock_find_kernel_specs.assert_called_once() + self.assertEqual(mock_kem.call_args[0][1], "some_julia_kernel") # kernel_name + self.assertEqual(mock_kem.call_args[0][2], ['cd("path/");', 'include("hello.jl")']) # commands + # With tool cmd line args + instance = self._make_julia_tool_instance(True) + with mock.patch("spine_items.tool.tool_instance.KernelExecutionManager") as mock_kem, mock.patch( + "os.path.isfile" + ) as mock_isfile, mock.patch("spine_items.tool.utils.find_kernel_specs") as mock_find_kernel_specs: + mock_find_kernel_specs.return_value = {"some_julia_kernel": Path(__file__).parent / "dummy_julia_kernel"} + mock_isfile.return_value = False + instance.prepare(["arg1", "arg2"]) + mock_isfile.assert_called() + mock_kem.assert_called_once() + mock_find_kernel_specs.assert_called_once() + self.assertEqual(mock_kem.call_args[0][1], "some_julia_kernel") + self.assertEqual( + mock_kem.call_args[0][2], + ['cd("path/");', 'empty!(ARGS); append!(ARGS, ["arg1", "arg2"]);', 'include("hello.jl")'], + ) + # With tool and tool spec cmd line args + instance = self._make_julia_tool_instance(True, ["arg3"]) + with mock.patch("spine_items.tool.tool_instance.KernelExecutionManager") as mock_kem, mock.patch( + "os.path.isfile" + ) as mock_isfile, mock.patch("spine_items.tool.utils.find_kernel_specs") as mock_find_kernel_specs: + mock_find_kernel_specs.return_value = {"some_julia_kernel": Path(__file__).parent / "dummy_julia_kernel"} + mock_isfile.return_value = False + instance.prepare(["arg1", "arg2"]) + mock_isfile.assert_called() + mock_kem.assert_called_once() + mock_find_kernel_specs.assert_called_once() + self.assertEqual(mock_kem.call_args[0][1], "some_julia_kernel") + self.assertEqual( + mock_kem.call_args[0][2], + ['cd("path/");', 'empty!(ARGS); append!(ARGS, ["arg3", "arg1", "arg2"]);', 'include("hello.jl")'], + ) - def test_prepare_with_cmd_line_arguments_in_persistent_process(self): - instance = self._make_tool_instance(False) - with mock.patch("spine_engine.execution_managers.persistent_execution_manager.PersistentManagerBase"): + def test_julia_prepare_with_basic_console(self): + # No cmd line args + instance = self._make_julia_tool_instance(False) + instance._owner.options = {"julia_sysimage": "path/to/sysimage.so"} + with mock.patch("spine_items.tool.tool_instance.JuliaPersistentExecutionManager") as mock_manager, mock.patch( + "os.path.isfile" + ) as mock_isfile, mock.patch("spine_items.tool.utils.resolve_julia_executable") as mock_resolve_julia: + mock_isfile.return_value = True # Make isfile() accept fake julia_sysimage path + mock_manager.return_value = True + mock_resolve_julia.return_value = "path/to/julia" + instance.prepare([]) + mock_isfile.assert_called() + mock_manager.assert_called_once() + mock_resolve_julia.assert_called() + self.assertEqual( + ["path/to/julia", "--sysimage=path/to/sysimage.so"], mock_manager.call_args[0][1] + ) # args attribute for JuliaPersistentExecutionManger + self.assertEqual(['cd("path/");', 'include("hello.jl")'], mock_manager.call_args[0][2]) # commands + self.assertEqual("julia hello.jl", mock_manager.call_args[0][3]) # alias + # With tool cmd line args + instance = self._make_julia_tool_instance(False) + with mock.patch("spine_items.tool.tool_instance.JuliaPersistentExecutionManager") as mock_manager, mock.patch( + "os.path.isfile" + ) as mock_isfile, mock.patch("spine_items.tool.utils.resolve_julia_executable") as mock_resolve_julia: + mock_isfile.return_value = False + mock_manager.return_value = True + mock_resolve_julia.return_value = "path/to/julia" + instance.prepare(["arg1", "arg2"]) + mock_isfile.assert_called() + mock_manager.assert_called_once() + mock_resolve_julia.assert_called() + self.assertEqual(["path/to/julia"], mock_manager.call_args[0][1]) + self.assertEqual( + ['cd("path/");', 'empty!(ARGS); append!(ARGS, ["arg1", "arg2"]);', 'include("hello.jl")'], + mock_manager.call_args[0][2], + ) + self.assertEqual("julia hello.jl arg1 arg2", mock_manager.call_args[0][3]) # alias + # With tool and tool spec cmd line args + instance = self._make_julia_tool_instance(False, ["arg3"]) + with mock.patch("spine_items.tool.tool_instance.JuliaPersistentExecutionManager") as mock_manager, mock.patch( + "os.path.isfile" + ) as mock_isfile, mock.patch("spine_items.tool.utils.resolve_julia_executable") as mock_resolve_julia: + mock_isfile.return_value = False + mock_manager.return_value = True + mock_resolve_julia.return_value = "path/to/julia" instance.prepare(["arg1", "arg2"]) - self.assertEqual(instance.program, [sys.executable]) - self.assertEqual(instance.exec_mngr.alias, "python main.py arg1 arg2") - instance.terminate_instance() + mock_isfile.assert_called() + mock_manager.assert_called_once() + mock_resolve_julia.assert_called() + self.assertEqual(["path/to/julia"], mock_manager.call_args[0][1]) + self.assertEqual( + ['cd("path/");', 'empty!(ARGS); append!(ARGS, ["arg3", "arg1", "arg2"]);', 'include("hello.jl")'], + mock_manager.call_args[0][2], + ) + self.assertEqual("julia hello.jl arg3 arg1 arg2", mock_manager.call_args[0][3]) # alias + + def test_prepare_sysimg_maker(self): + instance = self._make_julia_tool_instance(False) + instance._settings = FakeQSettings() + with mock.patch("spine_items.tool.tool_instance.ProcessExecutionManager") as mock_pem, mock.patch( + "spine_items.tool.utils.resolve_julia_executable" + ) as mock_resolve_julia: + mock_resolve_julia.return_value = "path/to/julia" + instance.prepare([]) + mock_pem.assert_called_once() + mock_resolve_julia.assert_called() + self.assertEqual("path/to/julia", mock_pem.call_args[0][1]) # Julia exe + self.assertEqual(["hello.jl"], mock_pem.call_args[0][2]) # script + self.assertEqual("path/", mock_pem.call_args[1]["workdir"]) # workdir + instance.terminate_instance() # Increase coverage - def test_prepare_without_cmd_line_arguments_in_persistent_process(self): - instance = self._make_tool_instance(False) - with mock.patch("spine_engine.execution_managers.persistent_execution_manager.PersistentManagerBase"): + def test_julia_prepare_with_invalid_kernel(self): + instance = self._make_julia_tool_instance(True) + instance.prepare([]) + self.assertEqual(None, instance.exec_mngr) + self.assertEqual(False, instance.is_running()) + instance.terminate_instance() # Cover terminate_instance() + + def test_gams_prepare_with_cmd_line_arguments(self): + # No cmd line args + instance = self._make_gams_tool_instance() + path_to_gams = "path/to/gams" + with mock.patch("spine_items.tool.tool_instance.ProcessExecutionManager") as mock_manager, mock.patch( + "spine_items.tool.tool_instance.resolve_gams_executable" + ) as mock_gams_exe: + mock_manager.return_value = True + mock_gams_exe.return_value = path_to_gams instance.prepare([]) - self.assertEqual(instance.program, [sys.executable]) - self.assertEqual(instance.exec_mngr.alias, "python main.py") - instance.terminate_instance() + mock_manager.assert_called() + mock_gams_exe.assert_called() + self.assertEqual(path_to_gams, instance.program) + self.assertEqual(3, len(instance.args)) + self.assertEqual("model.gms", instance.args[0]) + self.assertEqual("curDir=path/", instance.args[1]) + self.assertEqual("logoption=3", instance.args[2]) + # With tool cmd line args + instance = self._make_gams_tool_instance() + path_to_gams = "path/to/gams" + with mock.patch("spine_items.tool.tool_instance.ProcessExecutionManager") as mock_manager, mock.patch( + "spine_items.tool.tool_instance.resolve_gams_executable" + ) as mock_gams_exe: + mock_manager.return_value = True + mock_gams_exe.return_value = path_to_gams + instance.prepare(["arg1", "arg2"]) + mock_manager.assert_called() + mock_gams_exe.assert_called() + self.assertEqual(path_to_gams, instance.program) + self.assertEqual(5, len(instance.args)) + self.assertEqual("model.gms", instance.args[0]) + self.assertEqual("curDir=path/", instance.args[1]) + self.assertEqual("logoption=3", instance.args[2]) + self.assertEqual("arg1", instance.args[3]) + self.assertEqual("arg2", instance.args[4]) + # With tool and tool spec cmd line args + instance = self._make_gams_tool_instance(tool_spec_args=["arg3"]) + path_to_gams = "path/to/gams" + with mock.patch("spine_items.tool.tool_instance.ProcessExecutionManager") as mock_manager, mock.patch( + "spine_items.tool.tool_instance.resolve_gams_executable" + ) as mock_gams_exe: + mock_manager.return_value = True + mock_gams_exe.return_value = path_to_gams + instance.prepare(["arg1", "arg2"]) + mock_manager.assert_called() + mock_gams_exe.assert_called() + self.assertEqual(path_to_gams, instance.program) + self.assertEqual(6, len(instance.args)) + self.assertEqual("model.gms", instance.args[0]) + self.assertEqual("curDir=path/", instance.args[1]) + self.assertEqual("logoption=3", instance.args[2]) + self.assertEqual("arg3", instance.args[3]) + self.assertEqual("arg1", instance.args[4]) + self.assertEqual("arg2", instance.args[5]) + + def test_executable_prepare_with_main_program(self): + instance = self._make_executable_tool_instance(tool_spec_args=["arg3"]) + # when os.path.isfile fails, we throw a RuntimeError + self.assertRaises(RuntimeError, instance.prepare, ["arg1", "arg2"]) + # Test when sys.platform is win32 + with mock.patch("spine_items.tool.tool_instance.ProcessExecutionManager") as mock_manager, mock.patch( + "os.path.isfile" + ) as mock_isfile, mock.patch("sys.platform", "win32"): + mock_isfile.return_value = True + mock_manager.return_value = True + instance.prepare(["arg1", "arg2"]) # With tool cmd line args + self.assertEqual(1, mock_manager.call_count) + self.assertEqual(1, mock_isfile.call_count) + self.assertEqual("path/program.exe", instance.program) + self.assertEqual(3, len(instance.args)) + self.assertEqual(["arg3", "arg1", "arg2"], instance.args) + instance = self._make_executable_tool_instance() + instance.prepare([]) # Without cmd line args + self.assertEqual(2, mock_manager.call_count) + self.assertEqual(2, mock_isfile.call_count) + self.assertEqual("path/program.exe", instance.program) + self.assertEqual(0, len(instance.args)) + # Test when sys.platform is linux + with mock.patch("spine_items.tool.tool_instance.ProcessExecutionManager") as mock_manager, mock.patch( + "os.path.isfile" + ) as mock_isfile, mock.patch("sys.platform", "linux"): + instance = self._make_executable_tool_instance(tool_spec_args=["arg3"]) + mock_isfile.return_value = True + mock_manager.return_value = True + instance.prepare(["arg1", "arg2"]) # With cmd line args + self.assertEqual(1, mock_manager.call_count) + self.assertEqual(1, mock_isfile.call_count) + self.assertEqual("sh", instance.program) + self.assertEqual(4, len(instance.args)) + self.assertEqual(["path/program.exe", "arg3", "arg1", "arg2"], instance.args) + + def test_executable_prepare_with_cmd(self): + instance = self._make_executable_tool_instance(shell="cmd.exe", cmd="dir", tool_spec_args=["arg3"]) + with mock.patch("spine_items.tool.tool_instance.ProcessExecutionManager") as mock_manager: + # Run command with cmd.exe + mock_manager.return_value = True + instance.prepare(["arg1", "arg2"]) + self.assertEqual(1, mock_manager.call_count) + self.assertEqual("cmd.exe", instance.program) + self.assertEqual(5, len(instance.args)) + self.assertEqual(["/C", "dir", "arg3", "arg1", "arg2"], instance.args) + # Run command with bash shell + instance = self._make_executable_tool_instance(shell="bash", cmd="ls") + instance.prepare(["-a"]) + self.assertEqual(2, mock_manager.call_count) + self.assertEqual("sh", instance.program) + self.assertEqual(2, len(instance.args)) + self.assertEqual(["ls", "-a"], instance.args) + # Run command without shell + instance = self._make_executable_tool_instance(cmd="cat") + instance.prepare(["file.txt"]) + self.assertEqual(3, mock_manager.call_count) + self.assertEqual("cat", instance.program) + self.assertEqual(1, len(instance.args)) + self.assertEqual(["file.txt"], instance.args) + + def test_execute_julia_tool_instance(self): + instance = self._make_julia_tool_instance(False) + self.execute_fake_python_julia_and_executable_tool_instances(instance) + + def test_execute_python_tool_instance(self): + instance = self._make_python_tool_instance(False) + self.execute_fake_python_julia_and_executable_tool_instances(instance) + + def test_execute_executable_tool_instance(self): + instance = self._make_executable_tool_instance("cmd.exe", cmd="dir") + self.execute_fake_python_julia_and_executable_tool_instances(instance) + + def execute_fake_python_julia_and_executable_tool_instances(self, instance): + """Python and Julia Tool Specification return codes are the same.""" + instance.exec_mngr = FakeExecutionManager(0) # Valid return code + self.assertEqual(0, instance.execute()) + instance.exec_mngr = FakeExecutionManager(-1) # Valid return code + self.assertEqual(-1, instance.execute()) + instance.exec_mngr = FakeExecutionManager(1) # Invalid return code + self.assertEqual(1, instance.execute()) + + def test_execute_gams_tool_instance(self): + temp_dir = TemporaryDirectory() + instance = self._make_gams_tool_instance(temp_dir.name) + instance.exec_mngr = FakeExecutionManager(0) # Valid return code + self.assertEqual(0, instance.execute()) + instance.exec_mngr = FakeExecutionManager(1) # Valid return code + self.assertEqual(1, instance.execute()) # This creates a GAMS project file for debugging + debug_gpr_path = Path(temp_dir.name) / "specification_name_autocreated.gpr" + self.assertTrue(debug_gpr_path.is_file()) + debug_gpr_path.unlink() # Remove file (Path.unlink is equivalent to os.remove) + self.assertFalse(debug_gpr_path.is_file()) + instance.exec_mngr = FakeExecutionManager(-1) # Invalid return code + self.assertEqual(-1, instance.execute()) # This creates a GAMS project file for debugging + self.assertTrue(debug_gpr_path.is_file()) + temp_dir.cleanup() + + @staticmethod + def _make_python_tool_instance(use_jupyter_console, tool_spec_args=None): + specification = PythonTool( + "specification name", + "python", + "", + ["main.py"], + MockQSettings(), + mock.MagicMock(), + cmdline_args=tool_spec_args, + ) + specification.init_execution_settings() + if use_jupyter_console: + specification.execution_settings["use_jupyter_console"] = True + specification.execution_settings["kernel_spec_name"] = "some_kernel" + return specification.create_tool_instance("path/", False, logger=mock.MagicMock(), owner=mock.MagicMock()) + + @staticmethod + def _make_julia_tool_instance(use_jupyter_console, tool_spec_args=None): + specification = JuliaTool( + "specification name", + "julia", + "", + ["hello.jl"], + MockQSettings(), + mock.MagicMock(), + cmdline_args=tool_spec_args, + ) + specification.init_execution_settings() + if use_jupyter_console: + specification.execution_settings["use_jupyter_console"] = True + specification.execution_settings["kernel_spec_name"] = "some_julia_kernel" + return specification.create_tool_instance("path/", False, logger=mock.MagicMock(), owner=mock.MagicMock()) + + @staticmethod + def _make_gams_tool_instance(temp_dir=None, tool_spec_args=None): + path = temp_dir if temp_dir else "" + specification = GAMSTool( + "specification name", + "gams", + path, + ["model.gms"], + MockQSettings(), + mock.MagicMock(), + cmdline_args=tool_spec_args, + ) + return specification.create_tool_instance("path/", False, logger=mock.MagicMock(), owner=mock.MagicMock()) @staticmethod - def _make_tool_instance(execute_in_embedded_console): - settings = mock.NonCallableMagicMock() - if execute_in_embedded_console: - settings.value = mock.MagicMock(return_value="2") + def _make_executable_tool_instance(shell=None, cmd=None, tool_spec_args=None): + if cmd: + specification = ExecutableTool( + "name", "executable", "", [], MockQSettings(), mock.MagicMock(), cmdline_args=tool_spec_args + ) else: + specification = ExecutableTool( + "name", + "executable", + "", + ["program.exe"], + MockQSettings(), + mock.MagicMock(), + cmdline_args=tool_spec_args, + ) + specification.init_execution_settings() + if shell == "cmd.exe": + specification.execution_settings["shell"] = "cmd.exe" + elif shell == "bash": + specification.execution_settings["shell"] = "bash" + if cmd: + specification.execution_settings["cmd"] = cmd + return specification.create_tool_instance("path/", False, logger=mock.MagicMock(), owner=mock.MagicMock()) + + +class FakeExecutionManager: + def __init__(self, retval): + self.retval = retval + + def run_until_complete(self): + return self.retval - def get_setting(name, defaultValue): - return {"appSettings/pythonPath": sys.executable, "appSettings/useEmbeddedPython": "0"}.get( - name, defaultValue - ) - settings.value = mock.MagicMock(side_effect=get_setting) - logger = mock.Mock() - path = "" - source_files = ["main.py"] - specification = PythonTool("specification name", "python", path, source_files, settings, logger) - base_directory = "path/" - return specification.create_tool_instance(base_directory, False, logger, mock.Mock()) +class FakeQSettings: + def value(self, key, defaultValue=""): + if key == "appSettings/makeSysImage": + return "true" -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/tool/test_tool_specifications.py b/tests/tool/test_tool_specifications.py index 98d4701b..6fc51000 100644 --- a/tests/tool/test_tool_specifications.py +++ b/tests/tool/test_tool_specifications.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -8,23 +9,49 @@ # 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 . ###################################################################################################################### + """Unit tests for ``tool_specifications`` module.""" +import os import unittest -from unittest.mock import MagicMock -from spine_items.tool.tool_specifications import ToolSpecification +from unittest import mock +from tempfile import TemporaryDirectory +from spine_items.tool.tool_specifications import ( + ToolSpecification, + make_specification, + PythonTool, + JuliaTool, + GAMSTool, + ExecutableTool, +) +from spine_engine.spine_engine import SpineEngine +from spine_engine.project_item.connection import Jump +from tests.mock_helpers import MockQSettings class TestToolSpecification(unittest.TestCase): + def setUp(self) -> None: + self.qsettings = MockQSettings() + self.logger = mock.MagicMock() + self.test_dict = { + "name": "specification name", + "tooltype": "Python", + "includes": ["/included_file_1.bat", "/aa/included_file_2.std"], + "description": "Test tool specification", + "inputfiles": ["a_input.jpg", "b_input.png"], + "inputfiles_opt": ["*.dat", "*.xlsx", "a.csv", "z.zip"], + "outputfiles": ["*.atk", "*.zip"], + "cmdline_args": ["99", "10"], + "includes_main_path": "../tool", + } + def test_to_dict_sorts_input_and_output_files(self): - app_settings = MagicMock() - logger = MagicMock() specification = ToolSpecification( "specification name", "Python", "/path/to/tool", ["/included_file_1.bat", "/aa/included_file_2.std"], - app_settings, - logger, + self.qsettings, + self.logger, "Test tool specification", ["b_input.png", "a_input.jpg"], ["*.dat", "a.csv", "z.zip", "*.xlsx"], @@ -33,21 +60,66 @@ def test_to_dict_sorts_input_and_output_files(self): ) specification.definition_file_path = "/path/to/specification/file.json" specification_dict = specification.to_dict() - self.assertEqual( - specification_dict, - { - "name": "specification name", - "tooltype": "Python", - "includes": ["/included_file_1.bat", "/aa/included_file_2.std"], - "description": "Test tool specification", - "inputfiles": ["a_input.jpg", "b_input.png"], - "inputfiles_opt": ["*.dat", "*.xlsx", "a.csv", "z.zip"], - "outputfiles": ["*.atk", "*.zip"], - "cmdline_args": ["99", "10"], - "includes_main_path": "../tool", - }, - ) + self.assertEqual(specification_dict, self.test_dict) + + def test_make_specification(self): + self.test_dict["definition_file_path"] = "/path/to/specification/file.json" + spec = make_specification(self.test_dict, self.qsettings, self.logger) # Make PythonTool + self.assertIsInstance(spec, PythonTool) + spec.init_execution_settings() + self.assertIsNotNone(spec.execution_settings) + self.assertTrue(len(spec.execution_settings.keys()), 4) + spec.to_dict() + # Convert to Julia spec + self.test_dict["tooltype"] = "Julia" + spec = make_specification(self.test_dict, self.qsettings, self.logger) # Make JuliaTool + self.assertIsInstance(spec, JuliaTool) + spec.init_execution_settings() + self.assertIsNotNone(spec.execution_settings) + self.assertTrue(len(spec.execution_settings.keys()), 5) + spec.to_dict() + # Convert to GAMS spec + self.test_dict["tooltype"] = "GAMS" + spec = make_specification(self.test_dict, self.qsettings, self.logger) # Make GAMSTool + self.assertIsInstance(spec, GAMSTool) + spec.to_dict() + # Convert to Executable spec + self.test_dict["tooltype"] = "Executable" + spec = make_specification(self.test_dict, self.qsettings, self.logger) # Make ExecutableTool + self.assertIsInstance(spec, ExecutableTool) + spec.init_execution_settings() + self.assertIsNotNone(spec.execution_settings) + self.assertTrue(len(spec.execution_settings.keys()), 2) + spec.to_dict() + + def test_clone(self): + self.test_dict["definition_file_path"] = "/path/to/specification/file.json" + spec = make_specification(self.test_dict, self.qsettings, self.logger) + cloned_spec = spec.clone() + self.assertFalse(spec.is_equivalent(cloned_spec)) # False because 'path' is different. Bug? + + def test_tool_specification_as_jump_condition(self): + condition = {"type": "tool-specification", "specification": "loop_twice"} + jump = Jump("source", "bottom", "destination", "top", condition) + jump.make_logger(mock.Mock()) + with TemporaryDirectory() as temp_dir: + main_file = "script.py" + main_file_path = os.path.join(temp_dir, main_file) + with open(main_file_path, "w+") as program_file: + program_file.writelines( + ["import sys\n", "counter = int(sys.argv[1])\n", "exit(0 if counter == 23 else 1)\n"] + ) + spec_dict = { + "name": "loop_twice", + "tooltype": "python", + "includes_main_path": temp_dir, + "includes": [main_file], + "definition_file_path": "path/to/specification_file.json", + } + engine = SpineEngine(project_dir=temp_dir, specifications={"Tool": [spec_dict]}, connections=list()) + jump.set_engine(engine) + self.assertTrue(jump.is_condition_true(23)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/tool/test_utils.py b/tests/tool/test_utils.py index 8fd1b47f..3c3bab34 100644 --- a/tests/tool/test_utils.py +++ b/tests/tool/test_utils.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,15 +10,14 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for :literal:`utils` module. - -""" +""" Unit tests for :literal:`utils` module. """ from hashlib import sha1 from pathlib import Path from tempfile import TemporaryDirectory import unittest -from spine_items.tool.utils import find_last_output_files +from unittest import mock +from spine_items.tool.utils import find_last_output_files, get_julia_path_and_project +from spine_engine.utils.helpers import AppSettings class TestFindLastOutputFiles(unittest.TestCase): @@ -130,5 +130,57 @@ def test_finds_latest_output_directory_within_non_filter_id_directories_with_mix self.assertEqual(files, {"data.dat": [str(data_file_new)]}) -if __name__ == '__main__': +class TestGetJuliaPathAndProject(unittest.TestCase): + def test_get_julia_path_and_project(self): + # Use Jupyter Console: False + exec_settings = { + "use_jupyter_console": False, + "executable": "", + "env": "", + "kernel_spec_name": "", + "project": "", + } + app_settings = AppSettings({"appSettings/juliaPath": "path/to/julia"}) + julia_args = get_julia_path_and_project(exec_settings, app_settings) + self.assertTrue(len(julia_args) == 1) + self.assertTrue(julia_args[0] == "path/to/julia") + # Use Jupyter Console: False + exec_settings = { + "use_jupyter_console": False, + "executable": "/path/to/julia", + "env": "", + "kernel_spec_name": "", + "project": "/path/to/myjuliaproject", + } + julia_args = get_julia_path_and_project(exec_settings, app_settings) + self.assertTrue(julia_args[0] == "/path/to/julia") + self.assertTrue(julia_args[1] == "--project=/path/to/myjuliaproject") + # Use Jupyter Console: True + exec_settings = { + "use_jupyter_console": True, + "executable": "", + "env": "", + "kernel_spec_name": "unknown_kernel", + "project": "", + } + julia_args = get_julia_path_and_project(exec_settings, app_settings) + self.assertIsNone(julia_args) + # Use Jupyter Console: True + exec_settings = { + "use_jupyter_console": True, + "executable": "/path/to/nowhere", + "env": "conda", + "kernel_spec_name": "test_kernel", + "project": "/path/to/nonexistingprojectthatshouldnotbereturnedherebecausetheprojectisdefinedinkerneljson", + } + with mock.patch("spine_items.tool.utils.find_kernel_specs") as mock_find_kernel_specs: + # Return a dict containing a path to a dummy kernel resource dir when find_kernel_specs is called + mock_find_kernel_specs.return_value = {"test_kernel": Path(__file__).parent / "dummy_julia_kernel"} + julia_args = get_julia_path_and_project(exec_settings, app_settings) + self.assertEqual(2, len(julia_args)) + self.assertTrue(julia_args[0] == "/path/to/somejulia") + self.assertTrue(julia_args[1] == "--project=/path/to/someotherjuliaproject") + + +if __name__ == "__main__": unittest.main() diff --git a/tests/tool/widgets/__init__.py b/tests/tool/widgets/__init__.py index 8095b663..046209e7 100644 --- a/tests/tool/widgets/__init__.py +++ b/tests/tool/widgets/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) diff --git a/tests/tool/widgets/test_custom_menus.py b/tests/tool/widgets/test_custom_menus.py new file mode 100644 index 00000000..06938fce --- /dev/null +++ b/tests/tool/widgets/test_custom_menus.py @@ -0,0 +1,209 @@ +###################################################################################################################### +# Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors +# This file is part of Spine Items. +# Spine Items 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 . +###################################################################################################################### + +"""Unit tests for the custom_menus.py module.""" +import unittest +from unittest import mock +import logging +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from PySide6.QtWidgets import QApplication, QWidget +from PySide6.QtGui import QStandardItemModel, QStandardItem +from spine_items.tool.widgets.custom_menus import ToolSpecificationMenu +from spine_items.tool.tool_specifications import JuliaTool, ExecutableTool +from tests.mock_helpers import create_mock_toolbox_with_mock_qsettings, MockQSettings + + +class TestToolSpecificationMenu(unittest.TestCase): + @classmethod + def setUpClass(cls): + """Overridden method. Runs once before all tests in this class.""" + try: + cls.app = QApplication().processEvents() + except RuntimeError: + pass + logging.basicConfig( + stream=sys.stderr, + level=logging.WARNING, + format="%(asctime)s %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + def setUp(self): + """Overridden method. Runs before each test.""" + self.toolbox = create_mock_toolbox_with_mock_qsettings() + self._temp_dir = TemporaryDirectory() + + def tearDown(self): + """Overridden method. Runs after each test. + Use this to free resources after a test if needed. + """ + self._temp_dir.cleanup() + + def test_open_main_program_file(self): + spec = self.make_julia_tool_spec() + spec_model = CustomQStandardItemModel() + spec_item = QStandardItem(spec.name) + spec_item.setData(spec) + spec_model.appendRow(spec_item) + index = spec_model.index(0, 0) + w = ParentWidget(spec_model) + menu = ToolSpecificationMenu(w, index) + with mock.patch("spine_items.tool.widgets.custom_menus.open_url") as mock_open_url: + menu._open_main_program_file() + mock_open_url.assert_called() + + def test_open_main_program_file_fails_without_path(self): + spec = self.make_julia_tool_spec() + spec.path = None + spec_model = CustomQStandardItemModel() + spec_item = QStandardItem(spec.name) + spec_item.setData(spec) + spec_model.appendRow(spec_item) + index = spec_model.index(0, 0) + w = ParentWidget(spec_model) + menu = ToolSpecificationMenu(w, index) + with mock.patch("spine_items.tool.widgets.custom_menus.open_url") as mock_open_url: + menu._open_main_program_file() + mock_open_url.assert_not_called() + + def test_open_main_program_file_fails_without_path_and_includes(self): + spec = self.make_julia_tool_spec() + spec.path = None + spec.includes = None + spec_model = CustomQStandardItemModel() + spec_item = QStandardItem(spec.name) + spec_item.setData(spec) + spec_model.appendRow(spec_item) + index = spec_model.index(0, 0) + w = ParentWidget(spec_model) + menu = ToolSpecificationMenu(w, index) + with mock.patch("spine_items.tool.widgets.custom_menus.open_url") as mock_open_url: + menu._open_main_program_file() + mock_open_url.assert_not_called() + + def test_open_main_program_file_fails_with_bat_file(self): + spec = self.make_exec_tool_spec() + spec_model = CustomQStandardItemModel() + spec_item = QStandardItem(spec.name) + spec_item.setData(spec) + spec_model.appendRow(spec_item) + index = spec_model.index(0, 0) + w = ParentWidget(spec_model) + menu = ToolSpecificationMenu(w, index) + with mock.patch("spine_items.tool.widgets.custom_menus.open_url") as mock_open_url: + menu._open_main_program_file() + mock_open_url.assert_not_called() + + def test_open_main_program_dir(self): + spec = self.make_julia_tool_spec() + spec_model = CustomQStandardItemModel() + spec_item = QStandardItem(spec.name) + spec_item.setData(spec) + spec_model.appendRow(spec_item) + index = spec_model.index(0, 0) + w = ParentWidget(spec_model) + menu = ToolSpecificationMenu(w, index) + menu._open_main_program_dir() + + def test_open_main_program_dir_fails_without_includes(self): + spec = self.make_julia_tool_spec() + spec.includes = None + spec_model = CustomQStandardItemModel() + spec_item = QStandardItem(spec.name) + spec_item.setData(spec) + spec_model.appendRow(spec_item) + index = spec_model.index(0, 0) + w = ParentWidget(spec_model) + menu = ToolSpecificationMenu(w, index) + menu._open_main_program_dir() + + def test_open_main_program_dir_fails_without_path(self): + spec = self.make_julia_tool_spec() + spec.path = None + spec_model = CustomQStandardItemModel() + spec_item = QStandardItem(spec.name) + spec_item.setData(spec) + spec_model.appendRow(spec_item) + index = spec_model.index(0, 0) + w = ParentWidget(spec_model) + menu = ToolSpecificationMenu(w, index) + menu._open_main_program_dir() + + def make_julia_tool_spec(self): + script_dir = Path(self._temp_dir.name, "scripts") + script_dir.mkdir() + script_file_name = "hello.jl" + file_path = Path(script_dir, script_file_name) + with open(file_path, "w") as script_file: + script_file.writelines(["println('hello')\n"]) + mock_logger = mock.MagicMock() + julia_tool_spec = JuliaTool( + "test_julia_spec", + "julia", + str(script_dir), + [script_file_name], + MockQSettings(), + mock_logger, + ) + julia_tool_spec.init_execution_settings() # Sets defaults + return julia_tool_spec + + def make_exec_tool_spec(self): + script_dir = Path(self._temp_dir.name, "scripts") + script_dir.mkdir() + script_file_name = "batch.bat" + file_path = Path(script_dir, script_file_name) + with open(file_path, "w") as script_file: + script_file.writelines(["dir\n"]) + mock_logger = mock.MagicMock() + return ExecutableTool( + "test_exec_spec", + "executable", + str(script_dir), + [script_file_name], + MockQSettings(), + mock_logger, + ) + + +class CustomQStandardItemModel(QStandardItemModel): + """Fake specification model.""" + + def __init__(self): + super().__init__() + + def specification(self, row): + item = self.item(row, 0) + return item.data() + + +class ParentWidget(QWidget): + """Fake self._toolbox.""" + + def __init__(self, spec_model): + super().__init__() + self.specification_model = spec_model + self.msg_warning = MiniLogger() + self.msg_error = MiniLogger() + + def open_anchor(self, url): + """Fakes self._toolbox.open_anchor()""" + return True + + +class MiniLogger: + """Fakes calls to signal emits.""" + + def emit(self, msg): + return True diff --git a/tests/tool/widgets/test_PythonToolSpecOptionalWidget.py b/tests/tool/widgets/test_options_widgets.py similarity index 54% rename from tests/tool/widgets/test_PythonToolSpecOptionalWidget.py rename to tests/tool/widgets/test_options_widgets.py index 1d09a589..6e62cadf 100644 --- a/tests/tool/widgets/test_PythonToolSpecOptionalWidget.py +++ b/tests/tool/widgets/test_options_widgets.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,22 +10,15 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for PythonToolSpecOptionalWidget class. - -""" - +"""Unit tests for the options_widgets.py module.""" import unittest -from unittest import mock -from unittest.mock import MagicMock import logging import sys from PySide6.QtWidgets import QApplication, QWidget -from spine_items.tool.widgets.tool_spec_optional_widgets import PythonToolSpecOptionalWidget -from spine_items.tool.tool_specifications import PythonTool +from spine_items.tool.widgets.options_widgets import JuliaOptionsWidget -class TestPythonToolSpecOptionalWidget(unittest.TestCase): +class TestJuliaOptionsWidget(unittest.TestCase): @classmethod def setUpClass(cls): """Overridden method. Runs once before all tests in this class.""" @@ -39,21 +33,11 @@ def setUpClass(cls): datefmt="%Y-%m-%d %H:%M:%S", ) - def test_constructor_and_init(self): - with mock.patch( - "spine_items.tool.widgets.tool_spec_optional_widgets.PythonToolSpecOptionalWidget._toolbox" - ) as mock_toolbox: - mock_toolbox.qsettings.return_value = MockQSettings() - mock_logger = MagicMock() - python_tool_spec = PythonTool("a", "python", "", ["fake_main_program.py"], MockQSettings(), mock_logger) - python_tool_spec.set_execution_settings() # Sets defaults - python_tool_spec.execution_settings["executable"] = "fake_python.exe" - opt_widget = PythonToolSpecOptionalWidget(mock_toolbox) - opt_widget.init_widget(python_tool_spec) - self.assertEqual("fake_python.exe", opt_widget.get_executable()) - self.assertIsInstance(opt_widget, PythonToolSpecOptionalWidget) - - -class MockQSettings: - def value(self, key, defaultValue=""): - return defaultValue + def test_options_widget(self): + ow = JuliaOptionsWidget() + ow.set_tool(QWidget()) # Obviously not a real Tool + ow._set_ui_at_work() + ow._set_ui_at_rest() + options = {"julia_sysimage": "/some/path"} + ow.do_update_options(options) + self.assertEqual("/some/path", ow.ui.lineEdit_sysimage.text()) diff --git a/tests/tool/widgets/test_toolSpecificationEditorWindow.py b/tests/tool/widgets/test_toolSpecificationEditorWindow.py index 7787568b..23e42af6 100644 --- a/tests/tool/widgets/test_toolSpecificationEditorWindow.py +++ b/tests/tool/widgets/test_toolSpecificationEditorWindow.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,20 +10,25 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for ToolSpecificationEditorWindow class. - -""" - +"""Unit tests for ToolSpecificationEditorWindow class and tool_spec_optional_widgets module.""" import unittest import logging import sys +import os from unittest import mock -from tempfile import NamedTemporaryFile +from tempfile import NamedTemporaryFile, TemporaryDirectory from pathlib import Path from PySide6.QtWidgets import QApplication +from PySide6.QtCore import Qt, QItemSelectionModel +from PySide6.QtGui import QIcon +from spine_items.tool.tool_specifications import JuliaTool, ExecutableTool, PythonTool from spine_items.tool.widgets.tool_specification_editor_window import ToolSpecificationEditorWindow -from tests.mock_helpers import create_mock_toolbox +from spine_items.tool.widgets.tool_spec_optional_widgets import ( + JuliaToolSpecOptionalWidget, + PythonToolSpecOptionalWidget, + ExecutableToolSpecOptionalWidget, +) +from tests.mock_helpers import create_mock_toolbox_with_mock_qsettings, MockQSettings class TestToolSpecificationEditorWindow(unittest.TestCase): @@ -42,135 +48,555 @@ def setUpClass(cls): def setUp(self): """Overridden method. Runs before each test.""" - self.toolbox = create_mock_toolbox() - with mock.patch("spinetoolbox.project_item.specification_editor_window.restore_ui"): - self.tool_specification_widget = ToolSpecificationEditorWindow(self.toolbox) + self._temp_dir = TemporaryDirectory() + self.toolbox = create_mock_toolbox_with_mock_qsettings() + self.tool_specification_widget = None def tearDown(self): """Overridden method. Runs after each test. Use this to free resources after a test if needed. """ - self.tool_specification_widget.deleteLater() - self.tool_specification_widget = None + self._temp_dir.cleanup() + if self.tool_specification_widget is not None: + with mock.patch( + "spinetoolbox.project_item.specification_editor_window.SpecificationEditorWindowBase.tear_down" + ) as mock_tear_down: + mock_tear_down.return_value = True + self.tool_specification_widget.close() + mock_tear_down.assert_called() + self.tool_specification_widget.deleteLater() + self.tool_specification_widget = None - def test_create_minimal_julia_tool_specification(self): - self.tool_specification_widget._ui.comboBox_tooltype.setCurrentIndex(0) # 0: Julia - self.tool_specification_widget._spec_toolbar._line_edit_name.setText("test_tool") - with NamedTemporaryFile(mode="r") as temp_file: - self.tool_specification_widget._set_main_program_file(str(Path(temp_file.name))) - self.tool_specification_widget._save() + def make_tool_spec_editor(self, spec=None): + if not spec: + with mock.patch("spinetoolbox.project_item.specification_editor_window.restore_ui") as mock_restore_ui: + self.tool_specification_widget = ToolSpecificationEditorWindow(self.toolbox) + mock_restore_ui.assert_called() + else: + with mock.patch( + "spinetoolbox.project_item.specification_editor_window.restore_ui" + ) as mock_restore_ui, mock.patch( + "spine_items.tool.tool_specifications.ToolSpecification._includes_main_path_relative" + ) as mock_impr: + mock_impr.return_value = "" + self.tool_specification_widget = ToolSpecificationEditorWindow(self.toolbox, spec) + mock_restore_ui.assert_called() + mock_impr.assert_called() - def test_create_minimal_gams_tool_specification(self): - self.tool_specification_widget._ui.comboBox_tooltype.setCurrentIndex(2) # 2: gams - self.tool_specification_widget._spec_toolbar._line_edit_name.setText("test_tool") - with NamedTemporaryFile(mode="r") as temp_file: - self.tool_specification_widget._set_main_program_file(str(Path(temp_file.name))) - self.tool_specification_widget._save() + def test_change_tooltype(self): + self.make_tool_spec_editor() + self.tool_specification_widget._ui.comboBox_tooltype.setCurrentIndex(0) # julia + self.assertIsInstance(self.tool_specification_widget._get_optional_widget("julia"), JuliaToolSpecOptionalWidget) + sd = self.tool_specification_widget.spec_dict + exec_settings = sd.get("execution_settings") + self.assertEqual(sd["tooltype"], "julia") + self.assertEqual(len(exec_settings), 5) + self.tool_specification_widget._ui.comboBox_tooltype.setCurrentIndex(1) # python + self.assertIsInstance( + self.tool_specification_widget._get_optional_widget("python"), PythonToolSpecOptionalWidget + ) + sd = self.tool_specification_widget.spec_dict + exec_settings = sd.get("execution_settings") + self.assertEqual(sd["tooltype"], "python") + self.assertEqual(len(exec_settings), 4) + self.tool_specification_widget._ui.comboBox_tooltype.setCurrentIndex(2) # gams + self.assertIsNone(self.tool_specification_widget._get_optional_widget("gams")) + sd = self.tool_specification_widget.spec_dict + exec_settings = sd.get("execution_settings") + self.assertEqual(sd["tooltype"], "gams") + self.assertIsNone(exec_settings) + self.tool_specification_widget._ui.comboBox_tooltype.setCurrentIndex(3) # executable + self.assertIsInstance( + self.tool_specification_widget._get_optional_widget("executable"), ExecutableToolSpecOptionalWidget + ) + sd = self.tool_specification_widget.spec_dict + exec_settings = sd.get("execution_settings") + self.assertEqual(sd["tooltype"], "executable") + self.assertEqual(len(exec_settings), 2) - def test_create_minimal_executable_tool_specification(self): - self.tool_specification_widget._ui.comboBox_tooltype.setCurrentIndex(3) # 3: executable - self.tool_specification_widget._spec_toolbar._line_edit_name.setText("test_tool") + def test_make_new_specification(self): + self.make_tool_spec_editor() + self.tool_specification_widget._ui.comboBox_tooltype.setCurrentIndex(0) # julia + self.assertIsInstance(self.tool_specification_widget.optional_widget, JuliaToolSpecOptionalWidget) + self.tool_specification_widget._spec_toolbar._line_edit_name.setText("test_julia_tool") with NamedTemporaryFile(mode="r") as temp_file: - self.tool_specification_widget._set_main_program_file(str(Path(temp_file.name))) + self.tool_specification_widget._set_main_program_file(temp_file.name) + spec = self.tool_specification_widget._make_new_specification("test_julia_tool") + self.tool_specification_widget._init_optional_widget(spec) + self.assertIsInstance(spec, JuliaTool) + self.assertEqual(len(spec.execution_settings), 5) + self._call_save() + + def _call_save(self): + """Calls tool spec widgets _save() while Toolbox's tool spec widget base _save() is mocked.""" + with mock.patch( + "spinetoolbox.project_item.specification_editor_window.SpecificationEditorWindowBase._save" + ) as mock_save: + mock_save.return_value = True self.tool_specification_widget._save() + mock_save.assert_called() + + def test_open_tool_specification_editor_with_julia_spec(self): + script_dir = Path(self._temp_dir.name, "scripts") + script_dir.mkdir() + script_file_name = "hello.jl" + file_path = Path(script_dir, script_file_name) + with open(file_path, "w") as script_file: + script_file.writelines(["println('hello')\n"]) + mock_logger = mock.MagicMock() + julia_tool_spec = JuliaTool( + "test_julia_spec", + "julia", + str(script_dir), + [script_file_name], + MockQSettings(), + mock_logger, + "Description", + inputfiles=["data.csv"], + inputfiles_opt=["*.dat"], + outputfiles=["results.txt"], + cmdline_args=["-A", "-B"], + ) + julia_tool_spec.init_execution_settings() # Sets defaults + self.make_tool_spec_editor(julia_tool_spec) + opt_widget = self.tool_specification_widget.optional_widget + self.assertIsInstance(opt_widget, JuliaToolSpecOptionalWidget) + self.assertTrue(self.tool_specification_widget._ui.comboBox_tooltype.currentText() == "Julia") + self.assertTrue(self.tool_specification_widget._ui.lineEdit_args.text() == "-A -B") + # Program files dock widget should have 2 rows :'Main program file' and 'Additional program files' + self.assertEqual(2, self.tool_specification_widget.programfiles_model.rowCount()) + # Get index of 'Main program file' item + parent = self.tool_specification_widget.programfiles_model.index(0, 0) + # There should be one row under 'Main program file' -> the main program file + self.assertEqual(1, self.tool_specification_widget.programfiles_model.rowCount(parent)) + index = self.tool_specification_widget.programfiles_model.index(0, 0, parent) # Index of 'hello.jl' + item = self.tool_specification_widget.programfiles_model.itemFromIndex(index) + self.assertEqual(script_file_name, item.data(Qt.ItemDataRole.DisplayRole)) + # Check Input & output files dock widget + self.assertEqual(3, self.tool_specification_widget.io_files_model.rowCount()) + if_index = self.tool_specification_widget.io_files_model.index(0, 0) # 'Input files' item index + oif_index = self.tool_specification_widget.io_files_model.index(1, 0) # 'Optional input files' item index + of_index = self.tool_specification_widget.io_files_model.index(2, 0) # 'Output files' item index + if_child_index = self.tool_specification_widget.io_files_model.index(0, 0, if_index) + oif_child_index = self.tool_specification_widget.io_files_model.index(0, 0, oif_index) + of_child_index = self.tool_specification_widget.io_files_model.index(0, 0, of_index) + self.assertEqual(1, self.tool_specification_widget.io_files_model.rowCount(if_index)) + self.assertEqual(1, self.tool_specification_widget.io_files_model.rowCount(oif_index)) + self.assertEqual(1, self.tool_specification_widget.io_files_model.rowCount(of_index)) + if_item = self.tool_specification_widget.io_files_model.itemFromIndex(if_child_index) + self.assertEqual("data.csv", if_item.data(Qt.ItemDataRole.DisplayRole)) + oif_item = self.tool_specification_widget.io_files_model.itemFromIndex(oif_child_index) + self.assertEqual("*.dat", oif_item.data(Qt.ItemDataRole.DisplayRole)) + of_item = self.tool_specification_widget.io_files_model.itemFromIndex(of_child_index) + self.assertEqual("results.txt", of_item.data(Qt.ItemDataRole.DisplayRole)) + + def test_open_tool_specification_editor_with_executable_spec(self): + mock_logger = mock.MagicMock() + exec_tool_spec = ExecutableTool( + "a", "executable", self._temp_dir.name, ["fake_main_program.bat"], MockQSettings(), mock_logger + ) + exec_tool_spec.init_execution_settings() # Sets defaults + self.make_tool_spec_editor(exec_tool_spec) + opt_widget = self.tool_specification_widget._get_optional_widget("executable") + self.assertIsInstance(opt_widget, ExecutableToolSpecOptionalWidget) + self.assertFalse(opt_widget.ui.lineEdit_command.isEnabled()) # Command is disabled when a program file is set + # Program files dock widet should have 2 rows :'Main program file' and 'Additional program files' + self.assertEqual(2, self.tool_specification_widget.programfiles_model.rowCount()) + # Get index of 'Main program file' item + parent = self.tool_specification_widget.programfiles_model.index(0, 0) + # There should be one row under 'Main program file' -> the main program file + self.assertEqual(1, self.tool_specification_widget.programfiles_model.rowCount(parent)) + index = self.tool_specification_widget.programfiles_model.index( + 0, 0, parent + ) # Index of 'fake_main_program.bat' + item = self.tool_specification_widget.programfiles_model.itemFromIndex(index) + self.assertEqual("fake_main_program.bat", item.data(Qt.ItemDataRole.DisplayRole)) + + def test_edit_and_save_program_file(self): + mock_logger = mock.MagicMock() + script_file_name = "hello.py" + file_path = Path(self._temp_dir.name, script_file_name) + with open(file_path, "w") as h: + h.writelines(["# hello.py"]) # Make hello.py + python_tool_spec = PythonTool( + "a", "python", self._temp_dir.name, [script_file_name], MockQSettings(), mock_logger + ) + python_tool_spec.init_execution_settings() # Sets defaults + self.make_tool_spec_editor(python_tool_spec) + parent = self.tool_specification_widget.programfiles_model.index(0, 0) + index = self.tool_specification_widget.programfiles_model.index(0, 0, parent) # Index of 'hello.py' + self.tool_specification_widget._ui.textEdit_program.appendPlainText("print('hi')") + self.tool_specification_widget._save_program_file( + file_path, self.tool_specification_widget._ui.textEdit_program.document() + ) + # Open file and check contents + with open(file_path, "r") as edited_file: + l = edited_file.readlines() + self.assertEqual(2, len(l)) + self.assertTrue(l[0].startswith("# hello")) # Don't match the whole str to avoid problems with newline + self.assertTrue(l[1].startswith("print('hi')")) + + def test_change_python_spec_options(self): + mock_logger = mock.MagicMock() + script_file_name = "hello.py" + file_path = Path(self._temp_dir.name, script_file_name) + with open(file_path, "w") as h: + h.writelines(["# hello.py"]) # Make hello.py + python_tool_spec = PythonTool( + "a", "python", self._temp_dir.name, [script_file_name], MockQSettings(), mock_logger + ) + python_tool_spec.init_execution_settings() # Sets defaults + python_tool_spec.execution_settings["use_jupyter_console"] = True + python_tool_spec.execution_settings["kernel_spec_name"] = "python310" + with mock.patch("spine_items.tool.widgets.tool_spec_optional_widgets.KernelFetcher", new=FakeKernelFetcher): + self.make_tool_spec_editor(python_tool_spec) + opt_widget = self.tool_specification_widget.optional_widget + self.assertTrue(opt_widget.ui.radioButton_jupyter_console.isChecked()) + self.assertEqual(3, opt_widget.kernel_spec_model.rowCount()) + self.assertEqual(1, opt_widget.ui.comboBox_kernel_specs.currentIndex()) + self.assertEqual("python310", opt_widget.ui.comboBox_kernel_specs.currentText()) + self.tool_specification_widget.push_change_kernel_spec_command(2) + self.assertEqual( + "python311", self.tool_specification_widget.spec_dict["execution_settings"]["kernel_spec_name"] + ) + self.assertEqual(2, opt_widget.ui.comboBox_kernel_specs.currentIndex()) + self.assertEqual("python311", opt_widget.ui.comboBox_kernel_specs.currentText()) + self.assertTrue(self.tool_specification_widget.spec_dict["execution_settings"]["use_jupyter_console"]) + # Test SharedToolSpecOptionalWidget._restore_selected_kernel() + # Restore selected kernel after the kernel spec model has been reloaded + opt_widget.start_kernel_fetcher() + self.assertEqual(3, opt_widget.kernel_spec_model.rowCount()) + self.assertEqual(2, opt_widget.ui.comboBox_kernel_specs.currentIndex()) + self.assertEqual("python311", opt_widget.ui.comboBox_kernel_specs.currentText()) + # Test push_set_jupyter_console_mode() + self.tool_specification_widget.push_set_jupyter_console_mode(False) + self.assertFalse(self.tool_specification_widget.spec_dict["execution_settings"]["use_jupyter_console"]) + opt_widget.set_executable("path/to/executable") + self.tool_specification_widget.push_change_executable(opt_widget.get_executable()) + self.assertEqual( + "path/to/executable", self.tool_specification_widget.spec_dict["execution_settings"]["executable"] + ) + self.tool_specification_widget._push_change_args_command("-A -B") + self.assertEqual(["-A", "-B"], self.tool_specification_widget.spec_dict["cmdline_args"]) + + def test_change_executable_spec_options(self): + mock_logger = mock.MagicMock() + batch_file = "hello.bat" + another_batch_file = "hello.sh" + exec_tool_spec = ExecutableTool( + "a", "executable", self._temp_dir.name, [batch_file, "data.file"], MockQSettings(), mock_logger + ) + exec_tool_spec.init_execution_settings() # Sets defaults + self.make_tool_spec_editor(exec_tool_spec) + file_path = Path(self._temp_dir.name, another_batch_file) + parent_main = self.tool_specification_widget.programfiles_model.index(0, 0) # Main program file item + parent_addit = self.tool_specification_widget.programfiles_model.index(1, 0) # Additional program files item + mp_index = self.tool_specification_widget.programfiles_model.index(0, 0, parent_main) + # Try to open main program file + with mock.patch("spine_items.tool.widgets.tool_specification_editor_window.open_url") as mock_open_url: + self.tool_specification_widget.open_program_file(mp_index) # Fails because it's a .bat + mock_open_url.assert_not_called() + # Change main program file + self.tool_specification_widget._push_change_main_program_file_command(file_path) + mp_index = self.tool_specification_widget.programfiles_model.index(0, 0, parent_main) + # Try to open main program file again + with mock.patch("spine_items.tool.widgets.tool_specification_editor_window.open_url") as mock_open_url: + self.tool_specification_widget.open_program_file(mp_index) # Calls open_url() + mock_open_url.assert_called() + # Try removing files without selecting anything + with mock.patch( + "spinetoolbox.project_item.specification_editor_window.SpecificationEditorWindowBase._show_status_bar_msg" + ) as m_notify: + self.tool_specification_widget.remove_program_files() + m_notify.assert_called() + # Set 'data.file' selected + self.assertEqual(1, self.tool_specification_widget.programfiles_model.rowCount(parent_main)) + self.assertEqual(1, self.tool_specification_widget.programfiles_model.rowCount(parent_addit)) + index = self.tool_specification_widget.programfiles_model.index(0, 0, parent_addit) # Index of 'data.file' + selection_model = self.tool_specification_widget._ui.treeView_programfiles.selectionModel() + selection_model.setCurrentIndex(index, QItemSelectionModel.SelectionFlag.Select) + # Remove additional program file 'data.file' + self.tool_specification_widget.remove_program_files() + self.assertEqual(0, self.tool_specification_widget.programfiles_model.rowCount(parent_addit)) + # Remove main program file + self.tool_specification_widget.remove_all_program_files() + self.assertEqual(0, self.tool_specification_widget.programfiles_model.rowCount(parent_main)) + # Do remove_all without selecting anything + with mock.patch( + "spinetoolbox.project_item.specification_editor_window.SpecificationEditorWindowBase._show_status_bar_msg" + ) as m_notify: + self.tool_specification_widget.remove_all_program_files() + m_notify.assert_called() + # Add command for executable tool spec + self.tool_specification_widget.optional_widget.ui.lineEdit_command.setText("ls -a") + self.tool_specification_widget.push_change_executable_command( + self.tool_specification_widget.optional_widget.ui.lineEdit_command.text() + ) + # Check that no shell is selected + self.assertEqual("", self.tool_specification_widget.optional_widget.get_current_shell()) + self.assertEqual("No shell", self.tool_specification_widget.optional_widget.ui.comboBox_shell.currentText()) + # Change shell to cmd.exe on win32, bash for others + if sys.platform == "win32": + index_of_cmd_exe = self.tool_specification_widget.optional_widget.shells.index("cmd.exe") + self.tool_specification_widget.push_change_shell_command(index_of_cmd_exe) + self.assertEqual("cmd.exe", self.tool_specification_widget.optional_widget.get_current_shell()) + else: + index_of_bash = self.tool_specification_widget.optional_widget.shells.index("bash") + self.tool_specification_widget.push_change_shell_command(index_of_bash) + self.assertEqual("bash", self.tool_specification_widget.optional_widget.get_current_shell()) + + def test_change_julia_project(self): + mock_logger = mock.MagicMock() + julia_tool_spec = JuliaTool("a", "julia", self._temp_dir.name, ["hello.jl"], MockQSettings(), mock_logger) + julia_tool_spec.init_execution_settings() # Sets defaults + julia_tool_spec.execution_settings["use_jupyter_console"] = True + with mock.patch("spine_items.tool.widgets.tool_spec_optional_widgets.KernelFetcher", new=FakeKernelFetcher): + self.make_tool_spec_editor(julia_tool_spec) + self.assertEqual("", self.tool_specification_widget.spec_dict["execution_settings"]["project"]) + self.tool_specification_widget.optional_widget.ui.lineEdit_julia_project.setText("path/to/julia_project") + self.tool_specification_widget.push_change_project() + self.assertEqual( + "path/to/julia_project", self.tool_specification_widget.spec_dict["execution_settings"]["project"] + ) + + def test_restore_unknown_saved_kernel_into_optional_widget(self): + mock_logger = mock.MagicMock() + script_file_name = "hello.py" + file_path = Path(self._temp_dir.name, script_file_name) + with open(file_path, "w") as h: + h.writelines(["# hello.py"]) # Make hello.py + python_tool_spec = PythonTool( + "a", "python", self._temp_dir.name, [script_file_name], MockQSettings(), mock_logger + ) + python_tool_spec.init_execution_settings() # Sets defaults + python_tool_spec.execution_settings["use_jupyter_console"] = True + python_tool_spec.execution_settings["kernel_spec_name"] = "unknown_kernel" + with mock.patch( + "spine_items.tool.widgets.tool_spec_optional_widgets.KernelFetcher", new=FakeKernelFetcher + ), mock.patch("spine_items.tool.widgets.tool_spec_optional_widgets.Notification") as mock_notify: + self.make_tool_spec_editor(python_tool_spec) + opt_widget = self.tool_specification_widget.optional_widget + self.assertTrue(opt_widget.ui.radioButton_jupyter_console.isChecked()) + self.assertEqual(3, opt_widget.kernel_spec_model.rowCount()) + self.assertEqual(0, opt_widget.ui.comboBox_kernel_specs.currentIndex()) + self.assertEqual("Select kernel spec...", opt_widget.ui.comboBox_kernel_specs.currentText()) + mock_notify.assert_called() + + def test_program_file_dialogs(self): + mock_logger = mock.MagicMock() + script_file_name = "hello.jl" + script_file_name2 = "hello2.jl" + data_file_name = "data.csv" + file_path = Path(self._temp_dir.name, script_file_name) + file_path2 = Path(self._temp_dir.name, script_file_name2) + file_path3 = Path(self._temp_dir.name, data_file_name) + # Make files so os.path.samefile() works + with open(file_path, "w") as h: + h.writelines(["println('Hello world')"]) # Make hello.jl + with open(file_path2, "w") as h: + h.writelines(["println('Hello world2')"]) # Make hello2.jl + with open(file_path3, "w") as h: + h.writelines(["1, 2, 3"]) # Make data.csv + julia_tool_spec = JuliaTool( + "a", "julia", self._temp_dir.name, [script_file_name, data_file_name], MockQSettings(), mock_logger + ) + julia_tool_spec.init_execution_settings() # Sets defaults + self.make_tool_spec_editor(julia_tool_spec) + self.assertEqual("hello.jl", os.path.split(self.tool_specification_widget._current_main_program_file())[1]) + # Test browse_main_program_file() + with mock.patch( + "spine_items.tool.widgets.tool_specification_editor_window.QFileDialog.getOpenFileName" + ) as mock_fd_gofn: + mock_fd_gofn.return_value = [file_path2] + # Change main program file hello.jl -> hello2.jl + self.tool_specification_widget.browse_main_program_file() + mock_fd_gofn.assert_called() + self.assertEqual("hello2.jl", os.path.split(self.tool_specification_widget._current_main_program_file())[1]) + # Try to change additional program file as the main program, should pop up a QMessageBox + mock_fd_gofn.return_value = [file_path3] + with mock.patch("spine_items.tool.widgets.tool_specification_editor_window.QMessageBox") as mock_mb: + self.tool_specification_widget.browse_main_program_file() + mock_mb.assert_called() + self.assertEqual("hello2.jl", os.path.split(self.tool_specification_widget._current_main_program_file())[1]) + # Test new_main_program_file() + with mock.patch( + "spine_items.tool.widgets.tool_specification_editor_window.QFileDialog.getSaveFileName" + ) as mock_fd_gsfn: + mock_fd_gsfn.return_value = [file_path] + # This should remove existing hello.jl, recreate it, and set it as main program + self.tool_specification_widget.new_main_program_file() + self.assertEqual(1, mock_fd_gsfn.call_count) + self.assertEqual("hello.jl", os.path.split(self.tool_specification_widget._current_main_program_file())[1]) + # Test new_program_file() + parent_addit = self.tool_specification_widget.programfiles_model.index(1, 0) + self.assertEqual(1, self.tool_specification_widget.programfiles_model.rowCount(parent_addit)) + mock_fd_gsfn.return_value = [Path(self._temp_dir.name, "input.txt")] + self.tool_specification_widget.new_program_file() + self.assertEqual(2, mock_fd_gsfn.call_count) + # Check that we now have 2 additional program files + parent_addit = self.tool_specification_widget.programfiles_model.index(1, 0) + self.assertEqual(2, self.tool_specification_widget.programfiles_model.rowCount(parent_addit)) + # Try to add file that's already been added + mock_fd_gsfn.return_value = [file_path3] + with mock.patch( + "spine_items.tool.widgets.tool_specification_editor_window.QMessageBox.information" + ) as mock_mb_info: + self.tool_specification_widget.new_program_file() # QMessageBox should appear + mock_mb_info.assert_called() + self.assertEqual(3, mock_fd_gsfn.call_count) + self.assertEqual(2, self.tool_specification_widget.programfiles_model.rowCount(parent_addit)) + # Test show_add_program_files_dialog() + with mock.patch( + "spine_items.tool.widgets.tool_specification_editor_window.QFileDialog.getOpenFileNames" + ) as mock_fd_gofns: + with mock.patch("spine_items.tool.widgets.tool_specification_editor_window.QMessageBox") as mock_mb: + mock_fd_gofns.return_value = [[file_path]] + self.tool_specification_widget.show_add_program_files_dialog() # Shows 'Can't add main...' msg box + self.assertEqual(1, mock_mb.call_count) + self.assertEqual(1, mock_fd_gofns.call_count) + mock_fd_gofns.return_value = [[file_path, file_path2, file_path3]] + self.tool_specification_widget.show_add_program_files_dialog() # Shows 'One file not add...' msg box + self.assertEqual(2, mock_mb.call_count) + self.assertEqual(2, mock_fd_gofns.call_count) + # Test show_add_program_dirs_dialog() + with mock.patch( + "spine_items.tool.widgets.tool_specification_editor_window.QFileDialog.getExistingDirectory" + ) as mock_fd_ged: + mock_fd_ged.return_value = self._temp_dir.name + self.tool_specification_widget.show_add_program_dirs_dialog() + mock_fd_ged.assert_called() + + def test_add_rename_select_remove_input_and_output_files(self): + mock_logger = mock.MagicMock() + main_file = "hello.jl" + main_path = Path(self._temp_dir.name, main_file) + with open(main_path, "w") as h: + h.writelines(["println('Hello world')"]) # Make hello.jl + julia_tool_spec = JuliaTool("a", "julia", self._temp_dir.name, [main_file], MockQSettings(), mock_logger) + julia_tool_spec.init_execution_settings() # Sets defaults + self.make_tool_spec_editor(julia_tool_spec) + # INPUT FILES + # Test add_input_files() + with mock.patch("spine_items.tool.widgets.tool_specification_editor_window.QInputDialog.getText") as mock_gt: + mock_gt.return_value = ["data.csv"] + self.tool_specification_widget.add_inputfiles() + mock_gt.assert_called() + iofm = self.tool_specification_widget.io_files_model + selection_model = self.tool_specification_widget._ui.treeView_io_files.selectionModel() + self.assertEqual(3, iofm.rowCount()) # row 0: Input files, row 1: Optional input files, row 2: Output files + # There should be one item under 'Input files' + input_files_root = iofm.index(0, 0) + self.assertEqual(1, iofm.rowCount(input_files_root)) + input_file_item = iofm.itemFromIndex(iofm.index(0, 0, input_files_root)) + self.assertEqual("data.csv", input_file_item.data(Qt.ItemDataRole.DisplayRole)) + # Test rename input file (tests _push_io_file_renamed_command()) + input_file_item.setData("renamed_file.csv", Qt.ItemDataRole.DisplayRole) + input_file_item = iofm.itemFromIndex(iofm.index(0, 0, input_files_root)) + self.assertEqual("renamed_file.csv", input_file_item.data(Qt.ItemDataRole.DisplayRole)) + # Try remove_inputfiles() without selections + with mock.patch( + "spinetoolbox.project_item.specification_editor_window.SpecificationEditorWindowBase._show_status_bar_msg" + ) as m_notify: + self.tool_specification_widget.remove_inputfiles() + m_notify.assert_called() + # Select input file item + selection_model.setCurrentIndex(iofm.index(0, 0, input_files_root), QItemSelectionModel.SelectionFlag.Select) + # Test remove_inputfiles() + self.tool_specification_widget.remove_inputfiles() + self.assertEqual(0, iofm.rowCount(input_files_root)) + # OPTIONAL INPUT FILES + # Test add_inputfiles_opt() + with mock.patch("spine_items.tool.widgets.tool_specification_editor_window.QInputDialog.getText") as mock_gt: + mock_gt.return_value = ["*.dat"] + self.tool_specification_widget.add_inputfiles_opt() + mock_gt.assert_called() + # There should be one item under 'Optional input files' + opt_input_files_root = iofm.index(1, 0) + self.assertEqual(1, iofm.rowCount(opt_input_files_root)) + opt_input_file_item = iofm.itemFromIndex(iofm.index(0, 0, opt_input_files_root)) + self.assertEqual("*.dat", opt_input_file_item.data(Qt.ItemDataRole.DisplayRole)) + # Test rename optional input item (tests _push_io_file_renamed_command()) + opt_input_file_item.setData("???.dat", Qt.ItemDataRole.DisplayRole) + opt_input_file_item = iofm.itemFromIndex(iofm.index(0, 0, opt_input_files_root)) + self.assertEqual("???.dat", opt_input_file_item.data(Qt.ItemDataRole.DisplayRole)) + # Try remove_inputfiles_opt() without selections + with mock.patch( + "spinetoolbox.project_item.specification_editor_window.SpecificationEditorWindowBase._show_status_bar_msg" + ) as m_notify: + self.tool_specification_widget.remove_inputfiles_opt() + m_notify.assert_called() + # Select optional input file item + selection_model.setCurrentIndex( + iofm.index(0, 0, opt_input_files_root), QItemSelectionModel.SelectionFlag.Select + ) + # Test remove_inputfiles_opt() + self.tool_specification_widget.remove_inputfiles_opt() + self.assertEqual(0, iofm.rowCount(opt_input_files_root)) + # OUTPUT FILES + # Test add_outputfiles() + with mock.patch("spine_items.tool.widgets.tool_specification_editor_window.QInputDialog.getText") as mock_gt: + mock_gt.return_value = ["results.txt"] + self.tool_specification_widget.add_outputfiles() + mock_gt.assert_called() + # There should be one item under 'Output files' + output_files_root = iofm.index(2, 0) + self.assertEqual(1, iofm.rowCount(output_files_root)) + output_file_item = iofm.itemFromIndex(iofm.index(0, 0, output_files_root)) + self.assertEqual("results.txt", output_file_item.data(Qt.ItemDataRole.DisplayRole)) + # Test rename output file + output_file_item.setData("output.txt", Qt.ItemDataRole.DisplayRole) + output_file_item = iofm.itemFromIndex(iofm.index(0, 0, output_files_root)) + self.assertEqual("output.txt", output_file_item.data(Qt.ItemDataRole.DisplayRole)) + # Try remove_outputfiles() without selections + with mock.patch( + "spinetoolbox.project_item.specification_editor_window.SpecificationEditorWindowBase._show_status_bar_msg" + ) as m_notify: + self.tool_specification_widget.remove_outputfiles() + m_notify.assert_called() + # Select output file item + selection_model.setCurrentIndex(iofm.index(0, 0, output_files_root), QItemSelectionModel.SelectionFlag.Select) + # Test remove_outputfiles() + self.tool_specification_widget.remove_outputfiles() + self.assertEqual(0, iofm.rowCount(output_files_root)) + + +class FakeSignal: + def __init__(self): + self.call_list = list() # List of slots + + def connect(self, method): + """Stores all slots connected to this FakeSignal into a list.""" + self.call_list.append(method) + + +class FakeKernelFetcher: + """Class for replacing KernelFetcher in tests.""" + + kernel_found = FakeSignal() + finished = FakeSignal() + + def __init__(self, conda_path="", fetch_mode=0): + self.conda_path = conda_path + self.fetch_mode = fetch_mode + + def isRunning(self): + return False - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_url_inputs(self): - # self._test_add_cmdline_tag_on_empty_args_field("@@url_inputs@@") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_url_inputs_middle_of_other_tags(self): - # self._test_add_cmdline_tag_middle_of_other_tags("@@url_inputs@@") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_url_inputs_no_space_before_regular_arg(self): - # self._test_add_cmdline_tag_adds_no_space_before_regular_arg("@@url_inputs@@") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_url_outputs(self): - # self._test_add_cmdline_tag_on_empty_args_field("@@url_outputs@@") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_url_outputs_middle_of_other_tags(self): - # self._test_add_cmdline_tag_middle_of_other_tags("@@url_outputs@@") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_url_outputs_no_space_before_regular_arg(self): - # self._test_add_cmdline_tag_adds_no_space_before_regular_arg("@@url_outputs@@") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_data_store_url(self): - # self._test_add_cmdline_tag_on_empty_args_field("@@url:@@") - # selection = self.tool_specification_widget.ui.lineEdit_args.selectedText() - # self.assertEqual(selection, "") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_data_store_url_middle_of_other_tags(self): - # self._test_add_cmdline_tag_middle_of_other_tags("@@url:@@") - # selection = self.tool_specification_widget.ui.lineEdit_args.selectedText() - # self.assertEqual(selection, "") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_data_store_url_no_space_before_regular_arg(self): - # self._test_add_cmdline_tag_adds_no_space_before_regular_arg("@@url:@@") - # selection = self.tool_specification_widget.ui.lineEdit_args.selectedText() - # self.assertEqual(selection, "") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_optional_inputs(self): - # self._test_add_cmdline_tag_on_empty_args_field("@@optional_inputs@@") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_optional_inputs_middle_of_other_tags(self): - # self._test_add_cmdline_tag_middle_of_other_tags("@@optional_inputs@@") - # - # @unittest.skip("Obsolete") - # def test_add_cmdline_tag_optional_inputs_no_space_before_regular_arg(self): - # self._test_add_cmdline_tag_adds_no_space_before_regular_arg("@@optional_inputs@@") - # - # def _find_action(self, action_text, actions): - # found_action = None - # for action in actions: - # if action.text() == action_text: - # found_action = action - # break - # self.assertIsNotNone(found_action) - # return found_action - # - # def _test_add_cmdline_tag_on_empty_args_field(self, tag): - # menu = self.tool_specification_widget.ui.toolButton_add_cmdline_tag.menu() - # url_inputs_action = self._find_action(tag, menu.actions()) - # url_inputs_action.trigger() - # args = self.tool_specification_widget.ui.lineEdit_args.text() - # expected = tag + " " - # self.assertEqual(args, expected) - # if not self.tool_specification_widget.ui.lineEdit_args.hasSelectedText(): - # cursor_position = self.tool_specification_widget.ui.lineEdit_args.cursorPosition() - # self.assertEqual(cursor_position, len(expected)) - # - # def _test_add_cmdline_tag_middle_of_other_tags(self, tag): - # self.tool_specification_widget.ui.lineEdit_args.setText("@@optional_inputs@@@@url_outputs@@") - # self.tool_specification_widget.ui.lineEdit_args.setCursorPosition(len("@@optional_inputs@@")) - # menu = self.tool_specification_widget.ui.toolButton_add_cmdline_tag.menu() - # url_inputs_action = self._find_action(tag, menu.actions()) - # url_inputs_action.trigger() - # args = self.tool_specification_widget.ui.lineEdit_args.text() - # self.assertEqual(args, f"@@optional_inputs@@ {tag} @@url_outputs@@") - # if not self.tool_specification_widget.ui.lineEdit_args.hasSelectedText(): - # cursor_position = self.tool_specification_widget.ui.lineEdit_args.cursorPosition() - # self.assertEqual(cursor_position, len(f"@@optional_inputs@@ {tag} ")) - # - # def _test_add_cmdline_tag_adds_no_space_before_regular_arg(self, tag): - # self.tool_specification_widget.ui.lineEdit_args.setText("--tag=") - # self.tool_specification_widget.ui.lineEdit_args.setCursorPosition(len("--tag=")) - # menu = self.tool_specification_widget.ui.toolButton_add_cmdline_tag.menu() - # url_inputs_action = self._find_action(tag, menu.actions()) - # url_inputs_action.trigger() - # args = self.tool_specification_widget.ui.lineEdit_args.text() - # self.assertEqual(args, f"--tag={tag} ") - # if not self.tool_specification_widget.ui.lineEdit_args.hasSelectedText(): - # cursor_position = self.tool_specification_widget.ui.lineEdit_args.cursorPosition() - # self.assertEqual(cursor_position, len(f"--tag={tag} ")) + def start(self): + for m in self.kernel_found.call_list: + # Calls SharedToolSpecOptionalWidget.add_kernel() + m("python310", "", False, QIcon(), dict()) + m("python311", "", False, QIcon(), dict()) + for meth in self.finished.call_list: + # Calls two methods: + # 1. Either SharedToolSpecOptionalWidget._restore_saved_kernel() or + # SharedToolSpecOptionalWidget._restore_selected_kernel() + # 2. mock.restore_overrider_cursor() + # Note: The order of connect() calls matters. + meth() + # Clear signal slots, so this can be used again + self.kernel_found.call_list.clear() + self.finished.call_list.clear() if __name__ == "__main__": diff --git a/tests/view/__init__.py b/tests/view/__init__.py index 046e2d6d..0294c597 100644 --- a/tests/view/__init__.py +++ b/tests/view/__init__.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,7 +10,4 @@ # this program. If not, see . ###################################################################################################################### -""" -Init file for tests.project_items.view package. Intentionally empty. - -""" +"""Init file for tests.project_items.view package. Intentionally empty.""" diff --git a/tests/view/test_ItemInfo.py b/tests/view/test_ItemInfo.py index 032a1692..701f971c 100644 --- a/tests/view/test_ItemInfo.py +++ b/tests/view/test_ItemInfo.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for View's ItemInfo class. - -""" +"""Unit tests for View's ItemInfo class.""" import unittest from spine_items.view.item_info import ItemInfo @@ -21,9 +19,6 @@ class TestItemInfo(unittest.TestCase): def test_item_type(self): self.assertEqual(ItemInfo.item_type(), "View") - def test_item_category(self): - self.assertEqual(ItemInfo.item_category(), "Views") - if __name__ == "__main__": unittest.main() diff --git a/tests/view/test_View.py b/tests/view/test_View.py index 7823a7a3..7dc3f383 100644 --- a/tests/view/test_View.py +++ b/tests/view/test_View.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,11 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for View project item. - -""" - +"""Unit tests for View project item.""" import os from tempfile import TemporaryDirectory import unittest @@ -49,9 +46,6 @@ def setUpClass(cls): def test_item_type(self): self.assertEqual(View.item_type(), ItemInfo.item_type()) - def test_item_category(self): - self.assertEqual(View.item_category(), ItemInfo.item_category()) - def test_item_dict(self): """Tests Item dictionary creation.""" d = self.view.item_dict() @@ -94,7 +88,7 @@ def test_rename(self): self.view.rename(expected_name, "") # Check name self.assertEqual(expected_name, self.view.name) # item name - self.assertEqual(expected_name, self.view.get_icon().name_item.text()) # name item on Design View + self.assertEqual(expected_name, self.view.get_icon().name()) # name item on Design View # Check data_dir expected_data_dir = os.path.join(self.project.items_dir, expected_short_name) self.assertEqual(expected_data_dir, self.view.data_dir) # Check data dir diff --git a/tests/view/test_ViewExecutable.py b/tests/view/test_ViewExecutable.py index c3389e6c..e8241c89 100644 --- a/tests/view/test_ViewExecutable.py +++ b/tests/view/test_ViewExecutable.py @@ -1,5 +1,6 @@ ###################################################################################################################### # Copyright (C) 2017-2022 Spine project consortium +# Copyright Spine Items contributors # This file is part of Spine Items. # Spine Items 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) @@ -9,10 +10,7 @@ # this program. If not, see . ###################################################################################################################### -""" -Unit tests for ViewExecutable. - -""" +"""Unit tests for ViewExecutable.""" from multiprocessing import Lock from tempfile import TemporaryDirectory import unittest