Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issues with Importer's source selector dialog #245

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions spine_items/importer/widgets/import_editor_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -106,7 +118,7 @@
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()
Expand Down Expand Up @@ -208,26 +220,26 @@
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)

Check warning on line 236 in spine_items/importer/widgets/import_editor_window.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/widgets/import_editor_window.py#L236

Added line #L236 was not covered by tests
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)

Check warning on line 242 in spine_items/importer/widgets/import_editor_window.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/widgets/import_editor_window.py#L242

Added line #L242 was not covered by tests
return connector

@Slot()
Expand Down Expand Up @@ -263,9 +275,16 @@

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(

Check warning on line 282 in spine_items/importer/widgets/import_editor_window.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/widgets/import_editor_window.py#L282

Added line #L282 was not covered by tests
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
Expand All @@ -275,6 +294,17 @@
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(

Check warning on line 301 in spine_items/importer/widgets/import_editor_window.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/widgets/import_editor_window.py#L301

Added line #L301 was not covered by tests
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()

Check warning on line 307 in spine_items/importer/widgets/import_editor_window.py

View check run for this annotation

Codecov / codecov/patch

spine_items/importer/widgets/import_editor_window.py#L307

Added line #L307 was not covered by tests
connector_name = connector.__name__
if self._is_database_connector(connector):
self._ui.source_label.setText("URL:")
Expand Down
2 changes: 1 addition & 1 deletion tests/data_transformer/test_DataTransformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/exporter/test_Exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/exporter/widgets/test_specification_editor_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/importer/test_Importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions tests/importer/widgets/test_ImportPreview_Window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions tests/importer/widgets/test_import_editor_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading