Skip to content

Commit

Permalink
Drop DB codenames, crystallize DB editor naming responsibilities
Browse files Browse the repository at this point in the history
Drop database codenames in favor of specific database display name
registry that centralizes all database name handling. This makes
it clearer who is responsible of giving names to DB editor tabs.
Also, moved DB editor window/tab management out of DB manager where
it arguable doesn't belong.
  • Loading branch information
soininen committed Nov 1, 2024
1 parent 18814fa commit e85ae72
Show file tree
Hide file tree
Showing 63 changed files with 824 additions and 460 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-items.git#egg=spine_items
-e git+https://github.com/spine-tools/spine-items.git@drop_codenames#egg=spine_items
-e .
120 changes: 120 additions & 0 deletions spinetoolbox/database_display_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox 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)
# 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 <http://www.gnu.org/licenses/>.
######################################################################################################################

"""This module contains functionality to manage database display names."""
import hashlib
import pathlib
from PySide6.QtCore import QObject, Signal, Slot
from sqlalchemy.engine.url import URL, make_url


class NameRegistry(QObject):
display_name_changed = Signal(str, str)
"""Emitted when the display name of a database changes."""

def __init__(self, parent=None):
"""
Args:
parent (QObject, optional): parent object
"""
super().__init__(parent)
self._names_by_url: dict[str, set[str]] = {}

@Slot(str, str)
def register(self, db_url, name):
"""Registers a new name for given database URL.
Args:
db_url (URL or str): database URL
name (str): name to register
"""
url = str(db_url)
if url in self._names_by_url and name in self._names_by_url[url]:
return
self._names_by_url.setdefault(url, set()).add(name)
self.display_name_changed.emit(url, self.display_name(db_url))

@Slot(str, str)
def unregister(self, db_url, name):
"""Removes a name from the registry.
Args:
db_url (URL or str): database URL
name (str): name to remove
"""
url = str(db_url)
names = self._names_by_url[url]
old_name = self.display_name(url) if len(names) in (1, 2) else None
names.remove(name)
if old_name is not None:
new_name = self.display_name(url)
self.display_name_changed.emit(url, new_name)

def display_name(self, db_url):
"""Makes display name for a database.
Args:
db_url (URL or str): database URL
Returns:
str: display name
"""
try:
registered_names = self._names_by_url[str(db_url)]
except KeyError:
return suggest_display_name(db_url)
else:
if len(registered_names) == 1:
return next(iter(registered_names))
return suggest_display_name(db_url)

def display_name_iter(self, db_maps):
"""Yields database mapping display names.
Args:
db_maps (Iterable of DatabaseMapping): database mappings
Yields:
str: display name
"""
yield from (self.display_name(db_map.sa_url) for db_map in db_maps)

def map_display_names_to_db_maps(self, db_maps):
"""Returns a dictionary that maps display names to database mappings.
Args:
db_maps (Iterable of DatabaseMapping): database mappings
Returns:
dict: database mappings keyed by display names
"""
return {self.display_name(db_map.sa_url): db_map for db_map in db_maps}


def suggest_display_name(db_url):
"""Returns a short name for the database mapping.
Args:
db_url (URL or str): database URL
Returns:
str: suggested name for the database for display purposes.
"""
if not isinstance(db_url, URL):
db_url = make_url(db_url)
if not db_url.drivername.startswith("sqlite"):
return db_url.database
if db_url.database is not None:
return pathlib.Path(db_url.database).stem
hashing = hashlib.sha1()
hashing.update(bytes(str(id(db_url)), "utf-8"))
return hashing.hexdigest()
71 changes: 71 additions & 0 deletions spinetoolbox/multi_tab_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox 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)
# 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 <http://www.gnu.org/licenses/>.
######################################################################################################################

"""Contains functionality to keep track on open MultiTabWindow instances."""
from spinetoolbox.widgets.multi_tab_window import MultiTabWindow


class MultiTabWindowRegistry:
"""Registry that holds multi tab windows."""

def __init__(self):
self._multi_tab_windows: list[MultiTabWindow] = []

def has_windows(self):
"""Tests if there are any windows registered.
Returns:
bool: True if editor windows exist, False otherwise
"""
return bool(self._multi_tab_windows)

def windows(self):
"""Returns a list of multi tab windows.
Returns:
list of MultiTabWindow: windows
"""
return list(self._multi_tab_windows)

def tabs(self):
"""Returns a list of tabs across all windows.
Returns:
list of QWidget: tab widgets
"""
return [
window.tab_widget.widget(k) for window in self._multi_tab_windows for k in range(window.tab_widget.count())
]

def register_window(self, window):
"""Registers a new multi tab window.
Args:
window (MultiTabWindow): window to register
"""
self._multi_tab_windows.append(window)

def unregister_window(self, window):
"""Removes multi tab window from the registry.
Args:
window (MultiTabWindow): window to unregister
"""
self._multi_tab_windows.remove(window)

def get_some_window(self):
"""Returns a random multi tab window or None if none is available.
Returns:
MultiTabWindow: editor window
"""
return self._multi_tab_windows[0] if self._multi_tab_windows else None
7 changes: 4 additions & 3 deletions spinetoolbox/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,12 +692,13 @@ def plot_pivot_table_selection(model, model_indexes, plot_widget=None):
return plot_data(data_list, plot_widget)


def plot_db_mngr_items(items, db_maps, plot_widget=None):
def plot_db_mngr_items(items, db_maps, db_name_registry, plot_widget=None):
"""Returns a plot widget with plots of database manager parameter value items.
Args:
items (list of dict): parameter value items
db_maps (list of DatabaseMapping): database mappings corresponding to items
db_name_registry (NameRegistry): database display name registry
plot_widget (PlotWidget, optional): widget to add plots to
"""
if not items:
Expand All @@ -707,13 +708,13 @@ def plot_db_mngr_items(items, db_maps, plot_widget=None):
root_node = TreeNode("database")
for item, db_map in zip(items, db_maps):
value = from_database(item["value"], item["type"])
db_name = db_name_registry.display_name(db_map.sa_url)
if value is None:
continue
try:
leaf_content = _convert_to_leaf(value)
except PlottingError as error:
raise PlottingError(f"Failed to plot value in {db_map.codename}: {error}") from error
db_name = db_map.codename
raise PlottingError(f"Failed to plot value in {db_name}: {error}") from error
parameter_name = item["parameter_definition_name"]
entity_byname = item["entity_byname"]
if not isinstance(entity_byname, tuple):
Expand Down
8 changes: 4 additions & 4 deletions spinetoolbox/spine_db_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs):
self.redo_data = data
self.undo_ids = None
self._check = check
self.setText(f"add {item_type} items to {db_map.codename}")
self.setText(f"add {item_type} items to {db_mngr.name_registry.display_name(db_map.sa_url)}")

def redo(self):
super().redo()
Expand Down Expand Up @@ -143,7 +143,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs):
if self.redo_data == self.undo_data:
self.setObsolete(True)
self._check = check
self.setText(f"update {item_type} items in {db_map.codename}")
self.setText(f"update {item_type} items in {self.db_mngr.name_registry.display_name(db_map.sa_url)}")

def redo(self):
super().redo()
Expand Down Expand Up @@ -183,7 +183,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs):
self.redo_update_data = None
self.undo_remove_ids = None
self.undo_update_data = None
self.setText(f"update {item_type} items in {db_map.codename}")
self.setText(f"update {item_type} items in {self.db_mngr.name_registry.display_name(db_map.sa_url)}")

def redo(self):
super().redo()
Expand Down Expand Up @@ -225,7 +225,7 @@ def __init__(self, db_mngr, db_map, item_type, ids, check=True, **kwargs):
self.item_type = item_type
self.ids = ids
self._check = check
self.setText(f"remove {item_type} items from {db_map.codename}")
self.setText(f"remove {item_type} items from {self.db_mngr.name_registry.display_name(db_map.sa_url)}")

def redo(self):
super().redo()
Expand Down
16 changes: 16 additions & 0 deletions spinetoolbox/spine_db_editor/editors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox 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)
# 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 <http://www.gnu.org/licenses/>.
######################################################################################################################

"""Contains Spine Database editor's window registry."""
from spinetoolbox.multi_tab_windows import MultiTabWindowRegistry

db_editor_registry = MultiTabWindowRegistry()
12 changes: 8 additions & 4 deletions spinetoolbox/spine_db_editor/graphics_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def display_data(self):

@property
def display_database(self):
return ",".join([db_map.codename for db_map in self.db_maps])
return ", ".join(self.db_mngr.name_registry.display_name_iter(self.db_maps))

@property
def db_maps(self):
Expand Down Expand Up @@ -370,7 +370,7 @@ def default_parameter_data(self):
return {
"entity_class_name": self.entity_class_name,
"entity_byname": DB_ITEM_SEPARATOR.join(self.byname),
"database": self.first_db_map.codename,
"database": self.db_mngr.name_registry.display_name(self.first_db_map.sa_url),
}

def shape(self):
Expand Down Expand Up @@ -667,7 +667,7 @@ def _populate_connect_entities_menu(self, menu):
for name, db_map_ent_clss in self._db_map_entity_class_lists.items():
for db_map, ent_cls in db_map_ent_clss:
icon = self.db_mngr.entity_class_icon(db_map, ent_cls["id"])
action_name = name + "@" + db_map.codename
action_name = name + "@" + self.db_mngr.name_registry.display_name(db_map.sa_url)
enabled = set(ent_cls["dimension_id_list"]) <= entity_class_ids_in_graph.get(db_map, set())
action_name_icon_enabled.append((action_name, icon, enabled))
for action_name, icon, enabled in sorted(action_name_icon_enabled):
Expand Down Expand Up @@ -702,7 +702,11 @@ def _start_connecting_entities(self, action):
class_name, db_name = action.text().split("@")
db_map_ent_cls_lst = self._db_map_entity_class_lists[class_name]
db_map, ent_cls = next(
iter((db_map, ent_cls) for db_map, ent_cls in db_map_ent_cls_lst if db_map.codename == db_name)
iter(
(db_map, ent_cls)
for db_map, ent_cls in db_map_ent_cls_lst
if self.db_mngr.name_registry.display_name(db_map.sa_url) == db_name
)
)
self._spine_db_editor.start_connecting_entities(db_map, ent_cls, self)

Expand Down
4 changes: 2 additions & 2 deletions spinetoolbox/spine_db_editor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ def main():
editor = MultiSpineDBEditor(db_mngr)
if args.separate_tabs:
for url in args.url:
editor.add_new_tab({url: None})
editor.add_new_tab([url])
else:
editor.add_new_tab({url: None for url in args.url})
editor.add_new_tab(args.url)
editor.show()
return_code = app.exec()
return return_code
Expand Down
13 changes: 2 additions & 11 deletions spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@
class DBItem(EmptyChildMixin, FetchMoreMixin, StandardDBItem):
"""A root item representing a db."""

@property
def item_type(self):
return "db"

@property
def fetch_item_type(self):
return "alternative"
Expand All @@ -38,13 +34,8 @@ def _make_child(self, id_):
class AlternativeItem(GrayIfLastMixin, EditableMixin, LeafItem):
"""An alternative leaf item."""

@property
def item_type(self):
return "alternative"

@property
def icon_code(self):
return _ALTERNATIVE_ICON
item_type = "alternative"
icon_code = _ALTERNATIVE_ICON

def tool_tip(self, column):
if column == 0 and self.id:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class AlternativeModel(TreeModelBase):
"""A model to display alternatives in a tree view."""

def _make_db_item(self, db_map):
return DBItem(self, db_map)
return DBItem(self, db_map, self.db_mngr.name_registry)

def mimeData(self, indexes):
"""Stores selected indexes into MIME data.
Expand Down
Loading

0 comments on commit e85ae72

Please sign in to comment.