Skip to content

Commit

Permalink
Replace DB codenames by DB manager's database name registry
Browse files Browse the repository at this point in the history
Data stores are no longer responsible of Database editor's tab
names. They merely register their names as candidates for
database display names but it is up to Toolbox what it chooses
to do with the information. This simplifies code and responsibilities
in spine_items.
  • Loading branch information
soininen committed Nov 1, 2024
1 parent 3b170a5 commit 0c58908
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 34 deletions.
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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-Database-API.git@drop_codenames#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 git+https://github.com/spine-tools/Spine-Toolbox.git@drop_codenames#egg=spinetoolbox
-e .
25 changes: 18 additions & 7 deletions spine_items/data_store/data_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from spinedb_api.helpers import remove_credentials_from_url, vacuum
from spinetoolbox.helpers import create_dir
from spinetoolbox.project_item.project_item import ProjectItem
from spinetoolbox.spine_db_editor.editors import db_editor_registry
from spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor import open_db_editor
from spinetoolbox.widgets.custom_qwidgets import SelectDatabaseItemsDialog
from ..database_validation import DatabaseConnectionValidator
from ..utils import convert_to_sqlalchemy_url, database_label
Expand Down Expand Up @@ -81,7 +83,7 @@ def get_db_map(self):
sa_url = self.sql_alchemy_url()
if sa_url is None:
return None
return self._toolbox.db_mngr.get_db_map(sa_url, self._logger, codename=None)
return self._toolbox.db_mngr.get_db_map(sa_url, self._logger)

@staticmethod
def item_type():
Expand All @@ -104,7 +106,7 @@ def parse_url(self, url):
if isinstance(url, dict):
if url.get("dialect") == "sqlite" and (database := url.get("database")):
# Convert relative database path back to absolute
url["database"] = os.path.abspath(os.path.join(self._project.project_dir, database))
url["database"] = os.path.normcase(os.path.abspath(os.path.join(self._project.project_dir, database)))
for key, value in url.items():
if value is not None:
base_url[key] = value
Expand Down Expand Up @@ -203,6 +205,8 @@ def do_update_url(self, **kwargs):
was_valid = self._url_validated
self._url_validated = False
old_url = convert_to_sqlalchemy_url(self._url, self.name)
if old_url is not None and was_valid:
self._toolbox.db_mngr.name_registry.unregister(old_url, self.name)
new_dialect = kwargs.get("dialect")
if new_dialect == "sqlite":
kwargs.update({"username": "", "password": "", "host": "", "port": "", "schema": ""})
Expand Down Expand Up @@ -271,7 +275,7 @@ def _clean_up_purge_dialog(self):
self._purge_dialog = None

def actions(self):
self._multi_db_editors_open = {x.name(): x for x in self._toolbox.db_mngr.get_all_multi_spine_db_editors()}
self._multi_db_editors_open = {x.name(): x for x in db_editor_registry.windows()}
if not self._multi_db_editors_open:
return super().actions()
self._open_url_menu.clear()
Expand Down Expand Up @@ -311,8 +315,7 @@ def _open_spine_db_editor(self, reuse_existing):
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, reuse_existing)
open_db_editor([sa_url], self._toolbox.db_mngr, reuse_existing)
self._check_notifications()

def _open_url_in_existing_db_editor(self, db_editor):
Expand All @@ -321,7 +324,7 @@ def _open_url_in_existing_db_editor(self, db_editor):
return
sa_url = self.sql_alchemy_url()
if sa_url is not None:
db_editor.add_new_tab({sa_url: self.name})
db_editor.add_new_tab([sa_url])
db_editor.raise_()
db_editor.activateWindow()

Expand Down Expand Up @@ -429,6 +432,7 @@ def _accept_url(self, url):
self._update_actions_enabled()
db_map = self.get_db_map()
if db_map:
self._toolbox.db_mngr.name_registry.register(db_map.sa_url, self.name)
clean = not self._toolbox.db_mngr.is_dirty(db_map)
self._notify_about_dirtiness(clean)

Expand Down Expand Up @@ -491,10 +495,13 @@ 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_data_dir = os.path.abspath(self.data_dir)
old_name = self.name
if not super().rename(new_name, rename_data_dir_message):
return False
if self._url_validated:
sa_url = self.sql_alchemy_url()
self._toolbox.db_mngr.name_registry.unregister(sa_url, old_name)
# If dialect is sqlite and db line edit refers to a file in the old data_dir, db line edit needs updating
if self._url["dialect"] == "sqlite":
db_dir, db_filename = os.path.split(os.path.abspath(self._url["database"].strip()))
Expand All @@ -504,6 +511,7 @@ def rename(self, new_name, rename_data_dir_message):
if os.path.exists(database):
self._url.update(database=database)
self.load_url_into_selections(self._url)
self._check_notifications()
return True

def notify_destination(self, source_item):
Expand Down Expand Up @@ -543,6 +551,9 @@ def resources_for_direct_predecessors(self):

def tear_down(self):
"""See base class"""
if self._url_validated:
sa_url = convert_to_sqlalchemy_url(self._url, self.name)
self._toolbox.db_mngr.name_registry.unregister(sa_url, self.name)
self._toolbox.db_mngr.database_clean_changed.disconnect(self._set_database_clean)
self._database_validator.wait_for_finish()
super().tear_down()
2 changes: 1 addition & 1 deletion spine_items/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _convert_url(url):
if dialect == "sqlite":
database = url.get("database", "")
if database:
url["database"] = os.path.abspath(database)
url["database"] = os.path.normcase(os.path.abspath(database))
return URL("sqlite", **url) # pylint: disable=unexpected-keyword-arg
db_api = spinedb_api.SUPPORTED_DIALECTS.get(dialect)
if db_api is None:
Expand Down
29 changes: 15 additions & 14 deletions spine_items/view/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from spinetoolbox.helpers import busy_effect
from spinetoolbox.plotting import PlottingError, plot_db_mngr_items
from spinetoolbox.project_item.project_item import ProjectItem
from spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor import open_db_editor
from spinetoolbox.spine_db_editor.widgets.spine_db_editor import SpineDBEditor
from spinetoolbox.widgets.notification import Notification
from .commands import PinOrUnpinDBValuesCommand
Expand Down Expand Up @@ -89,19 +90,19 @@ def save_selections(self):
def open_editor(self, checked=False):
"""Opens selected db in the Spine database editor."""
indexes = self._selected_reference_indexes()
db_url_codenames = self._db_url_codenames(indexes)
if not db_url_codenames:
db_urls = self._db_urls(indexes)
if not db_urls:
return
self._toolbox.db_mngr.open_db_editor(db_url_codenames, reuse_existing_editor=True)
open_db_editor(db_urls, self._toolbox.db_mngr, reuse_existing_editor=True)

@Slot(bool)
def pin_values(self, checked=False):
"""Opens selected db in the Spine database editor to pin values."""
indexes = self._selected_reference_indexes()
db_url_codenames = self._db_url_codenames(indexes)
if not db_url_codenames:
db_urls = self._db_urls(indexes)
if not db_urls:
return
db_editor = SpineDBEditor(self._toolbox.db_mngr, db_url_codenames)
db_editor = SpineDBEditor(self._toolbox.db_mngr, db_urls)
dialog = _PinValuesDialog(self, db_editor)
db_editor.pinned_values_updated.connect(dialog.update_pinned_values)
dialog.data_committed.connect(self._pin_db_values)
Expand All @@ -115,7 +116,7 @@ def selected_pinned_values(self):
return self._properties_ui.treeView_pinned_values.selectedIndexes()

def reference_resource_label_from_url(self, url):
for label, (ref_url, _) in self._references.items():
for label, ref_url in self._references.items():
if str(ref_url) == str(url):
return label
self._logger.msg_error.emit(f"<b>{self.name}</b>: Can't find any resource having url {url}.")
Expand Down Expand Up @@ -203,7 +204,7 @@ def add_to_plot(self, fetch_id, db_map, parameter_record):
def failed_to_plot(self, fetch_id, db_map, conditions):
self._logger.msg_warning.emit(
f"<b>{self.name}</b>: "
f"Couldn't find any values having {_pk_to_ul(conditions)} in <b>{db_map.codename}</b>"
f"Couldn't find any values having {_pk_to_ul(conditions)} in <b>{self._toolbox.db_mngr.name_registry.display_name(db_map.sa_url)}</b>"
)
if not self._fetched_parameter_values:
self._fetched_parameter_values = len(self._data_fetchers) * [None]
Expand All @@ -218,7 +219,7 @@ def _plot_if_all_fetched(self):
if any(v != "skip" for v in self._fetched_parameter_values):
plot_db_maps, plot_items = zip(*existing_fetched_parameter_values)
try:
plot_widget = plot_db_mngr_items(plot_items, plot_db_maps)
plot_widget = plot_db_mngr_items(plot_items, plot_db_maps, self._toolbox.db_mngr.name_registry)
except PlottingError as error:
self._logger.msg_error.emit(str(error))
return
Expand Down Expand Up @@ -279,12 +280,12 @@ def upstream_resources_updated(self, resources):
for resource in resources:
if resource.type_ == "database":
url = make_url(resource.url)
self._references[resource.label] = (url, resource.provider_name)
self._references[resource.label] = url
elif resource.type_ == "file":
filepath = resource.path
if os.path.splitext(filepath)[1] == ".sqlite":
url = URL("sqlite", database=filepath)
self._references[resource.label] = (url, resource.provider_name)
self._references[resource.label] = url
self.populate_reference_list()

def replace_resources_from_upstream(self, old, new):
Expand Down Expand Up @@ -321,9 +322,9 @@ def _selected_pinned_value_indexes(self):
self._properties_ui.treeView_pinned_values.selectAll()
return self._properties_ui.treeView_pinned_values.selectionModel().selectedRows()

def _db_url_codenames(self, indexes):
"""Returns a dict mapping url to provider's name for given indexes in the reference model."""
return dict(self._references[index.data(Qt.ItemDataRole.DisplayRole)] for index in indexes)
def _db_urls(self, indexes):
"""Returns a list of database URLs for given indexes in the reference model."""
return [self._references[index.data(Qt.ItemDataRole.DisplayRole)] for index in indexes]

def notify_destination(self, source_item):
"""See base class."""
Expand Down
20 changes: 10 additions & 10 deletions tests/data_store/test_dataStore.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,14 +345,14 @@ def test_open_db_editor1(self):
self.ds_properties_ui.url_selector_widget.set_url({"dialect": "sqlite", "database": temp_db_path})
self.ds._update_url_from_properties()
self.toolbox.db_mngr = mock.MagicMock()
self.toolbox.db_mngr.get_all_multi_spine_db_editors = lambda: iter([])
while not self.ds.is_url_validated():
QApplication.processEvents()
self.assertTrue(self.ds_properties_ui.pushButton_ds_open_editor.isEnabled())
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"}, True)
with mock.patch("spine_items.data_store.data_store.open_db_editor") as open_db_editor:
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)
open_db_editor.assert_called_with([sa_url], self.toolbox.db_mngr, True)

def test_open_db_editor2(self):
"""Test that selecting the 'sqlite' dialect, typing the path to an existing db file,
Expand All @@ -363,14 +363,14 @@ def test_open_db_editor2(self):
self.ds_properties_ui.url_selector_widget.set_url({"dialect": "sqlite", "database": temp_db_path})
self.ds._update_url_from_properties()
self.toolbox.db_mngr = mock.MagicMock()
self.toolbox.db_mngr.get_all_multi_spine_db_editors = lambda: iter([])
while not self.ds.is_url_validated():
QApplication.processEvents()
self.assertTrue(self.ds_properties_ui.pushButton_ds_open_editor.isEnabled())
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"}, True)
with mock.patch("spine_items.data_store.data_store.open_db_editor") as open_db_editor:
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)
open_db_editor.assert_called_with([sa_url], self.toolbox.db_mngr, True)

def test_notify_destination(self):
self.ds.logger.msg = mock.MagicMock()
Expand Down

0 comments on commit 0c58908

Please sign in to comment.