From 0741e96e456238f4b1520d4ce11debfae526f3f5 Mon Sep 17 00:00:00 2001 From: Jack Nguyen Date: Mon, 13 Jul 2026 15:45:22 -0700 Subject: [PATCH] fix(paths): centralize file:// URL handling to fix broken save-dir paths --- src/pygui/ISC/FirstLaunchWizard.qml | 2 +- src/pygui/ISC/HomePage.qml | 6 +----- src/pygui/ISC/Tabs/StatusTab.qml | 7 ++----- src/pygui/ISC/Tabs/components/SourcePanel.qml | 4 ++-- .../controllers/batch_export_controller.py | 6 ++++-- .../backend/controllers/device_controller.py | 9 ++++---- src/pygui/backend/data/file_browser.py | 6 ++++-- src/pygui/backend/license/license_model.py | 16 ++------------ src/pygui/backend/utils.py | 21 +++++++++++++++++++ 9 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/pygui/ISC/FirstLaunchWizard.qml b/src/pygui/ISC/FirstLaunchWizard.qml index 2b43083..29d9cdb 100644 --- a/src/pygui/ISC/FirstLaunchWizard.qml +++ b/src/pygui/ISC/FirstLaunchWizard.qml @@ -43,7 +43,7 @@ Popup { id: saveDirDialog title: "Choose a folder to save data" onAccepted: { - var dir = decodeURIComponent(String(selectedFolder).replace(/^file:\/\//, "")) + var dir = String(selectedFolder) deviceController.setSaveDataDir(dir) file_browser.setCurrentDirectory(dir) root.finished() diff --git a/src/pygui/ISC/HomePage.qml b/src/pygui/ISC/HomePage.qml index 21e5432..ea19dbd 100644 --- a/src/pygui/ISC/HomePage.qml +++ b/src/pygui/ISC/HomePage.qml @@ -171,10 +171,6 @@ Rectangle { } } - function cleanFolderUrl(url) { - return decodeURIComponent(String(url).replace(/^file:\/\//, "")) - } - function _doDetect() { root.memoryRead = false root.waferDetected = false @@ -187,7 +183,7 @@ Rectangle { id: saveDirDialog title: "Choose a folder to save Data" onAccepted: { - var dir = root.cleanFolderUrl(selectedFolder) + var dir = String(selectedFolder) deviceController.setSaveDataDir(dir) file_browser.setCurrentDirectory(dir) root._doDetect() diff --git a/src/pygui/ISC/Tabs/StatusTab.qml b/src/pygui/ISC/Tabs/StatusTab.qml index ac4882c..da2b10f 100644 --- a/src/pygui/ISC/Tabs/StatusTab.qml +++ b/src/pygui/ISC/Tabs/StatusTab.qml @@ -113,9 +113,6 @@ ColumnLayout { readonly property bool isBusy: deviceController.connectionStatus.endsWith("...") readonly property string portName: deviceController.selectedPort || "β€”" - function cleanFolderUrl(url) { - return decodeURIComponent(String(url).replace(/^file:\/\//, "")); - } function currentFamilyCode() { var info = deviceController.lastWaferInfo; return info && info.length > 0 ? (info[0] || "") : ""; @@ -128,7 +125,7 @@ ColumnLayout { id: saveDirDialog title: "Choose a folder to save." onAccepted: { - var dir = root.cleanFolderUrl(selectedFolder); + var dir = String(selectedFolder); deviceController.setSaveDataDir(dir); file_browser.setCurrentDirectory(dir); root.parseAndSavePendingRead(); @@ -141,7 +138,7 @@ ColumnLayout { id: dirOnlyDialog title: "Choose a folder to save Data" onAccepted: { - var dir = root.cleanFolderUrl(selectedFolder); + var dir = String(selectedFolder); deviceController.setSaveDataDir(dir); file_browser.setCurrentDirectory(dir); } diff --git a/src/pygui/ISC/Tabs/components/SourcePanel.qml b/src/pygui/ISC/Tabs/components/SourcePanel.qml index a76116b..136ab3d 100644 --- a/src/pygui/ISC/Tabs/components/SourcePanel.qml +++ b/src/pygui/ISC/Tabs/components/SourcePanel.qml @@ -260,14 +260,14 @@ ColumnLayout { id: importFileDialog title: "Select Data File To Import" nameFilters: ["CSV files (*.csv)"] - onAccepted: file_browser.importZWaferCsv(String(selectedFile).replace(/^file:\/\//, "")) + onAccepted: file_browser.importZWaferCsv(String(selectedFile)) } FolderDialog { id: batchExportDirDialog title: "Export Wafer Maps To…" onAccepted: { - var dir = String(selectedFolder).replace(/^file:\/\//, ""); + var dir = String(selectedFolder); var paths = file_browser.files.map(function(row) { return row.fileName; }); batchExportController.start(paths, dir); } diff --git a/src/pygui/backend/controllers/batch_export_controller.py b/src/pygui/backend/controllers/batch_export_controller.py index 1e295e3..6e88dc8 100644 --- a/src/pygui/backend/controllers/batch_export_controller.py +++ b/src/pygui/backend/controllers/batch_export_controller.py @@ -5,7 +5,7 @@ import threading from PySide6.QtCore import Property, QObject, Qt, Signal, Slot -from pygui.backend.utils import slot_error_boundary +from pygui.backend.utils import slot_error_boundary, to_local_path from pygui.backend.wafer.batch_export import BatchExportResult, export_batch log = logging.getLogger(__name__) @@ -33,7 +33,9 @@ class BatchExportController(QObject): self._busy = True self.busyChanged.emit() threading.Thread( - target=self._worker, args=(list(file_paths), output_dir), daemon=True + target=self._worker, + args=(list(file_paths), str(to_local_path(output_dir))), + daemon=True, ).start() def _worker(self, file_paths: list[str], output_dir: str) -> None: diff --git a/src/pygui/backend/controllers/device_controller.py b/src/pygui/backend/controllers/device_controller.py index 22a98d0..a92cc81 100644 --- a/src/pygui/backend/controllers/device_controller.py +++ b/src/pygui/backend/controllers/device_controller.py @@ -15,7 +15,7 @@ from PySide6.QtCore import Property, QObject, Qt, Signal, Slot from pygui.backend.data.local_settings import LocalSettings from pygui.backend.models.data_model import TemperatureTableModel -from pygui.backend.utils import slot_error_boundary +from pygui.backend.utils import slot_error_boundary, to_local_path from pygui.backend.visualization.graph_view import GraphView from pygui.serialcomm.data_parser import ( convert_to_debug_temperatures, @@ -82,7 +82,8 @@ class DeviceController(QObject): self._operation_in_progress = False self._activity_log: list[str] = [] self._raw_bytes: Optional[bytes] = None - self._save_data_dir: str = getattr(settings, 'save_data_dir', "") or str(Path(self._data_dir) / "csv") + saved_dir = getattr(settings, 'save_data_dir', "") + self._save_data_dir: str = str(to_local_path(saved_dir)) if saved_dir else str(Path(self._data_dir) / "csv") self._last_wafer_info: dict[str, Any] = {} self._wafer_detected: bool = False self._last_csv_path: str = "" @@ -141,8 +142,8 @@ class DeviceController(QObject): @slot_error_boundary def setSaveDataDir(self, path: str) -> None: """Set the directory for saving parsed CSV data files.""" - self._save_data_dir = path - self._append_log(f"Save data dir set to: {path}") + self._save_data_dir = str(to_local_path(path)) + self._append_log(f"Save data dir set to: {self._save_data_dir}") self._save_status() @Slot(result=str) diff --git a/src/pygui/backend/data/file_browser.py b/src/pygui/backend/data/file_browser.py index 372d49d..2f0cab6 100644 --- a/src/pygui/backend/data/file_browser.py +++ b/src/pygui/backend/data/file_browser.py @@ -10,6 +10,7 @@ from PySide6.QtWidgets import QFileDialog, QMessageBox from pygui.backend.data.constants import default_data_dir from pygui.backend.data.csv_file_metadata import CSVFileMetadata +from pygui.backend.utils import to_local_path from pygui.backend.wafer.zwafer_parser import ZWaferParser @@ -45,7 +46,7 @@ class FileBrowser(QObject): Used to keep this directory in sync with DeviceController.saveDataDir when the user picks a save folder elsewhere in the app. """ - self._set_current_directory(Path(path)) + self._set_current_directory(to_local_path(path)) self._refresh_files(show_empty_message=False) @Slot() @@ -124,7 +125,8 @@ class FileBrowser(QObject): date still gets a deterministic (if degenerate) filename, so the duplicate-import guard below still fires on a second attempt. """ - source_path = Path(file_path) + source_path = to_local_path(file_path) + file_path = str(source_path) parser_data, _ = self._parser.parse(file_path) if parser_data is None: self.importFinished.emit("", f"Failed to parse CSV:\n{source_path.name}") diff --git a/src/pygui/backend/license/license_model.py b/src/pygui/backend/license/license_model.py index e75d8cc..7d7946f 100644 --- a/src/pygui/backend/license/license_model.py +++ b/src/pygui/backend/license/license_model.py @@ -5,7 +5,7 @@ import shutil import time from pathlib import Path -from PySide6.QtCore import Property, QObject, QUrl, Signal, Slot +from PySide6.QtCore import Property, QObject, Signal, Slot from pygui.backend.license.license_manager import ( LICENSE_FILENAME_RE, @@ -13,25 +13,13 @@ from pygui.backend.license.license_manager import ( parse_license_blob, read_license_files, ) +from pygui.backend.utils import to_local_path as _to_local_path log = logging.getLogger(__name__) TRIAL_DAYS = 30 -def _to_local_path(path_or_url: str) -> Path: - """Accept both plain paths and file:// URLs (QML FileDialog gives URLs). - - Uses QUrl.toLocalFile() rather than urlparse β€” a manual - urlparse().path strip leaves a leading slash before a Windows drive - letter (e.g. "/C:/Users/...", not a valid absolute path), which - QUrl's platform-aware conversion handles correctly. - """ - if path_or_url.startswith("file:"): - return Path(QUrl(path_or_url).toLocalFile()) - return Path(path_or_url) - - # ===== License Model ===== class LicenseModel(QObject): """Context property backing the About dialog license grid. diff --git a/src/pygui/backend/utils.py b/src/pygui/backend/utils.py index 66adc5d..3ca7d02 100644 --- a/src/pygui/backend/utils.py +++ b/src/pygui/backend/utils.py @@ -1,5 +1,26 @@ import logging +import re from functools import wraps +from pathlib import Path + +from PySide6.QtCore import QUrl + +_MANGLED_DRIVE_RE = re.compile(r"^/([A-Za-z]:)") + + +def to_local_path(path_or_url: str) -> Path: + """Accept plain paths, file:// URLs, and already-mangled "/C:/..." strings + (persisted by old buggy code before it stripped file:// by hand). + + Uses QUrl.toLocalFile() rather than a manual string strip β€” stripping + "file://" by hand leaves a leading slash before a Windows drive letter + (e.g. "/C:/Users/...", not a valid absolute path), which QUrl's + platform-aware conversion handles correctly. + """ + if path_or_url.startswith("file:"): + return Path(QUrl(path_or_url).toLocalFile()) + # ponytail: repair values saved by the old regex-stripping QML code + return Path(_MANGLED_DRIVE_RE.sub(r"\1", path_or_url)) def slot_error_boundary(func):