Skip to content

Commit 3f9da4b

Browse files
committed
Normalize path case in URLs in DB manager
Equal paths on Windows may take many forms thanks to case insensitivity. Normalizing the case in DB manager ensures we don't accidentally create duplicate DB mappings or try to access a DB mapping under slightly different but equal URL.
1 parent 403c082 commit 3f9da4b

File tree

6 files changed

+24
-10
lines changed

6 files changed

+24
-10
lines changed

spinetoolbox/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1779,7 +1779,7 @@ def display_byte_size(size_bytes: int) -> tuple[float | int, str]:
17791779

17801780

17811781
def normcase_database_url_path(url: str) -> str:
1782-
if not url.startswith("sqlite://"):
1782+
if not url.startswith("sqlite:///"):
17831783
return url
17841784
path = url[len("sqlite:///") :]
17851785
return "sqlite:///" + os.path.normcase(path)

spinetoolbox/spine_db_editor/widgets/multi_spine_db_editor.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
from PySide6.QtCore import QPoint, Slot
1717
from PySide6.QtGui import QFont, QIcon
1818
from PySide6.QtWidgets import QMenu, QStatusBar, QToolButton
19+
from sqlalchemy.engine.url import URL
1920
from ...config import MAINWINDOW_SS, ONLINE_DOCUMENTATION_URL
2021
from ...font import TOOLBOX_FONT
21-
from ...helpers import CharIconEngine, open_url
22+
from ...helpers import CharIconEngine, normcase_database_url_path, open_url
2223
from ...widgets.multi_tab_window import MultiTabWindow
2324
from ...widgets.settings_widget import SpineDBEditorSettingsWidget
2425
from ..editors import db_editor_registry
@@ -241,7 +242,12 @@ def open_db_editor(db_urls, db_mngr, reuse_existing_editor):
241242
if multi_db_editor.tab_load_success:
242243
multi_db_editor.show()
243244
return
244-
existing = _get_existing_spine_db_editor(list(map(str, db_urls)))
245+
normcased_urls = []
246+
for url in db_urls:
247+
if isinstance(url, URL):
248+
url = url.render_as_string(hide_password=False)
249+
normcased_urls.append(normcase_database_url_path(url))
250+
existing = _get_existing_spine_db_editor(normcased_urls)
245251
if existing is None:
246252
multi_db_editor.add_new_tab(db_urls)
247253
else:

spinetoolbox/spine_db_editor/widgets/spine_db_editor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def open_db_file(self, _=False):
255255
self.qsettings.endGroup()
256256
if not file_path:
257257
return
258-
url = "sqlite:///" + os.path.normcase(file_path)
258+
url = "sqlite:///" + file_path
259259
self.load_db_urls([url])
260260

261261
@Slot(bool)
@@ -267,7 +267,7 @@ def add_db_file(self, _=False):
267267
self.qsettings.endGroup()
268268
if not file_path:
269269
return
270-
url = "sqlite:///" + os.path.normcase(file_path)
270+
url = "sqlite:///" + file_path
271271
self.load_db_urls(self.db_urls + [url])
272272

273273
@Slot()
@@ -283,7 +283,7 @@ def create_db_file(self) -> None:
283283
os.remove(file_path)
284284
except OSError:
285285
pass
286-
url = "sqlite:///" + os.path.normcase(file_path)
286+
url = "sqlite:///" + file_path
287287
self.load_db_urls([url], create=True)
288288

289289
@Slot()

spinetoolbox/spine_db_manager.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
from spinedb_api.temp_id import TempId
6363
from .database_display_names import NameRegistry
6464
from .fetch_parent import FetchParent
65-
from .helpers import DBMapDictItems, DBMapPublicItems, busy_effect, plain_to_tool_tip
65+
from .helpers import DBMapDictItems, DBMapPublicItems, busy_effect, normcase_database_url_path, plain_to_tool_tip
6666
from .mvcmodels.shared import INVALID_TYPE, PARAMETER_TYPE_VALIDATION_ROLE, PARSED_ROLE, TYPE_NOT_VALIDATED, VALID_TYPE
6767
from .parameter_type_validation import ParameterTypeValidator
6868
from .spine_db_commands import (
@@ -297,6 +297,7 @@ def db_map(self, url: str) -> DatabaseMapping:
297297
"""
298298
if isinstance(url, URL):
299299
url = url.render_as_string(hide_password=False)
300+
url = normcase_database_url_path(url)
300301
return self._db_maps.get(url)
301302

302303
def create_new_spine_database(self, url: str, logger: LoggerInterface, overwrite: bool = False):
@@ -324,6 +325,7 @@ def create_new_spine_database(self, url: str, logger: LoggerInterface, overwrite
324325

325326
def close_session(self, url: str) -> None:
326327
"""Pops any db map on the given url and closes its connection."""
328+
url = normcase_database_url_path(url)
327329
self._no_prompt_urls.discard(url)
328330
try:
329331
db_map = self._db_maps.pop(url)
@@ -364,6 +366,7 @@ def get_db_map(
364366
"""
365367
if isinstance(url, URL):
366368
url = url.render_as_string(hide_password=False)
369+
url = normcase_database_url_path(url)
367370
db_map = self._db_maps.get(url)
368371
if db_map is not None:
369372
return db_map
@@ -1433,7 +1436,10 @@ def export_data(
14331436
raise ValueError()
14341437

14351438
def _is_url_available(self, url: Union[URL, str], logger: LoggerInterface) -> bool:
1436-
if str(url) in self.db_urls:
1439+
if isinstance(url, URL):
1440+
url = url.render_as_string(hide_password=False)
1441+
url = normcase_database_url_path(url)
1442+
if url in self.db_urls:
14371443
message = f"The URL <b>{url}</b> is in use. Please close all applications using it and try again."
14381444
logger.msg_error.emit(message)
14391445
return False

tests/spine_db_editor/widgets/test_multi_spine_db_editor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from unittest.mock import MagicMock, patch
1818
from PySide6.QtCore import QPoint, QSettings
1919
from PySide6.QtWidgets import QApplication
20+
from spinetoolbox.helpers import normcase_database_url_path
2021
from spinetoolbox.multi_tab_windows import MultiTabWindowRegistry
2122
from spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor import MultiSpineDBEditor, open_db_editor
2223
from spinetoolbox.spine_db_manager import SpineDBManager
@@ -93,7 +94,7 @@ def test_open_db_in_tab_when_editor_has_an_empty_tab(self):
9394
"spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.db_editor_registry",
9495
self._db_editor_registry,
9596
),
96-
patch("spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.MultiSpineDBEditor.show") as mock_show,
97+
patch("spinetoolbox.spine_db_editor.widgets.multi_spine_db_editor.MultiSpineDBEditor.show"),
9798
):
9899
self.assertFalse(self._db_editor_registry.has_windows())
99100
window = MultiSpineDBEditor(self._db_mngr, [])
@@ -103,5 +104,5 @@ def test_open_db_in_tab_when_editor_has_an_empty_tab(self):
103104
open_db_editor([self._db_url], self._db_mngr, reuse_existing_editor=True)
104105
self.assertEqual(window.tab_widget.count(), 2)
105106
tab = window.tab_widget.widget(1)
106-
self.assertEqual(tab.db_urls, [self._db_url])
107+
self.assertEqual(tab.db_urls, [normcase_database_url_path(self._db_url)])
107108
self._close_windows()

tests/test_helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,7 @@ def test_correctness(self):
551551
class TestNormcaseDatabaseUrlPath:
552552
def test_correctness(self):
553553
assert normcase_database_url_path("mysql://example.com/Path/MY_DB") == "mysql://example.com/Path/MY_DB"
554+
assert normcase_database_url_path("sqlite://") == "sqlite://"
554555
if sys.platform == "win32":
555556
assert (
556557
normcase_database_url_path("sqlite:///C:\\Users\\SansSerif\\in.sqlite")

0 commit comments

Comments
 (0)