From 5de072faaac0be8f551b448d8928cf60adaa3d97 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 14 Jul 2026 14:49:32 +0200 Subject: [PATCH 1/5] Lower GUI video logs to debug Change two MainWindow log calls in `deeplabcut/gui/window.py` from info to debug when setting the video type and clearing video files. This reduces routine GUI noise in normal logs while keeping the messages available for troubleshooting. --- deeplabcut/gui/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeplabcut/gui/window.py b/deeplabcut/gui/window.py index 6bfc13561..9aa474788 100644 --- a/deeplabcut/gui/window.py +++ b/deeplabcut/gui/window.py @@ -309,7 +309,7 @@ def video_type(self): def video_type(self, ext): self.videotype = ext self.video_type_.emit(ext) - self.logger.info(f"Video type set to {self.video_type}") + self.logger.debug(f"Video type set to {self.video_type}") @property def video_files(self): @@ -600,7 +600,7 @@ def clear_video_files(self): """ self.files.clear() # Reset the set to be empty self.video_files_.emit(self.files) # Emit the empty set - self.logger.info("All video files have been cleared.") + self.logger.debug("All video files have been cleared.") def window_set(self): WINDOW_RESIZE_FACTOR = 0.8 From 659a0b6bae00c8cce5f18542b2777982a82375f7 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 14 Jul 2026 14:50:00 +0200 Subject: [PATCH 2/5] Guard Top-Down method check in training tab Hardened the GUI logic that toggles detector snapshot selection by safely handling missing `pose_cfg` or `method` values. The check now defaults to an empty dict/string before lowercasing, preventing runtime errors and preserving the intended Top-Down (`td`) behavior. --- deeplabcut/gui/tabs/train_network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deeplabcut/gui/tabs/train_network.py b/deeplabcut/gui/tabs/train_network.py index 43a94a560..2335702b2 100644 --- a/deeplabcut/gui/tabs/train_network.py +++ b/deeplabcut/gui/tabs/train_network.py @@ -74,7 +74,8 @@ def _update_snapshot_selection_widgets_visibility(self): self.resume_from_snapshot_label.show() self.snapshot_selection_widget.show() # Display detector snapshot selection widget only if in Top-Down mode - if self._shuffle_display.pose_cfg.get("method", "").lower() == "td": + pose_cfg = self._shuffle_display.pose_cfg or {} + if str(pose_cfg.get("method") or "").lower() == "td": self.detector_snapshot_selection_widget.show() else: self.detector_snapshot_selection_widget.hide() From 5c25f1ae89e5eaf82904ef6cc25a62368b1c7966 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 14 Jul 2026 14:58:21 +0200 Subject: [PATCH 3/5] Fix file selection and state sync - Call QFileDialog positionally for PySide6 compatibility - Normalize Path values before passing them to Qt - Handle single-file and multi-file dialog results correctly - Preserve Path objects in GUI state - Prevent video type and shuffle signal feedback loops casing slow and redundant updates - Retain ConfigEditor instances after opening --- deeplabcut/gui/components.py | 397 ++++++++++++++++++++--------------- 1 file changed, 230 insertions(+), 167 deletions(-) diff --git a/deeplabcut/gui/components.py b/deeplabcut/gui/components.py index 4341d429c..282d8edfb 100644 --- a/deeplabcut/gui/components.py +++ b/deeplabcut/gui/components.py @@ -10,7 +10,7 @@ # from __future__ import annotations -import os +from os import PathLike from pathlib import Path from PySide6 import QtWidgets @@ -21,56 +21,98 @@ from deeplabcut.gui.gui_assets import icon_from_resource from deeplabcut.gui.widgets import ConfigEditor +PathInput = str | PathLike[str] +Margins = tuple[int, int, int, int] + def _create_label_widget( text: str, style: str = "", - margins: tuple = (20, 10, 0, 10), + margins: Margins = (20, 10, 0, 10), ) -> QtWidgets.QLabel: label = QtWidgets.QLabel(text) label.setContentsMargins(*margins) label.setStyleSheet(style) - return label def _create_horizontal_layout( - alignment=None, spacing: int = 20, margins: tuple = (20, 0, 0, 0) -) -> QtWidgets.QHBoxLayout(): + alignment: Qt.AlignmentFlag | None = None, + spacing: int = 20, + margins: Margins = (20, 0, 0, 0), +) -> QtWidgets.QHBoxLayout: layout = QtWidgets.QHBoxLayout() - layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + layout.setAlignment(alignment if alignment is not None else Qt.AlignLeft | Qt.AlignTop) layout.setSpacing(spacing) layout.setContentsMargins(*margins) - return layout def _create_vertical_layout( - alignment=None, spacing: int = 20, margins: tuple = (20, 0, 0, 0) -) -> QtWidgets.QVBoxLayout(): + alignment: Qt.AlignmentFlag | None = None, + spacing: int = 20, + margins: Margins = (20, 0, 0, 0), +) -> QtWidgets.QVBoxLayout: layout = QtWidgets.QVBoxLayout() - layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + layout.setAlignment(alignment if alignment is not None else Qt.AlignLeft | Qt.AlignTop) layout.setSpacing(spacing) layout.setContentsMargins(*margins) - return layout def _create_grid_layout( - alignment=None, + alignment: Qt.AlignmentFlag | None = None, spacing: int = 20, - margins: tuple = None, + margins: Margins | None = None, ) -> QtWidgets.QGridLayout: layout = QtWidgets.QGridLayout() - layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + layout.setAlignment(alignment if alignment is not None else Qt.AlignLeft | Qt.AlignTop) layout.setSpacing(spacing) - if margins: + + if margins is not None: layout.setContentsMargins(*margins) return layout -def set_combo_items(combo_box: QtWidgets.QComboBox, items: list[str], index: int = 0): +def _dialog_directory(directory: PathInput | None) -> str: + """Convert an optional path to a QFileDialog-compatible string.""" + if directory is None or directory == "": + return "" + return str(Path(directory)) + + +def _get_open_file_name( + parent: QtWidgets.QWidget, + caption: str, + directory: PathInput | None, + file_filter: str, +) -> tuple[str, str]: + """Open a binding-compatible single-file dialog.""" + return QtWidgets.QFileDialog.getOpenFileName( + parent, + caption, + _dialog_directory(directory), + file_filter, + ) + + +def _get_open_file_names( + parent: QtWidgets.QWidget, + caption: str, + directory: PathInput | None, + file_filter: str, +) -> tuple[list[str], str]: + """Open a binding-compatible multiple-file dialog.""" + return QtWidgets.QFileDialog.getOpenFileNames( + parent, + caption, + _dialog_directory(directory), + file_filter, + ) + + +def set_combo_items(combo_box: QtWidgets.QComboBox, items: list[str], index: int = 0) -> None: """Safely replaces all items in a QComboBox and sets the current index, ensuring that the `currentTextChanged` signal is emitted exactly once (and only if items are present). @@ -95,21 +137,20 @@ def set_combo_items(combo_box: QtWidgets.QComboBox, items: list[str], index: int - This method is designed to be safe for use with PySide, where signals cannot be manually emitted, and future-proof if multiple slots are connected. """ - combo_box.blockSignals(True) - combo_box.clear() - combo_box.addItems(items) - combo_box.blockSignals(False) - - if not items: - combo_box.setCurrentIndex(-1) - return - - current = combo_box.currentIndex() - if current == index: - # Temporarily change index to suppress duplicate signal - combo_box.blockSignals(True) - combo_box.setCurrentIndex(-1) - combo_box.blockSignals(False) + + previous = combo_box.blockSignals(True) + try: + combo_box.clear() + combo_box.addItems(items) + + if not items: + combo_box.setCurrentIndex(-1) + return + + if combo_box.currentIndex() == index: + combo_box.setCurrentIndex(-1) + finally: + combo_box.blockSignals(previous) combo_box.setCurrentIndex(index) @@ -178,7 +219,7 @@ def _init_layout(self, hide_videotype: bool): self.videotype_widget.setMinimumWidth(100) self.videotype_widget.addItems(DLCParams.VIDEOTYPES) self.videotype_widget.setCurrentText(self._normalize_videotype(self.root.video_type)) - self.root.video_type_.connect(self.videotype_widget.setCurrentText) + self.root.video_type_.connect(self._sync_videotype_from_root) self.videotype_widget.currentTextChanged.connect(self.update_videotype) # Select videos @@ -212,12 +253,20 @@ def _normalize_videotype(self, vtype: str) -> str: @property def selected_suffixes(self) -> set[str]: """Return normalized suffixes (without leading dot) of currently selected files.""" - suffixes = set() - for f in self.files: - suffix = f.suffix.lower().lstrip(".") - if suffix: - suffixes.add(suffix) - return suffixes + return {Path(video).suffix.lower().lstrip(".") for video in self.files if Path(video).suffix} + + @Slot(str) + def _sync_videotype_from_root(self, vtype: str) -> None: + normalized = self._normalize_videotype(vtype) + + if normalized == self._normalize_videotype(self.videotype_widget.currentText()): + return + + previous = self.videotype_widget.blockSignals(True) + try: + self.videotype_widget.setCurrentText(normalized) + finally: + self.videotype_widget.blockSignals(previous) def get_effective_videotype( self, @@ -242,14 +291,22 @@ def get_effective_videotype( return f".{videotype}" return videotype - def get_files_grouped_by_suffix(self, keep_dot: bool = False) -> dict[str, list[Path]]: - """Return a dict grouping selected files by their suffixes.""" + def get_files_grouped_by_suffix( + self, + keep_dot: bool = False, + ) -> dict[str, list[Path]]: + """Return selected files grouped by suffix.""" groups: dict[str, list[Path]] = {} - for f in self.files: - suffix = f.suffix.lower() + + for video in self.files: + path = Path(video) + suffix = path.suffix.lower() + if not keep_dot: suffix = suffix.lstrip(".") - groups.setdefault(suffix, []).append(f) + + groups.setdefault(suffix, []).append(path) + return groups def _all_supported_video_patterns(self) -> list[str]: @@ -285,14 +342,7 @@ def _build_video_filter(self) -> str: return f"Videos ({' '.join(video_types)})" - def _set_videotype_silently(self, vtype: str): - """ - Update the dropdown/root videotype without triggering update_videotype(), - because that method clears the current selection. - - Only updates state if the videotype is supported by the combo box. - Otherwise, leaves the current state unchanged. - """ + def _set_videotype_silently(self, vtype: str) -> None: normalized = self._normalize_videotype(vtype) current = self._normalize_videotype(self.videotype_widget.currentText()) @@ -300,81 +350,90 @@ def _set_videotype_silently(self, vtype: str): self.root.logger.warning("Attempted to set an empty videotype silently; keeping current selection.") return - # Validate against actual combo-box items if self.videotype_widget.findText(normalized) == -1: self.root.logger.warning( - f"Attempted to set unsupported videotype '{normalized}' silently; " - f"keeping current videotype '{current}'." + f"Attempted to set unsupported videotype " + f"{normalized!r} silently; keeping current videotype " + f"{current!r}." ) return - if normalized != current: - self.videotype_widget.blockSignals(True) + previous = self.videotype_widget.blockSignals(True) + try: self.videotype_widget.setCurrentText(normalized) - self.videotype_widget.blockSignals(False) + finally: + self.videotype_widget.blockSignals(previous) - self.root.video_type = normalized + if self._normalize_videotype(self.root.video_type) != normalized: + self.root.video_type = normalized - def update_videotype(self, vtype: str): + @Slot(str) + def update_videotype(self, vtype: str) -> None: normalized = self._normalize_videotype(vtype) + current = self._normalize_videotype(self.root.video_type) + + if normalized == current: + return + self.clear_selected_videos() self.root.video_type = normalized - def _update_video_selection(self, videopaths): + def _update_video_selection(self, _videopaths) -> None: n_videos = len(self.root.video_files) - if n_videos: - suffixes = self.selected_suffixes - if len(suffixes) == 1: - suffix = next(iter(suffixes)) - self.selected_videos_text.setText(f"{n_videos} videos selected (.{suffix})") - elif len(suffixes) > 1: - counts = { - suffix: len(files) for suffix, files in self.get_files_grouped_by_suffix(keep_dot=False).items() - } - summary = ", ".join(f"{count} .{suffix}" for suffix, count in sorted(counts.items())) - self.selected_videos_text.setText( - f"{n_videos} videos selected ({summary}; will run in separate batches)" - ) - else: - self.selected_videos_text.setText(f"{n_videos} videos selected") - self.select_video_button.setText("Add more videos") - else: + if not n_videos: self.selected_videos_text.setText("") self.select_video_button.setText("Select videos") + return + + suffixes = self.selected_suffixes + + if len(suffixes) == 1: + suffix = next(iter(suffixes)) + text = f"{n_videos} videos selected (.{suffix})" + elif len(suffixes) > 1: + counts = {suffix: len(files) for suffix, files in self.get_files_grouped_by_suffix().items()} + summary = ", ".join(f"{count} .{suffix}" for suffix, count in sorted(counts.items())) + text = f"{n_videos} videos selected ({summary}; will run in separate batches)" + else: + text = f"{n_videos} videos selected" + + self.selected_videos_text.setText(text) + self.select_video_button.setText("Add more videos") def update_videos(self): - directory_to_open = os.fspath(self.root.project_folder) video_filter = self._build_video_filter() - filenames = QtWidgets.QFileDialog.getOpenFileNames( - parent=self, - caption="Select video(s) to analyze", - dir=directory_to_open, - filter=video_filter, + filenames, _ = _get_open_file_names( + self, + "Select video(s) to analyze", + self.root.project_folder, + video_filter, ) - if filenames[0]: - abs_files = [Path(vid).absolute() for vid in filenames[0]] - self.root.add_video_files(abs_files) + if not filenames: + return + + abs_files = [Path(filename).absolute() for filename in filenames] + self.root.add_video_files(abs_files) + + if not self.sync_videotype_with_selection: + return - # Optional safety: sync dropdown to selected file suffix - if self.sync_videotype_with_selection: - suffixes = {v.suffix.lower().lstrip(".") for v in abs_files if v.suffix} + suffixes = {video.suffix.lower().lstrip(".") for video in abs_files if video.suffix} - if len(suffixes) == 1: - inferred = next(iter(suffixes)) - self._set_videotype_silently(inferred) - self.root.logger.info(f"Inferred videotype '{inferred}' from selected file(s)") - elif len(suffixes) > 1: - self.root.logger.warning( - f"Selected videos have mixed suffixes {sorted(suffixes)}; " - "keeping current videotype dropdown unchanged." - ) + if len(suffixes) == 1: + inferred = next(iter(suffixes)) + self._set_videotype_silently(inferred) + self.root.logger.info(f"Inferred videotype {inferred!r} from selected file(s)") + elif len(suffixes) > 1: + self.root.logger.warning( + f"Selected videos have mixed suffixes {sorted(suffixes)}; keeping current videotype dropdown unchanged." + ) def clear_selected_videos(self): self.root.clear_video_files() - self.root.logger.info("Cleared selected videos") + self.root.logger.debug("Cleared selected videos") class SnapshotSelectionWidget(QtWidgets.QWidget): @@ -422,19 +481,16 @@ def _update_selected_snapshot_display(self): self.clear_snapshot_button.show() def select_snapshot(self): - # Create a filter string with both lowercase and uppercase extensions snapshot_types = ["*.pt", "*.PT"] snapshot_filter = f"Snapshots ({' '.join(snapshot_types)})" - directory_to_open = os.fspath(self.root.models_folder) - - selected_snapshot, _ = QtWidgets.QFileDialog.getOpenFileName( - parent=self, - caption="Select snapshot to start training from", - dir=directory_to_open, - filter=snapshot_filter, + selected_snapshot, _ = _get_open_file_name( + self, + "Select snapshot to start training from", + self.root.models_folder, + snapshot_filter, ) - # When Canceling a file selection, Qt returns an empty string as selected file + if selected_snapshot: self.selected_snapshot = Path(selected_snapshot).absolute() @@ -475,20 +531,24 @@ def _init_layout(self): def _update_selected_conditions_display(self): def _shorten_path(path: Path | str, max_length: int = 30) -> str: - path_str = os.fspath(path) + path_str = str(path) if len(path_str) <= max_length: return path_str return "..." + path_str[-(max_length - 3) :] self.selected_conditions_text.setText( - "" if self.selected_conditions is None else f"{_shorten_path(self.selected_conditions)}" + "" if self.selected_conditions is None else _shorten_path(self.selected_conditions) ) def select_conditions(self): - def _is_model_bu(selected_conditions) -> bool: + def _is_model_bu( + selected_conditions: PathInput, + ) -> bool: model_config_path = Path(selected_conditions).parent / "pytorch_config.yaml" model_config = read_config_as_dict(model_config_path) - return model_config.get("method").lower() == "bu" + method = model_config.get("method") + + return isinstance(method, str) and method.lower() == "bu" # Create a filter string with both lowercase and uppercase extensions snapshots_label = "Snapshots" @@ -505,27 +565,30 @@ def _is_model_bu(selected_conditions) -> bool: ] ) - directory_to_open = os.fspath(self.root.project_folder) - - selected_conditions, selected_filter = QtWidgets.QFileDialog.getOpenFileName( - parent=self, - caption="Select conditions to use during inference (snapshot or predictions file)", - dir=directory_to_open, - filter=conditions_filter, + selected_conditions, selected_filter = _get_open_file_name( + self, + ("Select conditions to use during inference (snapshot or predictions file)"), + self.root.project_folder, + conditions_filter, ) - if selected_filter.startswith(snapshots_label) and selected_conditions: - if not _is_model_bu(selected_conditions): - msg = _create_message_box( - "Invalid conditions", - ( - f"The selected snapshot ({selected_conditions}) cannot be " - "used as conditions because it is not a Bottom-Up model." - ), - ) - msg.exec_() - selected_conditions = None - - # When Canceling a file selection, Qt returns an empty string as selected file + + if ( + selected_conditions + and selected_filter.startswith(snapshots_label) + and not _is_model_bu(selected_conditions) + ): + msg = _create_message_box( + "Invalid conditions", + ( + f"The selected snapshot " + f"({selected_conditions}) cannot be used " + "as conditions because it is not a " + "Bottom-Up model." + ), + ) + msg.exec() + selected_conditions = None + self.selected_conditions = Path(selected_conditions).absolute() if selected_conditions else None self._update_selected_conditions_display() @@ -556,9 +619,15 @@ def __init__(self, root, parent): self.root.shuffle_change.connect(self.update_shuffle) @Slot(int) - def update_shuffle(self, new_shuffle: int): - if new_shuffle != self.value(): + def update_shuffle(self, new_shuffle: int) -> None: + if new_shuffle == self.value(): + return + + previous = self.blockSignals(True) + try: self.setValue(new_shuffle) + finally: + self.blockSignals(previous) class DefaultTab(QtWidgets.QWidget): @@ -600,15 +669,16 @@ def _init_default_layout(self): class EditYamlButton(QtWidgets.QPushButton): def __init__(self, button_label: str, filepath: str, parent: QtWidgets.QWidget = None): - super().__init__(parent) + super().__init__(button_label, parent) self.filepath = filepath self.parent = parent + self._editor: ConfigEditor | None = None self.clicked.connect(self.open_config) def open_config(self): - editor = ConfigEditor(self.filepath) - editor.show() + self._editor = ConfigEditor(self.filepath) + self._editor.show() class BrowseFilesButton(QtWidgets.QPushButton): @@ -616,13 +686,13 @@ def __init__( self, button_label: str, filetype: str = None, - cwd: str = None, + cwd: PathInput | None = None, single_file: bool = False, dialog_text: str = None, file_text: str = None, parent=None, ): - super().__init__(parent) + super().__init__(button_label, parent) self.filetype = filetype self.single_file_only = single_file self.cwd = cwd @@ -635,38 +705,32 @@ def __init__( self.clicked.connect(self.browse_files) - def browse_files(self): - # Look for any extension by default + def browse_files(self) -> None: file_ext = "*" if self.filetype: - # This works both with e.g. .avi and avi - file_ext = self.filetype.split(".")[-1] - - # Choose multiple files by default - open_file_func = QtWidgets.QFileDialog.getOpenFileNames - if self.single_file_only: - open_file_func = QtWidgets.QFileDialog.getOpenFileName - - cwd = "" - if self.cwd: - cwd = self.cwd + file_ext = self.filetype.rsplit(".", 1)[-1] - dialog_text = f"Select .{file_ext} files" - if self.dialog_text: - dialog_text = self.dialog_text + dialog_text = self.dialog_text or f"Select .{file_ext} files" + file_text = self.file_text or f"Files (*.{file_ext})" - file_text = f"Files (*.{file_ext})" - if self.file_text: - file_text = self.file_text - - filepaths = open_file_func(self, dialog_text, cwd, file_text) + if self.single_file_only: + filepath, _ = _get_open_file_name( + self, + dialog_text, + self.cwd, + file_text, + ) + if filepath: + self.files.add(Path(filepath).absolute()) + return - if filepaths: - if self.single_file_only: - if filepaths[0]: - self.files.add(Path(filepaths[0]).absolute()) - else: - self.files.update(Path(path).absolute() for path in filepaths[0]) + filepaths, _ = _get_open_file_names( + self, + dialog_text, + self.cwd, + file_text, + ) + self.files.update(Path(filepath).absolute() for filepath in filepaths) def _create_message_box(text, info_text): @@ -677,7 +741,6 @@ def _create_message_box(text, info_text): msg.setWindowTitle("Info") msg.setMinimumWidth(900) - # logo = Path("logo.png").resolve().parent / "assets" / "logo.png" icon = icon_from_resource("logo.png") msg.setWindowIcon(icon) msg.setStandardButtons(QtWidgets.QMessageBox.Ok) From 4c846926acd46e2449712658f997312a2c54b37b Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:44:45 +0200 Subject: [PATCH 4/5] use os.fspath instead of str --- deeplabcut/gui/components.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deeplabcut/gui/components.py b/deeplabcut/gui/components.py index 282d8edfb..b8ad73616 100644 --- a/deeplabcut/gui/components.py +++ b/deeplabcut/gui/components.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from os import PathLike from pathlib import Path @@ -79,7 +80,7 @@ def _dialog_directory(directory: PathInput | None) -> str: """Convert an optional path to a QFileDialog-compatible string.""" if directory is None or directory == "": return "" - return str(Path(directory)) + return os.fspath(directory) def _get_open_file_name( @@ -531,7 +532,7 @@ def _init_layout(self): def _update_selected_conditions_display(self): def _shorten_path(path: Path | str, max_length: int = 30) -> str: - path_str = str(path) + path_str = os.fspath(path) if len(path_str) <= max_length: return path_str return "..." + path_str[-(max_length - 3) :] From 193258e3041d50169aafda8b419a7836abbeb287 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:26:41 +0200 Subject: [PATCH 5/5] revert use os.fspath (d795bab96d434f3f542d8316a3b41511b292be1b) str() conversions are clearer --- deeplabcut/gui/components.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/deeplabcut/gui/components.py b/deeplabcut/gui/components.py index b8ad73616..8c240a1a4 100644 --- a/deeplabcut/gui/components.py +++ b/deeplabcut/gui/components.py @@ -10,8 +10,6 @@ # from __future__ import annotations -import os -from os import PathLike from pathlib import Path from PySide6 import QtWidgets @@ -22,7 +20,7 @@ from deeplabcut.gui.gui_assets import icon_from_resource from deeplabcut.gui.widgets import ConfigEditor -PathInput = str | PathLike[str] +PathInput = str | Path Margins = tuple[int, int, int, int] @@ -80,7 +78,7 @@ def _dialog_directory(directory: PathInput | None) -> str: """Convert an optional path to a QFileDialog-compatible string.""" if directory is None or directory == "": return "" - return os.fspath(directory) + return str(directory) def _get_open_file_name( @@ -532,7 +530,7 @@ def _init_layout(self): def _update_selected_conditions_display(self): def _shorten_path(path: Path | str, max_length: int = 30) -> str: - path_str = os.fspath(path) + path_str = str(path) if len(path_str) <= max_length: return path_str return "..." + path_str[-(max_length - 3) :]