diff --git a/spine_items/importer/widgets/import_editor_window.py b/spine_items/importer/widgets/import_editor_window.py index f68a4897..71f0b3a3 100644 --- a/spine_items/importer/widgets/import_editor_window.py +++ b/spine_items/importer/widgets/import_editor_window.py @@ -14,8 +14,8 @@ import fnmatch import json import os -from PySide6.QtCore import QItemSelectionModel, QModelIndex, Qt, Signal, Slot -from PySide6.QtWidgets import QDialog, QDialogButtonBox, QFileDialog, QListWidget, QVBoxLayout +from PySide6.QtCore import QItemSelectionModel, QModelIndex, Qt, QTimer, Signal, Slot +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QFileDialog, QListWidget, QMessageBox, QVBoxLayout from spinedb_api.helpers import remove_credentials_from_url from spinedb_api.spine_io.gdx_utils import find_gams_directory from spinedb_api.spine_io.importers.csv_reader import CSVConnector @@ -39,6 +39,18 @@ from .import_mappings import ImportMappings from .import_sources import ImportSources + +class _ConnectorProblemInMapping(Exception): + """Raised when mapping has no connector or the connector looks different to file type.""" + + def __init__(self, connector_in_mapping): + """ + Args: + connector_in_mapping (type, optional): connector class defined in mapping + """ + self.connector_in_mapping = connector_in_mapping + + _CONNECTOR_NAME_TO_CLASS = { klass.__name__: klass for klass in (CSVConnector, ExcelConnector, GdxConnector, JSONConnector, DataPackageConnector, SqlAlchemyConnector) @@ -106,7 +118,7 @@ def __init__(self, toolbox, specification, item=None, source=None, source_extras self.connection_failed.connect(self.show_error) self._import_sources.preview_data_updated.connect(self._import_mapping_options.set_num_available_columns) self._mappings_model.restore(self.specification.mapping if self.specification is not None else {}) - self.start_ui() + QTimer.singleShot(0, self.start_ui) def is_file_less(self): return not self._ui.source_line_edit.text() @@ -208,26 +220,26 @@ def _switch_connector(self, _=False): self.start_ui() def _get_connector_from_mapping(self, source): - """Guesses connector for given source. + """Reads connector for given source from mapping. Args: source (str): importee file path or URL Returns: - type: connector class, or None if no suitable connector was found + type: connector class """ if not self.specification: - return None + raise _ConnectorProblemInMapping(None) mapping = self.specification.mapping source_type = mapping.get("source_type") if source_type is None: - return None + raise _ConnectorProblemInMapping(None) connector = _CONNECTOR_NAME_TO_CLASS[source_type] file_extensions = connector.FILE_EXTENSIONS.split(";;") if source != self._FILE_LESS and not any(fnmatch.fnmatch(source, ext) for ext in file_extensions): if connector is SqlAlchemyConnector and self._is_url(source): return connector - return None + raise _ConnectorProblemInMapping(connector) return connector @Slot() @@ -263,9 +275,16 @@ def _maybe_switch_to_file_less_mode(self, text): def start_ui(self): """Connects to source and fills the tables and lists with data.""" - connector = self._get_connector_from_mapping(self._source) - if connector is None: - # Ask user + try: + connector = self._get_connector_from_mapping(self._source) + except _ConnectorProblemInMapping as connector_problem: + if connector_problem.connector_in_mapping is not None: + QMessageBox.warning( + self, + "Verify source type", + f"Source type is set to {connector_problem.connector_in_mapping.DISPLAY_NAME} but the source looks incompatible. " + "You will be prompted to verify the type.", + ) connector = self._get_connector(self._source) if not connector: return @@ -275,6 +294,17 @@ def start_ui(self): self._ui.source_line_edit.clear() self._source = self._FILE_LESS self._source_extras = None + if ( + connector_problem.connector_in_mapping is not None + and connector is not connector_problem.connector_in_mapping + ): + QMessageBox.information( + self, + "Source type changed", + f"Source type changed from {connector_problem.connector_in_mapping.DISPLAY_NAME} to {connector.DISPLAY_NAME}. " + "Don't forget to save the changes.", + ) + self._undo_stack.resetClean() connector_name = connector.__name__ if self._is_database_connector(connector): self._ui.source_label.setText("URL:") diff --git a/tests/data_transformer/test_DataTransformer.py b/tests/data_transformer/test_DataTransformer.py index 6d1ac2d2..85c6be08 100644 --- a/tests/data_transformer/test_DataTransformer.py +++ b/tests/data_transformer/test_DataTransformer.py @@ -24,7 +24,7 @@ from spine_items.data_transformer.item_info import ItemInfo from spine_items.data_transformer.settings import EntityClassRenamingSettings from spinedb_api import append_filter_config -from ..mock_helpers import create_mock_project, create_mock_toolbox, mock_finish_project_item_construction +from tests.mock_helpers import create_mock_project, create_mock_toolbox, mock_finish_project_item_construction class TestDataTransformer(unittest.TestCase): diff --git a/tests/exporter/test_Exporter.py b/tests/exporter/test_Exporter.py index d8be21d2..5a7d599a 100644 --- a/tests/exporter/test_Exporter.py +++ b/tests/exporter/test_Exporter.py @@ -26,7 +26,7 @@ from spine_items.utils import database_label from spinedb_api import create_new_spine_database from spinetoolbox.project_item.logging_connection import LoggingConnection -from ..mock_helpers import ( +from tests.mock_helpers import ( clean_up_toolbox, create_mock_project, create_mock_toolbox, diff --git a/tests/exporter/widgets/test_specification_editor_window.py b/tests/exporter/widgets/test_specification_editor_window.py index d5335c77..47ee7c54 100644 --- a/tests/exporter/widgets/test_specification_editor_window.py +++ b/tests/exporter/widgets/test_specification_editor_window.py @@ -22,7 +22,7 @@ from spinedb_api.export_mapping.export_mapping import EntityClassMapping, FixedValueMapping 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 +from tests.mock_helpers import clean_up_toolbox, create_toolboxui_with_project class TestSpecificationEditorWindow(unittest.TestCase): diff --git a/tests/importer/test_Importer.py b/tests/importer/test_Importer.py index eb6dddf4..7f93f849 100644 --- a/tests/importer/test_Importer.py +++ b/tests/importer/test_Importer.py @@ -23,7 +23,7 @@ from spine_items.importer.importer_factory import ImporterFactory from spine_items.importer.importer_specification import ImporterSpecification from spine_items.importer.item_info import ItemInfo -from ..mock_helpers import create_mock_project, create_mock_toolbox, mock_finish_project_item_construction +from tests.mock_helpers import create_mock_project, create_mock_toolbox, mock_finish_project_item_construction class TestImporter(unittest.TestCase): diff --git a/tests/importer/widgets/test_ImportPreview_Window.py b/tests/importer/widgets/test_ImportPreview_Window.py index 0b69e514..26b59f48 100644 --- a/tests/importer/widgets/test_ImportPreview_Window.py +++ b/tests/importer/widgets/test_ImportPreview_Window.py @@ -34,6 +34,7 @@ def test_closeEvent(self): toolbox.qsettings = mock.MagicMock(return_value=QSettings(toolbox)) toolbox.restore_and_activate = mock.MagicMock() widget = ImportEditorWindow(toolbox, spec) + QApplication.processEvents() # Let QTimer call ImportEditorWindow.start_ui(). widget._app_settings = mock.NonCallableMagicMock() widget.close() widget._app_settings.beginGroup.assert_called_once_with("mappingPreviewWindow") diff --git a/tests/importer/widgets/test_import_editor_window.py b/tests/importer/widgets/test_import_editor_window.py index c2c2f1ee..b8674c78 100644 --- a/tests/importer/widgets/test_import_editor_window.py +++ b/tests/importer/widgets/test_import_editor_window.py @@ -48,6 +48,7 @@ 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) + QApplication.processEvents() # Let QTimer call ImportEditorWindow.start_ui() exec_dialog.assert_called_once() exec_dialog.reset_mock() connector = editor._get_connector("mysql://server.com/db")