From d6e0568709690a7b679a3c669e8b981a882190f9 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Sun, 5 Sep 2021 21:46:38 +0200 Subject: [PATCH 01/39] First path restriction test --- ipyfilechooser/utils.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 523e982..3e7077b 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -6,28 +6,40 @@ import sys from typing import List, Sequence, Iterable, Optional -def get_subpaths(path: str) -> List[str]: +def get_subpaths(path: str, root_path: str) -> List[str]: """Walk a path and return a list of subpaths.""" if os.path.isfile(path): path = os.path.dirname(path) + path = strip_root_path(path, root_path) paths = [path] + path, tail = os.path.split(path) while tail: paths.append(path) path, tail = os.path.split(path) - try: - # Add Windows drive letters, but remove the current drive - drives = get_drive_letters() - drives.remove(paths[-1]) - paths.extend(drives) - except ValueError: - pass + if not root_path: + try: + # Add Windows drive letters, but remove the current drive + drives = get_drive_letters() + drives.remove(paths[-1]) + paths.extend(drives) + except ValueError: + pass return paths +def strip_root_path(path: str, root_path: str) -> str: + """Remove a root path from a path""" + if path == root_path: + return os.path.sep + elif path.startswith(root_path): + return path[len(root_path):] + return '' + + def has_parent(path: str) -> bool: """Check if a path has a parent folder.""" return os.path.basename(path) != '' From cff684b5b485de41db125ec6577aaf3422b0c11d Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Sun, 5 Sep 2021 22:08:37 +0200 Subject: [PATCH 02/39] Minor fix --- ipyfilechooser/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 3e7077b..173778d 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -20,7 +20,7 @@ def get_subpaths(path: str, root_path: str) -> List[str]: paths.append(path) path, tail = os.path.split(path) - if not root_path: + if root_path != '': try: # Add Windows drive letters, but remove the current drive drives = get_drive_letters() From d2fbe5afe86e49f3f35b18623e8ca8810cef7998 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Sun, 5 Sep 2021 22:49:48 +0200 Subject: [PATCH 03/39] Better drive letter handling --- ipyfilechooser/utils.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 173778d..2318c16 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -20,11 +20,10 @@ def get_subpaths(path: str, root_path: str) -> List[str]: paths.append(path) path, tail = os.path.split(path) - if root_path != '': + if not root_path: try: # Add Windows drive letters, but remove the current drive - drives = get_drive_letters() - drives.remove(paths[-1]) + drives = get_drive_letters(paths[-1]) paths.extend(drives) except ValueError: pass @@ -97,14 +96,20 @@ def prepend_dir_icons(dir_list: Iterable[str]) -> List[str]: return ['\U0001F4C1 ' + dirname for dirname in dir_list] -def get_drive_letters() -> List[str]: +def get_drive_letters(path: str) -> List[str]: """Get drive letters.""" if sys.platform == 'win32': + # Check if driveletter is upper or lowercase + chars = string.ascii_lowercase + + if path[0].isupper(): + chars = string.ascii_uppercase + # Windows has drive letters return [ - '%s:\\' % d for d in string.ascii_uppercase - if os.path.exists('%s:' % d) - ] + f'{d}:\\' for d in chars + if os.path.exists(f'{d}:') + ].remove(path) else: # Unix does not have drive letters return [] From 3f5c14c92ce0b367679a4192389d4fb5ceb878e1 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Sun, 5 Sep 2021 23:05:37 +0200 Subject: [PATCH 04/39] Fix bug --- ipyfilechooser/utils.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 2318c16..99c58d9 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -99,17 +99,16 @@ def prepend_dir_icons(dir_list: Iterable[str]) -> List[str]: def get_drive_letters(path: str) -> List[str]: """Get drive letters.""" if sys.platform == 'win32': - # Check if driveletter is upper or lowercase + # Check if path uses upper or lowercase drive letters chars = string.ascii_lowercase if path[0].isupper(): chars = string.ascii_uppercase # Windows has drive letters - return [ - f'{d}:\\' for d in chars - if os.path.exists(f'{d}:') - ].remove(path) + drives = [f'{d}:\\' for d in chars if os.path.exists(f'{d}:')] + drives.remove(path) + return drives else: # Unix does not have drive letters return [] From fa44a64200f57d763d5b0904f6c45657d3ff4f85 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Sun, 5 Sep 2021 23:16:41 +0200 Subject: [PATCH 05/39] Add support for root_path parameter --- ipyfilechooser/filechooser.py | 4 +++- ipyfilechooser/utils.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 078b96d..0d355a3 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -23,6 +23,7 @@ class FileChooser(VBox, ValueWidget): use_dir_icons: bool = False, show_only_dirs: bool = False, filter_pattern: Optional[Sequence[str]] = None, + root_path: str = '', layout: Layout = Layout(width='500px'), **kwargs): """Initialize FileChooser object.""" @@ -37,6 +38,7 @@ class FileChooser(VBox, ValueWidget): self._use_dir_icons = use_dir_icons self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern + self._root_path = root_path self._callback: Optional[Callable] = None # Widgets @@ -160,7 +162,7 @@ class FileChooser(VBox, ValueWidget): filename = '' # Set form values - self._pathlist.options = get_subpaths(path) + self._pathlist.options = get_subpaths(path, self._root_path) self._pathlist.value = path self._filename.value = filename diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 99c58d9..34b25e0 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -97,7 +97,7 @@ def prepend_dir_icons(dir_list: Iterable[str]) -> List[str]: def get_drive_letters(path: str) -> List[str]: - """Get drive letters.""" + """Get all drive letters minus the drive used in path.""" if sys.platform == 'win32': # Check if path uses upper or lowercase drive letters chars = string.ascii_lowercase From 34802201cb10f4571051928752d6078359a89626 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Mon, 6 Sep 2021 00:00:48 +0200 Subject: [PATCH 06/39] One step closer --- ipyfilechooser/filechooser.py | 6 ++++-- ipyfilechooser/utils.py | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 0d355a3..7b5d57a 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -172,7 +172,8 @@ class FileChooser(VBox, ValueWidget): show_hidden=self._show_hidden, prepend_icons=False, show_only_dirs=self._show_only_dirs, - filter_pattern=self._filter_pattern + filter_pattern=self._filter_pattern, + root_path=self._root_path ) # file/folder display names @@ -181,7 +182,8 @@ class FileChooser(VBox, ValueWidget): show_hidden=self._show_hidden, prepend_icons=self._use_dir_icons, show_only_dirs=self._show_only_dirs, - filter_pattern=self._filter_pattern + filter_pattern=self._filter_pattern, + root_path=self._root_path ) # Dict to map real names to display names diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 34b25e0..2f435da 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -6,7 +6,7 @@ import sys from typing import List, Sequence, Iterable, Optional -def get_subpaths(path: str, root_path: str) -> List[str]: +def get_subpaths(path: str, root_path: str = '') -> List[str]: """Walk a path and return a list of subpaths.""" if os.path.isfile(path): path = os.path.dirname(path) @@ -64,7 +64,8 @@ def get_dir_contents( show_hidden: bool = False, prepend_icons: bool = False, show_only_dirs: bool = False, - filter_pattern: Optional[Sequence[str]] = None) -> List[str]: + filter_pattern: Optional[Sequence[str]] = None, + root_path: str = '') -> List[str]: """Get directory contents.""" files = list() dirs = list() @@ -83,7 +84,7 @@ def get_dir_contents( files.append(item) else: files.append(item) - if has_parent(path): + if has_parent(strip_root_path(path, root_path)): dirs.insert(0, '..') if prepend_icons: return prepend_dir_icons(sorted(dirs)) + sorted(files) From f60e0b5f3b3edf1a35ac9fab676a90f0e580f901 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Mon, 6 Sep 2021 22:50:08 +0200 Subject: [PATCH 07/39] Test release --- ipyfilechooser/filechooser.py | 66 +++++++++++++++++++++++++---------- ipyfilechooser/utils.py | 22 ++++++------ 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 7b5d57a..647d432 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -2,7 +2,17 @@ import os from typing import Optional, Sequence, Mapping, Callable from ipywidgets import Dropdown, Text, Select, Button, HTML from ipywidgets import Layout, GridBox, Box, HBox, VBox, ValueWidget -from .utils import get_subpaths, get_dir_contents, match_item +from .utils import get_subpaths, get_dir_contents, match_item, strip_parent_path + + +class SandboxPathError(Exception): + """SandboxPathError class.""" + + def __init__(self, path: str, sandbox_path: str, message: str = ''): + self.path = path + self.sandbox_path = sandbox_path + self.message = message or f'{sandbox_path} is not a parent of {path}.' + super().__init__(self.message) class FileChooser(VBox, ValueWidget): @@ -23,14 +33,14 @@ class FileChooser(VBox, ValueWidget): use_dir_icons: bool = False, show_only_dirs: bool = False, filter_pattern: Optional[Sequence[str]] = None, - root_path: str = '', + sandbox_path: str = '', layout: Layout = Layout(width='500px'), **kwargs): """Initialize FileChooser object.""" self._default_path = os.path.normpath(path) self._default_filename = filename - self._selected_path = None - self._selected_filename = None + self._selected_path: Optional[str] = None + self._selected_filename: Optional[str] = None self._show_hidden = show_hidden self._select_desc = select_desc self._change_desc = change_desc @@ -38,7 +48,7 @@ class FileChooser(VBox, ValueWidget): self._use_dir_icons = use_dir_icons self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern - self._root_path = root_path + self._sandbox_path = os.path.normpath(sandbox_path) self._callback: Optional[Callable] = None # Widgets @@ -151,6 +161,10 @@ class FileChooser(VBox, ValueWidget): def _set_form_values(self, path: str, filename: str) -> None: """Set the form values.""" + # Check if the path falls inside the configured sandbox path + if not self._is_sandboxed(os.path.normpath(path)): + raise SandboxPathError(path, self._sandbox_path) + # Disable triggers to prevent selecting an entry in the Select # box from automatically triggering a new event. self._pathlist.unobserve(self._on_pathlist_select, names='value') @@ -162,8 +176,9 @@ class FileChooser(VBox, ValueWidget): filename = '' # Set form values - self._pathlist.options = get_subpaths(path, self._root_path) - self._pathlist.value = path + sandboxed_path = self._apply_sandbox(path) + self._pathlist.options = get_subpaths(sandboxed_path) + self._pathlist.value = sandboxed_path self._filename.value = filename # file/folder real names @@ -173,7 +188,7 @@ class FileChooser(VBox, ValueWidget): prepend_icons=False, show_only_dirs=self._show_only_dirs, filter_pattern=self._filter_pattern, - root_path=self._root_path + sandbox_path=self._sandbox_path ) # file/folder display names @@ -183,7 +198,7 @@ class FileChooser(VBox, ValueWidget): prepend_icons=self._use_dir_icons, show_only_dirs=self._show_only_dirs, filter_pattern=self._filter_pattern, - root_path=self._root_path + sandbox_path=self._sandbox_path ) # Dict to map real names to display names @@ -243,25 +258,28 @@ class FileChooser(VBox, ValueWidget): def _on_pathlist_select(self, change: Mapping[str, str]) -> None: """Handle selecting a path entry.""" - self._set_form_values(change['new'], self._filename.value) + self._set_form_values(self._remove_sandbox(change['new']), self._filename.value) def _on_dircontent_select(self, change: Mapping[str, str]) -> None: """Handle selecting a folder entry.""" - new_path = os.path.realpath(os.path.join(self._pathlist.value, self._map_disp_to_name[change['new']])) + new_path = os.path.realpath(os.path.join( + self._remove_sandbox(self._pathlist.value), + self._map_disp_to_name[change['new']] + )) # Check if folder or file if os.path.isdir(new_path): path = new_path filename = self._filename.value else: - path = self._pathlist.value + path = self._remove_sandbox(self._pathlist.value) filename = self._map_disp_to_name[change['new']] self._set_form_values(path, filename) def _on_filename_change(self, change: Mapping[str, str]) -> None: """Handle filename field changes.""" - self._set_form_values(self._pathlist.value, change['new']) + self._set_form_values(self._remove_sandbox(self._pathlist.value), change['new']) def _on_select_click(self, _b) -> None: """Handle select button clicks.""" @@ -298,7 +316,7 @@ class FileChooser(VBox, ValueWidget): def _apply_selection(self) -> None: """Close the dialog and apply the selection.""" - self._selected_path = self._pathlist.value + self._selected_path = self._remove_sandbox(self._pathlist.value) self._selected_filename = self._filename.value if ((self._selected_path is not None) and (self._selected_filename is not None)): @@ -308,9 +326,9 @@ class FileChooser(VBox, ValueWidget): self._select.description = self._change_desc if os.path.isfile(selected): - self._label.value = self._LBL_TEMPLATE.format(selected, 'orange') + self._label.value = self._LBL_TEMPLATE.format(self._apply_sandbox(selected), 'orange') else: - self._label.value = self._LBL_TEMPLATE.format(selected, 'green') + self._label.value = self._LBL_TEMPLATE.format(self._apply_sandbox(selected), 'green') def _on_cancel_click(self, _b) -> None: """Handle cancel button clicks.""" @@ -318,6 +336,18 @@ class FileChooser(VBox, ValueWidget): self._cancel.layout.display = 'none' self._select.disabled = False + def _remove_sandbox(self, path) -> str: + """Calculate the full path using the sandbox path.""" + return os.path.normpath(self._sandbox_path + path) + + def _apply_sandbox(self, path) -> str: + """Calculate the sandboxed path using the sandbox path.""" + return os.path.normpath(strip_parent_path(path, self._sandbox_path)) + + def _is_sandboxed(self, path) -> bool: + """Verifies if sandbox_path is a parent of path.""" + return os.path.commonpath([self._sandbox_path, path]) == self._sandbox_path + def reset(self, path: Optional[str] = None, filename: Optional[str] = None) -> None: """Reset the form to the default path and filename.""" self._selected_path = None @@ -347,7 +377,7 @@ class FileChooser(VBox, ValueWidget): def refresh(self) -> None: """Re-render the form.""" - self._set_form_values(self._pathlist.value, self._filename.value) + self._set_form_values(self._remove_sandbox(self._pathlist.value), self._filename.value) @property def show_hidden(self) -> bool: @@ -421,7 +451,7 @@ class FileChooser(VBox, ValueWidget): def default_filename(self, filename: str) -> None: """Set the default_filename.""" self._default_filename = filename - self._set_form_values(self._pathlist.value, self._default_filename) + self._set_form_values(self._remove_sandbox(self._pathlist.value), self._default_filename) @property def show_only_dirs(self) -> bool: diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 2f435da..fbe2131 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -6,23 +6,21 @@ import sys from typing import List, Sequence, Iterable, Optional -def get_subpaths(path: str, root_path: str = '') -> List[str]: +def get_subpaths(path: str) -> List[str]: """Walk a path and return a list of subpaths.""" if os.path.isfile(path): path = os.path.dirname(path) - path = strip_root_path(path, root_path) paths = [path] - path, tail = os.path.split(path) while tail: paths.append(path) path, tail = os.path.split(path) - if not root_path: + if len(os.path.splitdrive(path)[0]) == 2: + # If path starts with a drive letter, get the remaining drive letters try: - # Add Windows drive letters, but remove the current drive drives = get_drive_letters(paths[-1]) paths.extend(drives) except ValueError: @@ -30,12 +28,12 @@ def get_subpaths(path: str, root_path: str = '') -> List[str]: return paths -def strip_root_path(path: str, root_path: str) -> str: - """Remove a root path from a path""" - if path == root_path: +def strip_parent_path(path: str, parent_path: str) -> str: + """Remove a parent path from a path.""" + if path == parent_path: return os.path.sep - elif path.startswith(root_path): - return path[len(root_path):] + elif path.startswith(parent_path): + return path[len(parent_path):] return '' @@ -65,7 +63,7 @@ def get_dir_contents( prepend_icons: bool = False, show_only_dirs: bool = False, filter_pattern: Optional[Sequence[str]] = None, - root_path: str = '') -> List[str]: + sandbox_path: str = '') -> List[str]: """Get directory contents.""" files = list() dirs = list() @@ -84,7 +82,7 @@ def get_dir_contents( files.append(item) else: files.append(item) - if has_parent(strip_root_path(path, root_path)): + if has_parent(strip_parent_path(path, sandbox_path)): dirs.insert(0, '..') if prepend_icons: return prepend_dir_icons(sorted(dirs)) + sorted(files) From 371aa501f70176aecfd525a8ea15a41e72ec7869 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Mon, 6 Sep 2021 23:29:41 +0200 Subject: [PATCH 08/39] Fix normalized sandbox_path handling --- ipyfilechooser/filechooser.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 647d432..c53ffe4 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -48,7 +48,7 @@ class FileChooser(VBox, ValueWidget): self._use_dir_icons = use_dir_icons self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern - self._sandbox_path = os.path.normpath(sandbox_path) + self._sandbox_path = os.path.normpath(sandbox_path) if sandbox_path else sandbox_path self._callback: Optional[Callable] = None # Widgets @@ -162,8 +162,9 @@ class FileChooser(VBox, ValueWidget): def _set_form_values(self, path: str, filename: str) -> None: """Set the form values.""" # Check if the path falls inside the configured sandbox path - if not self._is_sandboxed(os.path.normpath(path)): - raise SandboxPathError(path, self._sandbox_path) + if self._sandbox_path != '': + if not self._is_sandboxed(os.path.normpath(path)): + raise SandboxPathError(path, self._sandbox_path) # Disable triggers to prevent selecting an entry in the Select # box from automatically triggering a new event. From a2c3f52a7c001bfbf3dc7e5735b6023c999567f0 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Mon, 6 Sep 2021 23:59:03 +0200 Subject: [PATCH 09/39] Properly handle Windows case insensitive paths --- ipyfilechooser/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index fbe2131..4dd5734 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -30,6 +30,9 @@ def get_subpaths(path: str) -> List[str]: def strip_parent_path(path: str, parent_path: str) -> str: """Remove a parent path from a path.""" + path = os.path.normcase(path) + parent_path = os.path.normcase(parent_path) + if path == parent_path: return os.path.sep elif path.startswith(parent_path): From f81b1cc0a9dee58027862e3d27d5e1ab0a4a0a61 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 7 Sep 2021 00:04:20 +0200 Subject: [PATCH 10/39] Comment --- ipyfilechooser/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 4dd5734..a5fb98d 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -30,6 +30,7 @@ def get_subpaths(path: str) -> List[str]: def strip_parent_path(path: str, parent_path: str) -> str: """Remove a parent path from a path.""" + # Normalize case so Windows can compare paths in a case-insensitive way path = os.path.normcase(path) parent_path = os.path.normcase(parent_path) From 01a9e60309c25f78de046dd83ae9d43b48701cb1 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 7 Sep 2021 00:25:34 +0200 Subject: [PATCH 11/39] Cleanup --- ipyfilechooser/filechooser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index c53ffe4..26d3359 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -162,9 +162,8 @@ class FileChooser(VBox, ValueWidget): def _set_form_values(self, path: str, filename: str) -> None: """Set the form values.""" # Check if the path falls inside the configured sandbox path - if self._sandbox_path != '': - if not self._is_sandboxed(os.path.normpath(path)): - raise SandboxPathError(path, self._sandbox_path) + if self._sandbox_path and not self._is_sandboxed(os.path.normpath(path)): + raise SandboxPathError(path, self._sandbox_path) # Disable triggers to prevent selecting an entry in the Select # box from automatically triggering a new event. @@ -238,6 +237,7 @@ class FileChooser(VBox, ValueWidget): # - equal an existing folder in the current view # - equal the already selected values # - don't match the provided filter pattern(s) + # - contains a path separator check1 = filename in dircontent_real_names check2 = os.path.isdir(os.path.join(path, filename)) check3 = False @@ -505,7 +505,7 @@ class FileChooser(VBox, ValueWidget): selected = None if ((self._selected_path is not None) and (self._selected_filename is not None)): - selected = os.path.join(self._selected_path, self._selected_filename) + selected = os.path.normpath(os.path.join(self._selected_path, self._selected_filename)) return selected From fcabb448aa06066e0cf9746ab8a25d937bb47b96 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 7 Sep 2021 00:33:28 +0200 Subject: [PATCH 12/39] Minor fixes --- ipyfilechooser/filechooser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 26d3359..499a0f0 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -339,11 +339,11 @@ class FileChooser(VBox, ValueWidget): def _remove_sandbox(self, path) -> str: """Calculate the full path using the sandbox path.""" - return os.path.normpath(self._sandbox_path + path) + return os.path.normpath(self._sandbox_path + os.path.sep + path) def _apply_sandbox(self, path) -> str: """Calculate the sandboxed path using the sandbox path.""" - return os.path.normpath(strip_parent_path(path, self._sandbox_path)) + return strip_parent_path(path, self._sandbox_path) def _is_sandboxed(self, path) -> bool: """Verifies if sandbox_path is a parent of path.""" From b2dd8d6cc3d32645f191766f882dea0c35f5f33a Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 7 Sep 2021 01:04:20 +0200 Subject: [PATCH 13/39] Filter filenames --- ipyfilechooser/filechooser.py | 13 +++++++------ ipyfilechooser/utils.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 499a0f0..163f48f 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -2,7 +2,7 @@ import os from typing import Optional, Sequence, Mapping, Callable from ipywidgets import Dropdown, Text, Select, Button, HTML from ipywidgets import Layout, GridBox, Box, HBox, VBox, ValueWidget -from .utils import get_subpaths, get_dir_contents, match_item, strip_parent_path +from .utils import get_subpaths, get_dir_contents, match_item, strip_parent_path, is_valid_filename class SandboxPathError(Exception): @@ -237,22 +237,23 @@ class FileChooser(VBox, ValueWidget): # - equal an existing folder in the current view # - equal the already selected values # - don't match the provided filter pattern(s) - # - contains a path separator + # - contains an invalid character sequence check1 = filename in dircontent_real_names check2 = os.path.isdir(os.path.join(path, filename)) - check3 = False + check3 = not is_valid_filename(filename) check4 = False + check5 = False # Only check selected if selected is set if ((self._selected_path is not None) and (self._selected_filename is not None)): selected = os.path.join(self._selected_path, self._selected_filename) - check3 = os.path.join(path, filename) == selected + check4 = os.path.join(path, filename) == selected # Ensure only allowed extensions are used if self._filter_pattern: - check4 = not match_item(filename, self._filter_pattern) + check5 = not match_item(filename, self._filter_pattern) - if (check1 and check2) or check3 or check4: + if (check1 and check2) or check3 or check4 or check5: self._select.disabled = True else: self._select.disabled = False diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index a5fb98d..5933b40 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -115,3 +115,13 @@ def get_drive_letters(path: str) -> List[str]: else: # Unix does not have drive letters return [] + + +def is_valid_filename(filename: str) -> bool: + """Verifies if a filename does not contain illegal character sequences""" + return ( + not filename.startswith('/') and + not filename.startswith('\\') and + '../' not in filename and + '..\\' not in filename + ) \ No newline at end of file From 704db465423c383cb32985470385c5ba7f02e23d Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 7 Sep 2021 01:15:52 +0200 Subject: [PATCH 14/39] Validate filename --- ipyfilechooser/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 5933b40..d54a85f 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -122,6 +122,8 @@ def is_valid_filename(filename: str) -> bool: return ( not filename.startswith('/') and not filename.startswith('\\') and + not filename.endswith('/') and + not filename.endswith('\\') and '../' not in filename and '..\\' not in filename - ) \ No newline at end of file + ) From 1b08e8dce767e79dca537a213936331498d60bcc Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 7 Sep 2021 01:29:29 +0200 Subject: [PATCH 15/39] Fix sandbo functions --- ipyfilechooser/filechooser.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 163f48f..c47fb78 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -340,11 +340,17 @@ class FileChooser(VBox, ValueWidget): def _remove_sandbox(self, path) -> str: """Calculate the full path using the sandbox path.""" - return os.path.normpath(self._sandbox_path + os.path.sep + path) + if self._sandbox_path: + path = os.path.normpath(self._sandbox_path + os.path.sep + path) + + return path def _apply_sandbox(self, path) -> str: """Calculate the sandboxed path using the sandbox path.""" - return strip_parent_path(path, self._sandbox_path) + if self._sandbox_path: + path = strip_parent_path(path, self._sandbox_path) + + return path def _is_sandboxed(self, path) -> bool: """Verifies if sandbox_path is a parent of path.""" From a1ebe8df4138585fab557285ee660999f30588e5 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Thu, 9 Sep 2021 00:21:49 +0200 Subject: [PATCH 16/39] Add path and filename validation --- ipyfilechooser/filechooser.py | 105 ++++++++++++++++++++++++---------- ipyfilechooser/utils.py | 69 ++++++++++------------ 2 files changed, 104 insertions(+), 70 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index c47fb78..bd82470 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -2,7 +2,8 @@ import os from typing import Optional, Sequence, Mapping, Callable from ipywidgets import Dropdown, Text, Select, Button, HTML from ipywidgets import Layout, GridBox, Box, HBox, VBox, ValueWidget -from .utils import get_subpaths, get_dir_contents, match_item, strip_parent_path, is_valid_filename +from .utils import get_subpaths, get_dir_contents, match_item, strip_parent_path +from .utils import is_valid_filename, get_drive_letters, normalize_path, has_parent_path class SandboxPathError(Exception): @@ -11,7 +12,20 @@ class SandboxPathError(Exception): def __init__(self, path: str, sandbox_path: str, message: str = ''): self.path = path self.sandbox_path = sandbox_path - self.message = message or f'{sandbox_path} is not a parent of {path}.' + self.message = message or f'{path} is located outside of {sandbox_path}' + super().__init__(self.message) + + +class InvalidFileNameError(Exception): + """InvalidFileNameError class.""" + invalid_str = [os.sep, os.pardir] + + if os.altsep: + invalid_str.append(os.altsep) + + def __init__(self, filename: str, message: str = ''): + self.filename = filename + self.message = message or f'{filename} cannot contain {self.invalid_str}' super().__init__(self.message) @@ -37,7 +51,15 @@ class FileChooser(VBox, ValueWidget): layout: Layout = Layout(width='500px'), **kwargs): """Initialize FileChooser object.""" - self._default_path = os.path.normpath(path) + # Check if path and sandbox_path align + if not has_parent_path(normalize_path(path), normalize_path(sandbox_path)): + raise SandboxPathError(path, sandbox_path) + + # Verify the filename is valid + if not is_valid_filename(filename): + raise InvalidFileNameError(filename) + + self._default_path = normalize_path(path) self._default_filename = filename self._selected_path: Optional[str] = None self._selected_filename: Optional[str] = None @@ -48,7 +70,7 @@ class FileChooser(VBox, ValueWidget): self._use_dir_icons = use_dir_icons self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern - self._sandbox_path = os.path.normpath(sandbox_path) if sandbox_path else sandbox_path + self._sandbox_path = normalize_path(sandbox_path) if sandbox_path else '' self._callback: Optional[Callable] = None # Widgets @@ -162,7 +184,7 @@ class FileChooser(VBox, ValueWidget): def _set_form_values(self, path: str, filename: str) -> None: """Set the form values.""" # Check if the path falls inside the configured sandbox path - if self._sandbox_path and not self._is_sandboxed(os.path.normpath(path)): + if not has_parent_path(path, self._sandbox_path): raise SandboxPathError(path, self._sandbox_path) # Disable triggers to prevent selecting an entry in the Select @@ -176,9 +198,16 @@ class FileChooser(VBox, ValueWidget): filename = '' # Set form values - sandboxed_path = self._apply_sandbox(path) - self._pathlist.options = get_subpaths(sandboxed_path) - self._pathlist.value = sandboxed_path + restricted_path = self._restrict_path(path) + subpaths = get_subpaths(restricted_path) + + if len(os.path.splitdrive(subpaths[-1])) == 2: + # Add missing Windows drive letters + drives = get_drive_letters() + subpaths.extend(list(set(drives) - set(subpaths))) + + self._pathlist.options = subpaths + self._pathlist.value = restricted_path self._filename.value = filename # file/folder real names @@ -188,7 +217,7 @@ class FileChooser(VBox, ValueWidget): prepend_icons=False, show_only_dirs=self._show_only_dirs, filter_pattern=self._filter_pattern, - sandbox_path=self._sandbox_path + top_path=self._sandbox_path ) # file/folder display names @@ -198,7 +227,7 @@ class FileChooser(VBox, ValueWidget): prepend_icons=self._use_dir_icons, show_only_dirs=self._show_only_dirs, filter_pattern=self._filter_pattern, - sandbox_path=self._sandbox_path + top_path=self._sandbox_path ) # Dict to map real names to display names @@ -235,9 +264,9 @@ class FileChooser(VBox, ValueWidget): if self._gb.layout.display is None: # Disable the select button if path and filename # - equal an existing folder in the current view + # - contains an invalid character sequence # - equal the already selected values # - don't match the provided filter pattern(s) - # - contains an invalid character sequence check1 = filename in dircontent_real_names check2 = os.path.isdir(os.path.join(path, filename)) check3 = not is_valid_filename(filename) @@ -260,12 +289,12 @@ class FileChooser(VBox, ValueWidget): def _on_pathlist_select(self, change: Mapping[str, str]) -> None: """Handle selecting a path entry.""" - self._set_form_values(self._remove_sandbox(change['new']), self._filename.value) + self._set_form_values(self._expand_path(change['new']), self._filename.value) def _on_dircontent_select(self, change: Mapping[str, str]) -> None: """Handle selecting a folder entry.""" new_path = os.path.realpath(os.path.join( - self._remove_sandbox(self._pathlist.value), + self._expand_path(self._pathlist.value), self._map_disp_to_name[change['new']] )) @@ -274,14 +303,14 @@ class FileChooser(VBox, ValueWidget): path = new_path filename = self._filename.value else: - path = self._remove_sandbox(self._pathlist.value) + path = self._expand_path(self._pathlist.value) filename = self._map_disp_to_name[change['new']] self._set_form_values(path, filename) def _on_filename_change(self, change: Mapping[str, str]) -> None: """Handle filename field changes.""" - self._set_form_values(self._remove_sandbox(self._pathlist.value), change['new']) + self._set_form_values(self._expand_path(self._pathlist.value), change['new']) def _on_select_click(self, _b) -> None: """Handle select button clicks.""" @@ -318,7 +347,7 @@ class FileChooser(VBox, ValueWidget): def _apply_selection(self) -> None: """Close the dialog and apply the selection.""" - self._selected_path = self._remove_sandbox(self._pathlist.value) + self._selected_path = self._expand_path(self._pathlist.value) self._selected_filename = self._filename.value if ((self._selected_path is not None) and (self._selected_filename is not None)): @@ -328,9 +357,9 @@ class FileChooser(VBox, ValueWidget): self._select.description = self._change_desc if os.path.isfile(selected): - self._label.value = self._LBL_TEMPLATE.format(self._apply_sandbox(selected), 'orange') + self._label.value = self._LBL_TEMPLATE.format(self._restrict_path(selected), 'orange') else: - self._label.value = self._LBL_TEMPLATE.format(self._apply_sandbox(selected), 'green') + self._label.value = self._LBL_TEMPLATE.format(self._restrict_path(selected), 'green') def _on_cancel_click(self, _b) -> None: """Handle cancel button clicks.""" @@ -338,26 +367,32 @@ class FileChooser(VBox, ValueWidget): self._cancel.layout.display = 'none' self._select.disabled = False - def _remove_sandbox(self, path) -> str: + def _expand_path(self, path) -> str: """Calculate the full path using the sandbox path.""" if self._sandbox_path: - path = os.path.normpath(self._sandbox_path + os.path.sep + path) + path = os.path.join(self._sandbox_path, path.lstrip(os.sep)) return path - def _apply_sandbox(self, path) -> str: + def _restrict_path(self, path) -> str: """Calculate the sandboxed path using the sandbox path.""" - if self._sandbox_path: + if self._sandbox_path == path: + path = os.sep + elif self._sandbox_path: path = strip_parent_path(path, self._sandbox_path) return path - def _is_sandboxed(self, path) -> bool: - """Verifies if sandbox_path is a parent of path.""" - return os.path.commonpath([self._sandbox_path, path]) == self._sandbox_path - def reset(self, path: Optional[str] = None, filename: Optional[str] = None) -> None: """Reset the form to the default path and filename.""" + # Check if path and sandbox_path align + if path is not None and not has_parent_path(normalize_path(path), self._sandbox_path): + raise SandboxPathError(path, self._sandbox_path) + + # Verify the filename is valid + if filename is not None and not is_valid_filename(filename): + raise InvalidFileNameError(filename) + self._selected_path = None self._selected_filename = None @@ -366,7 +401,7 @@ class FileChooser(VBox, ValueWidget): self._label.value = self._LBL_TEMPLATE.format(self._LBL_NOFILE, 'black') if path is not None: - self._default_path = os.path.normpath(path) + self._default_path = normalize_path(path) if filename is not None: self._default_filename = filename @@ -385,7 +420,7 @@ class FileChooser(VBox, ValueWidget): def refresh(self) -> None: """Re-render the form.""" - self._set_form_values(self._remove_sandbox(self._pathlist.value), self._filename.value) + self._set_form_values(self._expand_path(self._pathlist.value), self._filename.value) @property def show_hidden(self) -> bool: @@ -447,7 +482,11 @@ class FileChooser(VBox, ValueWidget): @default_path.setter def default_path(self, path: str) -> None: """Set the default_path.""" - self._default_path = os.path.normpath(path) + # Check if path and sandbox_path align + if not has_parent_path(normalize_path(path), self._sandbox_path): + raise SandboxPathError(path, self._sandbox_path) + + self._default_path = normalize_path(path) self._set_form_values(self._default_path, self._filename.value) @property @@ -458,8 +497,12 @@ class FileChooser(VBox, ValueWidget): @default_filename.setter def default_filename(self, filename: str) -> None: """Set the default_filename.""" + # Verify the filename is valid + if not is_valid_filename(filename): + raise InvalidFileNameError(filename) + self._default_filename = filename - self._set_form_values(self._remove_sandbox(self._pathlist.value), self._default_filename) + self._set_form_values(self._expand_path(self._pathlist.value), self._default_filename) @property def show_only_dirs(self) -> bool: @@ -512,7 +555,7 @@ class FileChooser(VBox, ValueWidget): selected = None if ((self._selected_path is not None) and (self._selected_filename is not None)): - selected = os.path.normpath(os.path.join(self._selected_path, self._selected_filename)) + selected = os.path.join(self._selected_path, self._selected_filename) return selected diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index d54a85f..4dd9a51 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -18,27 +18,15 @@ def get_subpaths(path: str) -> List[str]: paths.append(path) path, tail = os.path.split(path) - if len(os.path.splitdrive(path)[0]) == 2: - # If path starts with a drive letter, get the remaining drive letters - try: - drives = get_drive_letters(paths[-1]) - paths.extend(drives) - except ValueError: - pass return paths def strip_parent_path(path: str, parent_path: str) -> str: """Remove a parent path from a path.""" - # Normalize case so Windows can compare paths in a case-insensitive way - path = os.path.normcase(path) - parent_path = os.path.normcase(parent_path) - - if path == parent_path: - return os.path.sep - elif path.startswith(parent_path): + if path.startswith(parent_path): return path[len(parent_path):] - return '' + + return path def has_parent(path: str) -> bool: @@ -46,6 +34,11 @@ def has_parent(path: str) -> bool: return os.path.basename(path) != '' +def has_parent_path(path: str, parent_path: str) -> bool: + """Verifies if path falls under parent_path.""" + return os.path.commonpath([path, parent_path]) == parent_path + + def match_item(item: str, filter_pattern: Sequence[str]) -> bool: """Check if a string matches one or more fnmatch patterns.""" if isinstance(filter_pattern, str): @@ -67,7 +60,7 @@ def get_dir_contents( prepend_icons: bool = False, show_only_dirs: bool = False, filter_pattern: Optional[Sequence[str]] = None, - sandbox_path: str = '') -> List[str]: + top_path: str = '') -> List[str]: """Get directory contents.""" files = list() dirs = list() @@ -86,8 +79,8 @@ def get_dir_contents( files.append(item) else: files.append(item) - if has_parent(strip_parent_path(path, sandbox_path)): - dirs.insert(0, '..') + if has_parent(strip_parent_path(path, top_path)): + dirs.insert(0, os.pardir) if prepend_icons: return prepend_dir_icons(sorted(dirs)) + sorted(files) else: @@ -99,31 +92,29 @@ def prepend_dir_icons(dir_list: Iterable[str]) -> List[str]: return ['\U0001F4C1 ' + dirname for dirname in dir_list] -def get_drive_letters(path: str) -> List[str]: +def get_drive_letters() -> List[str]: """Get all drive letters minus the drive used in path.""" + drives: List[str] = [] + if sys.platform == 'win32': - # Check if path uses upper or lowercase drive letters - chars = string.ascii_lowercase - - if path[0].isupper(): - chars = string.ascii_uppercase - # Windows has drive letters - drives = [f'{d}:\\' for d in chars if os.path.exists(f'{d}:')] - drives.remove(path) - return drives - else: - # Unix does not have drive letters - return [] + drives = [f'{d}:\\' for d in string.ascii_lowercase if os.path.exists(f'{d}:')] + + return drives def is_valid_filename(filename: str) -> bool: """Verifies if a filename does not contain illegal character sequences""" - return ( - not filename.startswith('/') and - not filename.startswith('\\') and - not filename.endswith('/') and - not filename.endswith('\\') and - '../' not in filename and - '..\\' not in filename - ) + valid = True + valid = valid and os.pardir not in filename + valid = valid and os.sep not in filename + + if os.altsep: + valid = valid and os.altsep not in filename + + return valid + + +def normalize_path(path: str) -> str: + """Normalize a path string.""" + return os.path.normpath(os.path.normcase(path)) From 38db8c6ca120a498c5beb0d5c6085730f580c9c4 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Thu, 9 Sep 2021 00:40:01 +0200 Subject: [PATCH 17/39] Bug fix in helper functions --- ipyfilechooser/utils.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 4dd9a51..d35b22a 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -36,7 +36,12 @@ def has_parent(path: str) -> bool: def has_parent_path(path: str, parent_path: str) -> bool: """Verifies if path falls under parent_path.""" - return os.path.commonpath([path, parent_path]) == parent_path + if parent_path: + check = os.path.commonpath([path, parent_path]) == parent_path + else: + check = True + + return check def match_item(item: str, filter_pattern: Sequence[str]) -> bool: @@ -117,4 +122,9 @@ def is_valid_filename(filename: str) -> bool: def normalize_path(path: str) -> str: """Normalize a path string.""" - return os.path.normpath(os.path.normcase(path)) + normalized_path = '' + + if path: + normalized_path = os.path.normpath(os.path.normcase(path)) + + return normalized_path From 6fc806bb3d6a3de1983af8ab29c7e944a1de77cd Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Thu, 9 Sep 2021 10:39:20 +0200 Subject: [PATCH 18/39] Add sandbox_path property --- ipyfilechooser/filechooser.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index bd82470..8eae84c 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -70,7 +70,7 @@ class FileChooser(VBox, ValueWidget): self._use_dir_icons = use_dir_icons self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern - self._sandbox_path = normalize_path(sandbox_path) if sandbox_path else '' + self._sandbox_path = normalize_path(sandbox_path) self._callback: Optional[Callable] = None # Widgets @@ -504,6 +504,23 @@ class FileChooser(VBox, ValueWidget): self._default_filename = filename self._set_form_values(self._expand_path(self._pathlist.value), self._default_filename) + @property + def sandbox_path(self) -> str: + """Get the sandbox_path.""" + return self._sandbox_path + + @sandbox_path.setter + def sandbox_path(self, sandbox_path: str) -> None: + """Set the sandbox_path.""" + # Check if path and sandbox_path align + if not has_parent_path(self._default_path, normalize_path(sandbox_path)): + raise SandboxPathError(self._default_path, sandbox_path) + + self._sandbox_path = normalize_path(sandbox_path) + + # Reset the dialog + self.reset() + @property def show_only_dirs(self) -> bool: """Get show_only_dirs property value.""" From 80bb352294bc2577b346973ccbb005d83d66b508 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Mon, 13 Sep 2021 23:49:04 +0200 Subject: [PATCH 19/39] Custom dir icon support + bug fixes --- ipyfilechooser/filechooser.py | 78 +++++++++++++++++------------------ ipyfilechooser/utils.py | 10 ++--- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 8eae84c..a2de4a5 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -44,7 +44,7 @@ class FileChooser(VBox, ValueWidget): change_desc: str = 'Change', show_hidden: bool = False, select_default: bool = False, - use_dir_icons: bool = False, + dir_icon: Optional[str] = '\U0001F4C1', show_only_dirs: bool = False, filter_pattern: Optional[Sequence[str]] = None, sandbox_path: str = '', @@ -67,7 +67,7 @@ class FileChooser(VBox, ValueWidget): self._select_desc = select_desc self._change_desc = change_desc self._select_default = select_default - self._use_dir_icons = use_dir_icons + self._dir_icon = dir_icon self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern self._sandbox_path = normalize_path(sandbox_path) @@ -214,8 +214,8 @@ class FileChooser(VBox, ValueWidget): dircontent_real_names = get_dir_contents( path, show_hidden=self._show_hidden, - prepend_icons=False, show_only_dirs=self._show_only_dirs, + dir_icon='', filter_pattern=self._filter_pattern, top_path=self._sandbox_path ) @@ -224,8 +224,8 @@ class FileChooser(VBox, ValueWidget): dircontent_display_names = get_dir_contents( path, show_hidden=self._show_hidden, - prepend_icons=self._use_dir_icons, show_only_dirs=self._show_only_dirs, + dir_icon=self._dir_icon, filter_pattern=self._filter_pattern, top_path=self._sandbox_path ) @@ -355,6 +355,7 @@ class FileChooser(VBox, ValueWidget): self._gb.layout.display = 'none' self._cancel.layout.display = 'none' self._select.description = self._change_desc + self._select.disabled = False if os.path.isfile(selected): self._label.value = self._LBL_TEMPLATE.format(self._restrict_path(selected), 'orange') @@ -393,11 +394,17 @@ class FileChooser(VBox, ValueWidget): if filename is not None and not is_valid_filename(filename): raise InvalidFileNameError(filename) + # Remove selection self._selected_path = None self._selected_filename = None + # Hide dialog and cancel button + self._gb.layout.display = 'none' + self._cancel.layout.display = 'none' + # Reset select button and label self._select.description = self._select_desc + self._select.disabled = False self._label.value = self._LBL_TEMPLATE.format(self._LBL_NOFILE, 'black') if path is not None: @@ -406,13 +413,7 @@ class FileChooser(VBox, ValueWidget): if filename is not None: self._default_filename = filename - # Set a proper filename value - if self._show_only_dirs: - filename = '' - else: - filename = self._default_filename - - self._set_form_values(self._default_path, filename) + self._set_form_values(self._default_path, self._default_filename) # Use the defaults as the selected values if self._select_default: @@ -434,14 +435,14 @@ class FileChooser(VBox, ValueWidget): self.refresh() @property - def use_dir_icons(self) -> bool: - """Get _use_dir_icons value.""" - return self._use_dir_icons + def dir_icon(self) -> Optional[str]: + """Get dir icon value.""" + return self._dir_icon - @use_dir_icons.setter - def use_dir_icons(self, dir_icons: bool) -> None: - """Set _use_dir_icons value.""" - self._use_dir_icons = dir_icons + @dir_icon.setter + def dir_icon(self, dir_icon: Optional[str]) -> None: + """Set dir icon value.""" + self._dir_icon = dir_icon self.refresh() @property @@ -588,27 +589,26 @@ class FileChooser(VBox, ValueWidget): def __repr__(self) -> str: """Build string representation.""" - str_ = ( - "FileChooser(" - "path='{0}', " - "filename='{1}', " - "title='{2}', " - "show_hidden='{3}', " - "use_dir_icons='{4}', " - "show_only_dirs='{5}', " - "select_desc='{6}', " - "change_desc='{7}')" - ).format( - self._default_path, - self._default_filename, - self._title, - self._show_hidden, - self._use_dir_icons, - self._show_only_dirs, - self._select_desc, - self._change_desc - ) - return str_ + properties = f"path='{self._default_path}'" + properties += f", filename='{self._default_filename}'" + properties += f", sandbox_path='{self._sandbox_path}'" + properties += f", title='{self._title.value}'" + properties += f", show_hidden={self._show_hidden}" + properties += f", select_desc='{self._select_desc}'" + properties += f", change_desc='{self._change_desc}'" + properties += f", select_default={self._select_default}" + properties += f", show_only_dirs={self._show_only_dirs}" + + if self._dir_icon: + properties += f", dir_icon='{self._dir_icon}'" + + if self._filter_pattern: + if isinstance(self._filter_pattern, str): + properties += f", filter_pattern='{self._filter_pattern}'" + else: + properties += f", filter_pattern={self._filter_pattern}" + + return f"{self.__class__.__name__}({properties})" def register_callback(self, callback: Callable[[Optional['FileChooser']], None]) -> None: """Register a callback function.""" diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index d35b22a..3938c18 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -62,8 +62,8 @@ def match_item(item: str, filter_pattern: Sequence[str]) -> bool: def get_dir_contents( path: str, show_hidden: bool = False, - prepend_icons: bool = False, show_only_dirs: bool = False, + dir_icon: Optional[str] = None, filter_pattern: Optional[Sequence[str]] = None, top_path: str = '') -> List[str]: """Get directory contents.""" @@ -86,15 +86,15 @@ def get_dir_contents( files.append(item) if has_parent(strip_parent_path(path, top_path)): dirs.insert(0, os.pardir) - if prepend_icons: - return prepend_dir_icons(sorted(dirs)) + sorted(files) + if dir_icon: + return prepend_dir_icons(sorted(dirs), dir_icon) + sorted(files) else: return sorted(dirs) + sorted(files) -def prepend_dir_icons(dir_list: Iterable[str]) -> List[str]: +def prepend_dir_icons(dir_list: Iterable[str], dir_icon: str) -> List[str]: """Prepend unicode folder icon to directory names.""" - return ['\U0001F4C1 ' + dirname for dirname in dir_list] + return [f'{dir_icon} ' + dirname for dirname in dir_list] def get_drive_letters() -> List[str]: From 350f52fb51611ecf5119b7f7e0da1c9bcf18858e Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Mon, 13 Sep 2021 23:59:39 +0200 Subject: [PATCH 20/39] Check first element length, not list length --- ipyfilechooser/filechooser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index a2de4a5..0a9486c 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -201,7 +201,7 @@ class FileChooser(VBox, ValueWidget): restricted_path = self._restrict_path(path) subpaths = get_subpaths(restricted_path) - if len(os.path.splitdrive(subpaths[-1])) == 2: + if len(os.path.splitdrive(subpaths[-1])[0]) == 2: # Add missing Windows drive letters drives = get_drive_letters() subpaths.extend(list(set(drives) - set(subpaths))) From 51bb1f5e84e599caf66204127e841b7dfb4520fe Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 00:20:33 +0200 Subject: [PATCH 21/39] Switch to using realpath --- ipyfilechooser/filechooser.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 0a9486c..7bf24ce 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -3,7 +3,7 @@ from typing import Optional, Sequence, Mapping, Callable from ipywidgets import Dropdown, Text, Select, Button, HTML from ipywidgets import Layout, GridBox, Box, HBox, VBox, ValueWidget from .utils import get_subpaths, get_dir_contents, match_item, strip_parent_path -from .utils import is_valid_filename, get_drive_letters, normalize_path, has_parent_path +from .utils import is_valid_filename, get_drive_letters, has_parent_path class SandboxPathError(Exception): @@ -52,14 +52,14 @@ class FileChooser(VBox, ValueWidget): **kwargs): """Initialize FileChooser object.""" # Check if path and sandbox_path align - if not has_parent_path(normalize_path(path), normalize_path(sandbox_path)): + if not has_parent_path(os.path.realpath(path), os.path.realpath(sandbox_path)): raise SandboxPathError(path, sandbox_path) # Verify the filename is valid if not is_valid_filename(filename): raise InvalidFileNameError(filename) - self._default_path = normalize_path(path) + self._default_path = os.path.realpath(path) self._default_filename = filename self._selected_path: Optional[str] = None self._selected_filename: Optional[str] = None @@ -70,7 +70,7 @@ class FileChooser(VBox, ValueWidget): self._dir_icon = dir_icon self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern - self._sandbox_path = normalize_path(sandbox_path) + self._sandbox_path = os.path.realpath(sandbox_path) self._callback: Optional[Callable] = None # Widgets @@ -387,7 +387,7 @@ class FileChooser(VBox, ValueWidget): def reset(self, path: Optional[str] = None, filename: Optional[str] = None) -> None: """Reset the form to the default path and filename.""" # Check if path and sandbox_path align - if path is not None and not has_parent_path(normalize_path(path), self._sandbox_path): + if path is not None and not has_parent_path(os.path.realpath(path), self._sandbox_path): raise SandboxPathError(path, self._sandbox_path) # Verify the filename is valid @@ -408,7 +408,7 @@ class FileChooser(VBox, ValueWidget): self._label.value = self._LBL_TEMPLATE.format(self._LBL_NOFILE, 'black') if path is not None: - self._default_path = normalize_path(path) + self._default_path = os.path.realpath(path) if filename is not None: self._default_filename = filename @@ -484,10 +484,10 @@ class FileChooser(VBox, ValueWidget): def default_path(self, path: str) -> None: """Set the default_path.""" # Check if path and sandbox_path align - if not has_parent_path(normalize_path(path), self._sandbox_path): + if not has_parent_path(os.path.realpath(path), self._sandbox_path): raise SandboxPathError(path, self._sandbox_path) - self._default_path = normalize_path(path) + self._default_path = os.path.realpath(path) self._set_form_values(self._default_path, self._filename.value) @property @@ -514,10 +514,10 @@ class FileChooser(VBox, ValueWidget): def sandbox_path(self, sandbox_path: str) -> None: """Set the sandbox_path.""" # Check if path and sandbox_path align - if not has_parent_path(self._default_path, normalize_path(sandbox_path)): + if not has_parent_path(self._default_path, os.path.realpath(sandbox_path)): raise SandboxPathError(self._default_path, sandbox_path) - self._sandbox_path = normalize_path(sandbox_path) + self._sandbox_path = os.path.realpath(sandbox_path) # Reset the dialog self.reset() From f6ce0cdb29f775036567f4aca770bdfd884d8b72 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 00:36:43 +0200 Subject: [PATCH 22/39] Back to normalize, but based on realpath --- ipyfilechooser/filechooser.py | 20 ++++++++++---------- ipyfilechooser/utils.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 7bf24ce..0a9486c 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -3,7 +3,7 @@ from typing import Optional, Sequence, Mapping, Callable from ipywidgets import Dropdown, Text, Select, Button, HTML from ipywidgets import Layout, GridBox, Box, HBox, VBox, ValueWidget from .utils import get_subpaths, get_dir_contents, match_item, strip_parent_path -from .utils import is_valid_filename, get_drive_letters, has_parent_path +from .utils import is_valid_filename, get_drive_letters, normalize_path, has_parent_path class SandboxPathError(Exception): @@ -52,14 +52,14 @@ class FileChooser(VBox, ValueWidget): **kwargs): """Initialize FileChooser object.""" # Check if path and sandbox_path align - if not has_parent_path(os.path.realpath(path), os.path.realpath(sandbox_path)): + if not has_parent_path(normalize_path(path), normalize_path(sandbox_path)): raise SandboxPathError(path, sandbox_path) # Verify the filename is valid if not is_valid_filename(filename): raise InvalidFileNameError(filename) - self._default_path = os.path.realpath(path) + self._default_path = normalize_path(path) self._default_filename = filename self._selected_path: Optional[str] = None self._selected_filename: Optional[str] = None @@ -70,7 +70,7 @@ class FileChooser(VBox, ValueWidget): self._dir_icon = dir_icon self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern - self._sandbox_path = os.path.realpath(sandbox_path) + self._sandbox_path = normalize_path(sandbox_path) self._callback: Optional[Callable] = None # Widgets @@ -387,7 +387,7 @@ class FileChooser(VBox, ValueWidget): def reset(self, path: Optional[str] = None, filename: Optional[str] = None) -> None: """Reset the form to the default path and filename.""" # Check if path and sandbox_path align - if path is not None and not has_parent_path(os.path.realpath(path), self._sandbox_path): + if path is not None and not has_parent_path(normalize_path(path), self._sandbox_path): raise SandboxPathError(path, self._sandbox_path) # Verify the filename is valid @@ -408,7 +408,7 @@ class FileChooser(VBox, ValueWidget): self._label.value = self._LBL_TEMPLATE.format(self._LBL_NOFILE, 'black') if path is not None: - self._default_path = os.path.realpath(path) + self._default_path = normalize_path(path) if filename is not None: self._default_filename = filename @@ -484,10 +484,10 @@ class FileChooser(VBox, ValueWidget): def default_path(self, path: str) -> None: """Set the default_path.""" # Check if path and sandbox_path align - if not has_parent_path(os.path.realpath(path), self._sandbox_path): + if not has_parent_path(normalize_path(path), self._sandbox_path): raise SandboxPathError(path, self._sandbox_path) - self._default_path = os.path.realpath(path) + self._default_path = normalize_path(path) self._set_form_values(self._default_path, self._filename.value) @property @@ -514,10 +514,10 @@ class FileChooser(VBox, ValueWidget): def sandbox_path(self, sandbox_path: str) -> None: """Set the sandbox_path.""" # Check if path and sandbox_path align - if not has_parent_path(self._default_path, os.path.realpath(sandbox_path)): + if not has_parent_path(self._default_path, normalize_path(sandbox_path)): raise SandboxPathError(self._default_path, sandbox_path) - self._sandbox_path = os.path.realpath(sandbox_path) + self._sandbox_path = normalize_path(sandbox_path) # Reset the dialog self.reset() diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 3938c18..814b19c 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -125,6 +125,6 @@ def normalize_path(path: str) -> str: normalized_path = '' if path: - normalized_path = os.path.normpath(os.path.normcase(path)) + normalized_path = os.path.realpath(path) return normalized_path From d12485873b2eb3fb27fa25b1b5a15f3073c38ee9 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 00:51:25 +0200 Subject: [PATCH 23/39] Fix get_drives function --- ipyfilechooser/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 814b19c..2b0e117 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -103,7 +103,7 @@ def get_drive_letters() -> List[str]: if sys.platform == 'win32': # Windows has drive letters - drives = [f'{d}:\\' for d in string.ascii_lowercase if os.path.exists(f'{d}:')] + drives = [os.path.realpath(f'{d}:\\') for d in string.ascii_uppercase if os.path.exists(f'{d}:')] return drives From d87a6792fbb50ac85612a2bb1dd319a25e824127 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 01:30:17 +0200 Subject: [PATCH 24/39] Small tweaks to utils --- ipyfilechooser/utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 2b0e117..9d50743 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -23,10 +23,12 @@ def get_subpaths(path: str) -> List[str]: def strip_parent_path(path: str, parent_path: str) -> str: """Remove a parent path from a path.""" - if path.startswith(parent_path): - return path[len(parent_path):] + stripped_path = path - return path + if path.startswith(parent_path): + stripped_path = path[len(parent_path):] + + return stripped_path def has_parent(path: str) -> bool: @@ -36,10 +38,10 @@ def has_parent(path: str) -> bool: def has_parent_path(path: str, parent_path: str) -> bool: """Verifies if path falls under parent_path.""" + check = True + if parent_path: check = os.path.commonpath([path, parent_path]) == parent_path - else: - check = True return check From 2619e456f120e843207703ef29a3f42507390388 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 11:56:38 +0200 Subject: [PATCH 25/39] Make sandbox_path optional using None as default. --- ipyfilechooser/filechooser.py | 26 ++++++++++++++------------ ipyfilechooser/utils.py | 31 +++++++++++++------------------ 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 0a9486c..05d8f7f 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -9,7 +9,7 @@ from .utils import is_valid_filename, get_drive_letters, normalize_path, has_par class SandboxPathError(Exception): """SandboxPathError class.""" - def __init__(self, path: str, sandbox_path: str, message: str = ''): + def __init__(self, path: str, sandbox_path: str, message: Optional[str] = None): self.path = path self.sandbox_path = sandbox_path self.message = message or f'{path} is located outside of {sandbox_path}' @@ -23,7 +23,7 @@ class InvalidFileNameError(Exception): if os.altsep: invalid_str.append(os.altsep) - def __init__(self, filename: str, message: str = ''): + def __init__(self, filename: str, message: Optional[str] = None): self.filename = filename self.message = message or f'{filename} cannot contain {self.invalid_str}' super().__init__(self.message) @@ -47,12 +47,12 @@ class FileChooser(VBox, ValueWidget): dir_icon: Optional[str] = '\U0001F4C1', show_only_dirs: bool = False, filter_pattern: Optional[Sequence[str]] = None, - sandbox_path: str = '', + sandbox_path: Optional[str] = None, layout: Layout = Layout(width='500px'), **kwargs): """Initialize FileChooser object.""" # Check if path and sandbox_path align - if not has_parent_path(normalize_path(path), normalize_path(sandbox_path)): + if sandbox_path and not has_parent_path(normalize_path(path), normalize_path(sandbox_path)): raise SandboxPathError(path, sandbox_path) # Verify the filename is valid @@ -70,7 +70,7 @@ class FileChooser(VBox, ValueWidget): self._dir_icon = dir_icon self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern - self._sandbox_path = normalize_path(sandbox_path) + self._sandbox_path = normalize_path(sandbox_path) if sandbox_path else None self._callback: Optional[Callable] = None # Widgets @@ -184,7 +184,7 @@ class FileChooser(VBox, ValueWidget): def _set_form_values(self, path: str, filename: str) -> None: """Set the form values.""" # Check if the path falls inside the configured sandbox path - if not has_parent_path(path, self._sandbox_path): + if self._sandbox_path and not has_parent_path(path, self._sandbox_path): raise SandboxPathError(path, self._sandbox_path) # Disable triggers to prevent selecting an entry in the Select @@ -387,7 +387,7 @@ class FileChooser(VBox, ValueWidget): def reset(self, path: Optional[str] = None, filename: Optional[str] = None) -> None: """Reset the form to the default path and filename.""" # Check if path and sandbox_path align - if path is not None and not has_parent_path(normalize_path(path), self._sandbox_path): + if path is not None and self._sandbox_path and not has_parent_path(normalize_path(path), self._sandbox_path): raise SandboxPathError(path, self._sandbox_path) # Verify the filename is valid @@ -484,7 +484,7 @@ class FileChooser(VBox, ValueWidget): def default_path(self, path: str) -> None: """Set the default_path.""" # Check if path and sandbox_path align - if not has_parent_path(normalize_path(path), self._sandbox_path): + if self._sandbox_path and not has_parent_path(normalize_path(path), self._sandbox_path): raise SandboxPathError(path, self._sandbox_path) self._default_path = normalize_path(path) @@ -506,7 +506,7 @@ class FileChooser(VBox, ValueWidget): self._set_form_values(self._expand_path(self._pathlist.value), self._default_filename) @property - def sandbox_path(self) -> str: + def sandbox_path(self) -> Optional[str]: """Get the sandbox_path.""" return self._sandbox_path @@ -514,10 +514,10 @@ class FileChooser(VBox, ValueWidget): def sandbox_path(self, sandbox_path: str) -> None: """Set the sandbox_path.""" # Check if path and sandbox_path align - if not has_parent_path(self._default_path, normalize_path(sandbox_path)): + if sandbox_path and not has_parent_path(self._default_path, normalize_path(sandbox_path)): raise SandboxPathError(self._default_path, sandbox_path) - self._sandbox_path = normalize_path(sandbox_path) + self._sandbox_path = normalize_path(sandbox_path) if sandbox_path else None # Reset the dialog self.reset() @@ -591,7 +591,6 @@ class FileChooser(VBox, ValueWidget): """Build string representation.""" properties = f"path='{self._default_path}'" properties += f", filename='{self._default_filename}'" - properties += f", sandbox_path='{self._sandbox_path}'" properties += f", title='{self._title.value}'" properties += f", show_hidden={self._show_hidden}" properties += f", select_desc='{self._select_desc}'" @@ -599,6 +598,9 @@ class FileChooser(VBox, ValueWidget): properties += f", select_default={self._select_default}" properties += f", show_only_dirs={self._show_only_dirs}" + if self._sandbox_path: + properties += f", sandbox_path='{self._sandbox_path}'" + if self._dir_icon: properties += f", dir_icon='{self._dir_icon}'" diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 9d50743..71f7b50 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -21,22 +21,12 @@ def get_subpaths(path: str) -> List[str]: return paths -def strip_parent_path(path: str, parent_path: str) -> str: - """Remove a parent path from a path.""" - stripped_path = path - - if path.startswith(parent_path): - stripped_path = path[len(parent_path):] - - return stripped_path - - def has_parent(path: str) -> bool: """Check if a path has a parent folder.""" return os.path.basename(path) != '' -def has_parent_path(path: str, parent_path: str) -> bool: +def has_parent_path(path: str, parent_path: Optional[str]) -> bool: """Verifies if path falls under parent_path.""" check = True @@ -46,6 +36,16 @@ def has_parent_path(path: str, parent_path: str) -> bool: return check +def strip_parent_path(path: str, parent_path: Optional[str]) -> str: + """Remove a parent path from a path.""" + stripped_path = path + + if parent_path and path.startswith(parent_path): + stripped_path = path[len(parent_path):] + + return stripped_path + + def match_item(item: str, filter_pattern: Sequence[str]) -> bool: """Check if a string matches one or more fnmatch patterns.""" if isinstance(filter_pattern, str): @@ -67,7 +67,7 @@ def get_dir_contents( show_only_dirs: bool = False, dir_icon: Optional[str] = None, filter_pattern: Optional[Sequence[str]] = None, - top_path: str = '') -> List[str]: + top_path: Optional[str] = None) -> List[str]: """Get directory contents.""" files = list() dirs = list() @@ -124,9 +124,4 @@ def is_valid_filename(filename: str) -> bool: def normalize_path(path: str) -> str: """Normalize a path string.""" - normalized_path = '' - - if path: - normalized_path = os.path.realpath(path) - - return normalized_path + return os.path.realpath(path) From 3a87d2e14f440385b32b7497b72a4f47af71a297 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 12:02:07 +0200 Subject: [PATCH 26/39] Fix sandbox value bug --- ipyfilechooser/filechooser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 05d8f7f..e6167a2 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -70,7 +70,7 @@ class FileChooser(VBox, ValueWidget): self._dir_icon = dir_icon self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern - self._sandbox_path = normalize_path(sandbox_path) if sandbox_path else None + self._sandbox_path = normalize_path(sandbox_path) if sandbox_path is not None else None self._callback: Optional[Callable] = None # Widgets @@ -517,7 +517,7 @@ class FileChooser(VBox, ValueWidget): if sandbox_path and not has_parent_path(self._default_path, normalize_path(sandbox_path)): raise SandboxPathError(self._default_path, sandbox_path) - self._sandbox_path = normalize_path(sandbox_path) if sandbox_path else None + self._sandbox_path = normalize_path(sandbox_path) if sandbox_path is not None else None # Reset the dialog self.reset() @@ -598,7 +598,7 @@ class FileChooser(VBox, ValueWidget): properties += f", select_default={self._select_default}" properties += f", show_only_dirs={self._show_only_dirs}" - if self._sandbox_path: + if self._sandbox_path is not None: properties += f", sandbox_path='{self._sandbox_path}'" if self._dir_icon: From f608c0f7d9fc3f9bfcd7e1c2afa125316bba7466 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 12:05:38 +0200 Subject: [PATCH 27/39] Remove default space from dir icon --- ipyfilechooser/filechooser.py | 2 +- ipyfilechooser/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index e6167a2..2ea6c50 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -44,7 +44,7 @@ class FileChooser(VBox, ValueWidget): change_desc: str = 'Change', show_hidden: bool = False, select_default: bool = False, - dir_icon: Optional[str] = '\U0001F4C1', + dir_icon: Optional[str] = '\U0001F4C1 ', show_only_dirs: bool = False, filter_pattern: Optional[Sequence[str]] = None, sandbox_path: Optional[str] = None, diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 71f7b50..c0aca98 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -96,7 +96,7 @@ def get_dir_contents( def prepend_dir_icons(dir_list: Iterable[str], dir_icon: str) -> List[str]: """Prepend unicode folder icon to directory names.""" - return [f'{dir_icon} ' + dirname for dirname in dir_list] + return [f'{dir_icon}' + dirname for dirname in dir_list] def get_drive_letters() -> List[str]: From 9171f5ab4ab22ed1f8c07cc0d29f86a8f1242399 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 12:18:24 +0200 Subject: [PATCH 28/39] Add dir_icon_append parameter --- ipyfilechooser/filechooser.py | 16 +++++++++++++++- ipyfilechooser/utils.py | 12 +++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 2ea6c50..e63ae5c 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -45,6 +45,7 @@ class FileChooser(VBox, ValueWidget): show_hidden: bool = False, select_default: bool = False, dir_icon: Optional[str] = '\U0001F4C1 ', + dir_icon_append: bool = False, show_only_dirs: bool = False, filter_pattern: Optional[Sequence[str]] = None, sandbox_path: Optional[str] = None, @@ -68,6 +69,7 @@ class FileChooser(VBox, ValueWidget): self._change_desc = change_desc self._select_default = select_default self._dir_icon = dir_icon + self._dir_icon_append = dir_icon_append self._show_only_dirs = show_only_dirs self._filter_pattern = filter_pattern self._sandbox_path = normalize_path(sandbox_path) if sandbox_path is not None else None @@ -215,7 +217,7 @@ class FileChooser(VBox, ValueWidget): path, show_hidden=self._show_hidden, show_only_dirs=self._show_only_dirs, - dir_icon='', + dir_icon=None, filter_pattern=self._filter_pattern, top_path=self._sandbox_path ) @@ -226,6 +228,7 @@ class FileChooser(VBox, ValueWidget): show_hidden=self._show_hidden, show_only_dirs=self._show_only_dirs, dir_icon=self._dir_icon, + dir_icon_append=self._dir_icon_append, filter_pattern=self._filter_pattern, top_path=self._sandbox_path ) @@ -445,6 +448,17 @@ class FileChooser(VBox, ValueWidget): self._dir_icon = dir_icon self.refresh() + @property + def dir_icon_append(self) -> bool: + """Get dir icon value.""" + return self._dir_icon_append + + @dir_icon_append.setter + def dir_icon_append(self, dir_icon_append: bool) -> None: + """Prepend or append the dir icon.""" + self._dir_icon_append = dir_icon_append + self.refresh() + @property def rows(self) -> int: """Get current number of rows.""" diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index c0aca98..6f6e424 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -66,6 +66,7 @@ def get_dir_contents( show_hidden: bool = False, show_only_dirs: bool = False, dir_icon: Optional[str] = None, + dir_icon_append: bool = False, filter_pattern: Optional[Sequence[str]] = None, top_path: Optional[str] = None) -> List[str]: """Get directory contents.""" @@ -89,14 +90,19 @@ def get_dir_contents( if has_parent(strip_parent_path(path, top_path)): dirs.insert(0, os.pardir) if dir_icon: - return prepend_dir_icons(sorted(dirs), dir_icon) + sorted(files) + return prepend_dir_icons(sorted(dirs), dir_icon, dir_icon_append) + sorted(files) else: return sorted(dirs) + sorted(files) -def prepend_dir_icons(dir_list: Iterable[str], dir_icon: str) -> List[str]: +def prepend_dir_icons(dir_list: Iterable[str], dir_icon: str, dir_icon_append: bool = False) -> List[str]: """Prepend unicode folder icon to directory names.""" - return [f'{dir_icon}' + dirname for dirname in dir_list] + if dir_icon_append: + str_ = [dirname + f'{dir_icon}' for dirname in dir_list] + else: + str_ = [f'{dir_icon}' + dirname for dirname in dir_list] + + return str_ def get_drive_letters() -> List[str]: From 315a9ac2010cd5a58b41c2a99252788c58c8803a Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 12:22:55 +0200 Subject: [PATCH 29/39] Add dir_icon_append to __repr__ --- ipyfilechooser/filechooser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index e63ae5c..c534420 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -618,6 +618,9 @@ class FileChooser(VBox, ValueWidget): if self._dir_icon: properties += f", dir_icon='{self._dir_icon}'" + if self._dir_icon_append: + properties += f", dir_icon_append={self._dir_icon_append}" + if self._filter_pattern: if isinstance(self._filter_pattern, str): properties += f", filter_pattern='{self._filter_pattern}'" From 7cb1555192ec754a9deaf509ccc6a741c832e279 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 13:12:43 +0200 Subject: [PATCH 30/39] properly handle a c:\ sandbox_path --- ipyfilechooser/filechooser.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index c534420..cc6d0df 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -203,7 +203,7 @@ class FileChooser(VBox, ValueWidget): restricted_path = self._restrict_path(path) subpaths = get_subpaths(restricted_path) - if len(os.path.splitdrive(subpaths[-1])[0]) == 2: + if os.path.splitdrive(subpaths[-1])[0]: # Add missing Windows drive letters drives = get_drive_letters() subpaths.extend(list(set(drives) - set(subpaths))) @@ -383,7 +383,11 @@ class FileChooser(VBox, ValueWidget): if self._sandbox_path == path: path = os.sep elif self._sandbox_path: - path = strip_parent_path(path, self._sandbox_path) + if os.path.splitdrive(self._sandbox_path)[0] and len(self._sandbox_path) == 3: + # If the value is 'c:\\', strip 'c:' so we retain the leading os.sep char + path = strip_parent_path(path, os.path.splitdrive(self._sandbox_path)[0]) + else: + path = strip_parent_path(path, self._sandbox_path) return path From e8290e0ff8277fab7b95c37d824266bbe743e0fd Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 13:18:58 +0200 Subject: [PATCH 31/39] Update __repr__ --- ipyfilechooser/filechooser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index cc6d0df..638d4e5 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -615,6 +615,7 @@ class FileChooser(VBox, ValueWidget): properties += f", change_desc='{self._change_desc}'" properties += f", select_default={self._select_default}" properties += f", show_only_dirs={self._show_only_dirs}" + properties += f", dir_icon_append={self._dir_icon_append}" if self._sandbox_path is not None: properties += f", sandbox_path='{self._sandbox_path}'" @@ -622,9 +623,6 @@ class FileChooser(VBox, ValueWidget): if self._dir_icon: properties += f", dir_icon='{self._dir_icon}'" - if self._dir_icon_append: - properties += f", dir_icon_append={self._dir_icon_append}" - if self._filter_pattern: if isinstance(self._filter_pattern, str): properties += f", filter_pattern='{self._filter_pattern}'" From f841f7b47be79c7c5dcabcda0e41b249bf2e2e10 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Tue, 14 Sep 2021 23:54:10 +0200 Subject: [PATCH 32/39] Update exception message --- ipyfilechooser/filechooser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 638d4e5..55b03a8 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -12,7 +12,7 @@ class SandboxPathError(Exception): def __init__(self, path: str, sandbox_path: str, message: Optional[str] = None): self.path = path self.sandbox_path = sandbox_path - self.message = message or f'{path} is located outside of {sandbox_path}' + self.message = message or f'{path} is located outside of {sandbox_path} sandbox' super().__init__(self.message) From b82dec0190836b53cdce20bb13ddc467afdc0e02 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Wed, 15 Sep 2021 00:39:05 +0200 Subject: [PATCH 33/39] Move exception classes to separate file --- ipyfilechooser/errors.py | 35 ++++++++++++++++++++++++++++++++ ipyfilechooser/filechooser.py | 38 +++++++++-------------------------- ipyfilechooser/utils.py | 8 +++++++- 3 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 ipyfilechooser/errors.py diff --git a/ipyfilechooser/errors.py b/ipyfilechooser/errors.py new file mode 100644 index 0000000..fad7e58 --- /dev/null +++ b/ipyfilechooser/errors.py @@ -0,0 +1,35 @@ +"""Exception classes.""" +import os +from typing import Optional + + +class ParentPathError(Exception): + """ParentPathError class.""" + + def __init__(self, path: str, parent_path: str, message: Optional[str] = None): + self.path = path + self.sandbox_path = parent_path + self.message = message or f'{path} is not a part of {parent_path}' + super().__init__(self.message) + + +class InvalidPathError(Exception): + """InvalidPathError class.""" + + def __init__(self, path: str, message: Optional[str] = None): + self.path = path + self.message = message or f'{path} does not exist' + super().__init__(self.message) + + +class InvalidFileNameError(Exception): + """InvalidFileNameError class.""" + invalid_str = [os.sep, os.pardir] + + if os.altsep: + invalid_str.append(os.altsep) + + def __init__(self, filename: str, message: Optional[str] = None): + self.filename = filename + self.message = message or f'{filename} cannot contain {self.invalid_str}' + super().__init__(self.message) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 55b03a8..5d8ad45 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -2,33 +2,11 @@ import os from typing import Optional, Sequence, Mapping, Callable from ipywidgets import Dropdown, Text, Select, Button, HTML from ipywidgets import Layout, GridBox, Box, HBox, VBox, ValueWidget +from .errors import ParentPathError, InvalidFileNameError from .utils import get_subpaths, get_dir_contents, match_item, strip_parent_path from .utils import is_valid_filename, get_drive_letters, normalize_path, has_parent_path -class SandboxPathError(Exception): - """SandboxPathError class.""" - - def __init__(self, path: str, sandbox_path: str, message: Optional[str] = None): - self.path = path - self.sandbox_path = sandbox_path - self.message = message or f'{path} is located outside of {sandbox_path} sandbox' - super().__init__(self.message) - - -class InvalidFileNameError(Exception): - """InvalidFileNameError class.""" - invalid_str = [os.sep, os.pardir] - - if os.altsep: - invalid_str.append(os.altsep) - - def __init__(self, filename: str, message: Optional[str] = None): - self.filename = filename - self.message = message or f'{filename} cannot contain {self.invalid_str}' - super().__init__(self.message) - - class FileChooser(VBox, ValueWidget): """FileChooser class.""" @@ -54,7 +32,7 @@ class FileChooser(VBox, ValueWidget): """Initialize FileChooser object.""" # Check if path and sandbox_path align if sandbox_path and not has_parent_path(normalize_path(path), normalize_path(sandbox_path)): - raise SandboxPathError(path, sandbox_path) + raise ParentPathError(path, sandbox_path) # Verify the filename is valid if not is_valid_filename(filename): @@ -187,7 +165,7 @@ class FileChooser(VBox, ValueWidget): """Set the form values.""" # Check if the path falls inside the configured sandbox path if self._sandbox_path and not has_parent_path(path, self._sandbox_path): - raise SandboxPathError(path, self._sandbox_path) + raise ParentPathError(path, self._sandbox_path) # Disable triggers to prevent selecting an entry in the Select # box from automatically triggering a new event. @@ -380,7 +358,9 @@ class FileChooser(VBox, ValueWidget): def _restrict_path(self, path) -> str: """Calculate the sandboxed path using the sandbox path.""" - if self._sandbox_path == path: + if self._sandbox_path == os.sep: + pass + elif self._sandbox_path == path: path = os.sep elif self._sandbox_path: if os.path.splitdrive(self._sandbox_path)[0] and len(self._sandbox_path) == 3: @@ -395,7 +375,7 @@ class FileChooser(VBox, ValueWidget): """Reset the form to the default path and filename.""" # Check if path and sandbox_path align if path is not None and self._sandbox_path and not has_parent_path(normalize_path(path), self._sandbox_path): - raise SandboxPathError(path, self._sandbox_path) + raise ParentPathError(path, self._sandbox_path) # Verify the filename is valid if filename is not None and not is_valid_filename(filename): @@ -503,7 +483,7 @@ class FileChooser(VBox, ValueWidget): """Set the default_path.""" # Check if path and sandbox_path align if self._sandbox_path and not has_parent_path(normalize_path(path), self._sandbox_path): - raise SandboxPathError(path, self._sandbox_path) + raise ParentPathError(path, self._sandbox_path) self._default_path = normalize_path(path) self._set_form_values(self._default_path, self._filename.value) @@ -533,7 +513,7 @@ class FileChooser(VBox, ValueWidget): """Set the sandbox_path.""" # Check if path and sandbox_path align if sandbox_path and not has_parent_path(self._default_path, normalize_path(sandbox_path)): - raise SandboxPathError(self._default_path, sandbox_path) + raise ParentPathError(self._default_path, sandbox_path) self._sandbox_path = normalize_path(sandbox_path) if sandbox_path is not None else None diff --git a/ipyfilechooser/utils.py b/ipyfilechooser/utils.py index 6f6e424..d14338d 100644 --- a/ipyfilechooser/utils.py +++ b/ipyfilechooser/utils.py @@ -4,6 +4,7 @@ import os import string import sys from typing import List, Sequence, Iterable, Optional +from .errors import InvalidPathError def get_subpaths(path: str) -> List[str]: @@ -130,4 +131,9 @@ def is_valid_filename(filename: str) -> bool: def normalize_path(path: str) -> str: """Normalize a path string.""" - return os.path.realpath(path) + normalized_path = os.path.realpath(path) + + if not os.path.isdir(normalized_path): + raise InvalidPathError(path) + + return normalized_path From ea8a84afbb5d00b82c615b0a564ecfa34f379b80 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Wed, 15 Sep 2021 01:46:13 +0200 Subject: [PATCH 34/39] Version bump and doc update --- README.md | 23 ++++++++++++++++++++--- ipyfilechooser/__init__.py | 2 +- ipyfilechooser/filechooser.py | 2 +- setup.py | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 86b20c3..face6ae 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,15 @@ fc.reset() # Shorthand reset fc.reset(path='/Users/crahan/', filename='output.txt') +# Restrict navigation to /Users +fc.sandbox_path = '/Users' + # Change hidden files fc.show_hidden = True -# Show or hide folder icons -fc.use_dir_icons = True +# Customize dir icon +fc.dir_icon = '/' +fc.dir_icon_append = True # Switch to folder-only mode fc.show_only_dirs = True @@ -63,7 +67,8 @@ fc.reset() fc.refresh() fc.register_callback(function_name) fc.show_hidden -fc.use_dir_icons +fc.dir_icon +fc.dir_icon_append fc.show_only_dirs fc.rows fc.title @@ -71,6 +76,7 @@ fc.filter_pattern fc.default fc.default_path fc.default_filename +fc.sandbox_path fc.value fc.selected fc.selected_path @@ -102,6 +108,17 @@ fc.selected_filename ## Release notes +### 0.6.0 + +- The ability to restrict file browsing to a `sandbox_path` folder has finally been added! +- Filenames can not contain path separator characters or parent folder strings (i.e., '..') +- `use_dir_icons` has been replaced with `dir_icon` which allows for customizing the folder icon +- `dir_icon_append` can now be used to put the folder icon before or after the folder name +- Better error handling with `ParentPathError`, `InvalidPathError`, and `InvalidFileNameError` +- Better and more consistent handling of Windows drive letters and paths +- Select button is now properly activated again when applying a selection or resetting the filechooser + + ### 0.5.0 - Widget width is now configurable using the `layout` property and a `Layout` object diff --git a/ipyfilechooser/__init__.py b/ipyfilechooser/__init__.py index d56ccea..78337e0 100755 --- a/ipyfilechooser/__init__.py +++ b/ipyfilechooser/__init__.py @@ -1,3 +1,3 @@ from .filechooser import FileChooser -__version__ = '0.5.0' +__version__ = '0.6.0' diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 5d8ad45..a307308 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -11,7 +11,7 @@ class FileChooser(VBox, ValueWidget): """FileChooser class.""" _LBL_TEMPLATE = '{0}' - _LBL_NOFILE = 'No file selected' + _LBL_NOFILE = 'No selection' def __init__( self, diff --git a/setup.py b/setup.py index 112c92d..9852372 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def read(fname: str) -> str: setup( name='ipyfilechooser', - version='0.5.0', + version='0.6.0', author='Thomas Bouve (@crahan)', author_email='crahan@n00.be', description=( From 5001e0c7b281b4ead56db1d74ecd3c71e7129a14 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Wed, 15 Sep 2021 23:20:46 +0200 Subject: [PATCH 35/39] Handle folder permission issues --- README.md | 4 +- ipyfilechooser/filechooser.py | 181 ++++++++++++++++++---------------- 2 files changed, 96 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index face6ae..4bf3c33 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,8 @@ fc.selected_filename - `dir_icon_append` can now be used to put the folder icon before or after the folder name - Better error handling with `ParentPathError`, `InvalidPathError`, and `InvalidFileNameError` - Better and more consistent handling of Windows drive letters and paths -- Select button is now properly activated again when applying a selection or resetting the filechooser - +- Fix bug where resetting the filechooser would not reenable the select/change button +- Properly handle folder permission errors by raising a warning indicating the folder can not be opened ### 0.5.0 diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index a307308..0053b7e 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -1,4 +1,5 @@ import os +import warnings from typing import Optional, Sequence, Mapping, Callable from ipywidgets import Dropdown, Text, Select, Button, HTML from ipywidgets import Layout, GridBox, Box, HBox, VBox, ValueWidget @@ -167,106 +168,112 @@ class FileChooser(VBox, ValueWidget): if self._sandbox_path and not has_parent_path(path, self._sandbox_path): raise ParentPathError(path, self._sandbox_path) - # Disable triggers to prevent selecting an entry in the Select - # box from automatically triggering a new event. - self._pathlist.unobserve(self._on_pathlist_select, names='value') - self._dircontent.unobserve(self._on_dircontent_select, names='value') - self._filename.unobserve(self._on_filename_change, names='value') + try: + # Fail early if the folder can not be read + _ = os.listdir(path) - # In folder only mode zero out the filename - if self._show_only_dirs: - filename = '' + # Disable triggers to prevent selecting an entry in the Select + # box from automatically triggering a new event. + self._pathlist.unobserve(self._on_pathlist_select, names='value') + self._dircontent.unobserve(self._on_dircontent_select, names='value') + self._filename.unobserve(self._on_filename_change, names='value') - # Set form values - restricted_path = self._restrict_path(path) - subpaths = get_subpaths(restricted_path) + # In folder only mode zero out the filename + if self._show_only_dirs: + filename = '' - if os.path.splitdrive(subpaths[-1])[0]: - # Add missing Windows drive letters - drives = get_drive_letters() - subpaths.extend(list(set(drives) - set(subpaths))) + # Set form values + restricted_path = self._restrict_path(path) + subpaths = get_subpaths(restricted_path) - self._pathlist.options = subpaths - self._pathlist.value = restricted_path - self._filename.value = filename + if os.path.splitdrive(subpaths[-1])[0]: + # Add missing Windows drive letters + drives = get_drive_letters() + subpaths.extend(list(set(drives) - set(subpaths))) - # file/folder real names - dircontent_real_names = get_dir_contents( - path, - show_hidden=self._show_hidden, - show_only_dirs=self._show_only_dirs, - dir_icon=None, - filter_pattern=self._filter_pattern, - top_path=self._sandbox_path - ) + self._pathlist.options = subpaths + self._pathlist.value = restricted_path + self._filename.value = filename - # file/folder display names - dircontent_display_names = get_dir_contents( - path, - show_hidden=self._show_hidden, - show_only_dirs=self._show_only_dirs, - dir_icon=self._dir_icon, - dir_icon_append=self._dir_icon_append, - filter_pattern=self._filter_pattern, - top_path=self._sandbox_path - ) - - # Dict to map real names to display names - self._map_name_to_disp = { - real_name: disp_name - for real_name, disp_name in zip( - dircontent_real_names, - dircontent_display_names + # file/folder real names + dircontent_real_names = get_dir_contents( + path, + show_hidden=self._show_hidden, + show_only_dirs=self._show_only_dirs, + dir_icon=None, + filter_pattern=self._filter_pattern, + top_path=self._sandbox_path ) - } - # Dict to map display names to real names - self._map_disp_to_name = { - disp_name: real_name - for real_name, disp_name in self._map_name_to_disp.items() - } + # file/folder display names + dircontent_display_names = get_dir_contents( + path, + show_hidden=self._show_hidden, + show_only_dirs=self._show_only_dirs, + dir_icon=self._dir_icon, + dir_icon_append=self._dir_icon_append, + filter_pattern=self._filter_pattern, + top_path=self._sandbox_path + ) - # Set _dircontent form value to display names - self._dircontent.options = dircontent_display_names + # Dict to map real names to display names + self._map_name_to_disp = { + real_name: disp_name + for real_name, disp_name in zip( + dircontent_real_names, + dircontent_display_names + ) + } - # If the value in the filename Text box equals a value in the - # Select box and the entry is a file then select the entry. - if ((filename in dircontent_real_names) and os.path.isfile(os.path.join(path, filename))): - self._dircontent.value = self._map_name_to_disp[filename] - else: - self._dircontent.value = None + # Dict to map display names to real names + self._map_disp_to_name = { + disp_name: real_name + for real_name, disp_name in self._map_name_to_disp.items() + } - # Reenable triggers again - self._pathlist.observe(self._on_pathlist_select, names='value') - self._dircontent.observe(self._on_dircontent_select, names='value') - self._filename.observe(self._on_filename_change, names='value') + # Set _dircontent form value to display names + self._dircontent.options = dircontent_display_names - # Update the state of the select button - if self._gb.layout.display is None: - # Disable the select button if path and filename - # - equal an existing folder in the current view - # - contains an invalid character sequence - # - equal the already selected values - # - don't match the provided filter pattern(s) - check1 = filename in dircontent_real_names - check2 = os.path.isdir(os.path.join(path, filename)) - check3 = not is_valid_filename(filename) - check4 = False - check5 = False - - # Only check selected if selected is set - if ((self._selected_path is not None) and (self._selected_filename is not None)): - selected = os.path.join(self._selected_path, self._selected_filename) - check4 = os.path.join(path, filename) == selected - - # Ensure only allowed extensions are used - if self._filter_pattern: - check5 = not match_item(filename, self._filter_pattern) - - if (check1 and check2) or check3 or check4 or check5: - self._select.disabled = True + # If the value in the filename Text box equals a value in the + # Select box and the entry is a file then select the entry. + if ((filename in dircontent_real_names) and os.path.isfile(os.path.join(path, filename))): + self._dircontent.value = self._map_name_to_disp[filename] else: - self._select.disabled = False + self._dircontent.value = None + + # Reenable triggers again + self._pathlist.observe(self._on_pathlist_select, names='value') + self._dircontent.observe(self._on_dircontent_select, names='value') + self._filename.observe(self._on_filename_change, names='value') + + # Update the state of the select button + if self._gb.layout.display is None: + # Disable the select button if path and filename + # - equal an existing folder in the current view + # - contains an invalid character sequence + # - equal the already selected values + # - don't match the provided filter pattern(s) + check1 = filename in dircontent_real_names + check2 = os.path.isdir(os.path.join(path, filename)) + check3 = not is_valid_filename(filename) + check4 = False + check5 = False + + # Only check selected if selected is set + if ((self._selected_path is not None) and (self._selected_filename is not None)): + selected = os.path.join(self._selected_path, self._selected_filename) + check4 = os.path.join(path, filename) == selected + + # Ensure only allowed extensions are used + if self._filter_pattern: + check5 = not match_item(filename, self._filter_pattern) + + if (check1 and check2) or check3 or check4 or check5: + self._select.disabled = True + else: + self._select.disabled = False + except PermissionError: + warnings.warn(f'Permission denied for {path}', RuntimeWarning) def _on_pathlist_select(self, change: Mapping[str, str]) -> None: """Handle selecting a path entry.""" From cf885c4f176b06dd3580e5e0c6064f6df7055246 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Wed, 15 Sep 2021 23:34:07 +0200 Subject: [PATCH 36/39] Deselect value --- ipyfilechooser/filechooser.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index 0053b7e..d07ef4f 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -273,6 +273,10 @@ class FileChooser(VBox, ValueWidget): else: self._select.disabled = False except PermissionError: + # Deselect the unreadable folder and generate a warning + self._dircontent.unobserve(self._on_dircontent_select, names='value') + self._dircontent.value = None + self._dircontent.observe(self._on_dircontent_select, names='value') warnings.warn(f'Permission denied for {path}', RuntimeWarning) def _on_pathlist_select(self, change: Mapping[str, str]) -> None: From 54a59818d86ae44809f173fd75b00feebc8e38c5 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Wed, 15 Sep 2021 23:40:38 +0200 Subject: [PATCH 37/39] Cleaner observer handling --- ipyfilechooser/filechooser.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/ipyfilechooser/filechooser.py b/ipyfilechooser/filechooser.py index d07ef4f..12f6479 100644 --- a/ipyfilechooser/filechooser.py +++ b/ipyfilechooser/filechooser.py @@ -168,16 +168,16 @@ class FileChooser(VBox, ValueWidget): if self._sandbox_path and not has_parent_path(path, self._sandbox_path): raise ParentPathError(path, self._sandbox_path) + # Disable triggers to prevent selecting an entry in the Select + # box from automatically triggering a new event. + self._pathlist.unobserve(self._on_pathlist_select, names='value') + self._dircontent.unobserve(self._on_dircontent_select, names='value') + self._filename.unobserve(self._on_filename_change, names='value') + try: # Fail early if the folder can not be read _ = os.listdir(path) - # Disable triggers to prevent selecting an entry in the Select - # box from automatically triggering a new event. - self._pathlist.unobserve(self._on_pathlist_select, names='value') - self._dircontent.unobserve(self._on_dircontent_select, names='value') - self._filename.unobserve(self._on_filename_change, names='value') - # In folder only mode zero out the filename if self._show_only_dirs: filename = '' @@ -241,11 +241,6 @@ class FileChooser(VBox, ValueWidget): else: self._dircontent.value = None - # Reenable triggers again - self._pathlist.observe(self._on_pathlist_select, names='value') - self._dircontent.observe(self._on_dircontent_select, names='value') - self._filename.observe(self._on_filename_change, names='value') - # Update the state of the select button if self._gb.layout.display is None: # Disable the select button if path and filename @@ -274,11 +269,14 @@ class FileChooser(VBox, ValueWidget): self._select.disabled = False except PermissionError: # Deselect the unreadable folder and generate a warning - self._dircontent.unobserve(self._on_dircontent_select, names='value') self._dircontent.value = None - self._dircontent.observe(self._on_dircontent_select, names='value') warnings.warn(f'Permission denied for {path}', RuntimeWarning) + # Reenable triggers + self._pathlist.observe(self._on_pathlist_select, names='value') + self._dircontent.observe(self._on_dircontent_select, names='value') + self._filename.observe(self._on_filename_change, names='value') + def _on_pathlist_select(self, change: Mapping[str, str]) -> None: """Handle selecting a path entry.""" self._set_form_values(self._expand_path(change['new']), self._filename.value) From 2ef6cc44f8ef972582c8668aff1c29df452d9d4a Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Wed, 15 Sep 2021 23:53:49 +0200 Subject: [PATCH 38/39] Add sandbox feature screenshot --- README.md | 5 ++++- screenshots/FileChooser_screenshot_7.png | Bin 0 -> 20059 bytes 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 screenshots/FileChooser_screenshot_7.png diff --git a/README.md b/README.md index 4bf3c33..33c7724 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,9 @@ fc.selected_filename ![Screenshot 6](https://github.com/crahan/ipyfilechooser/raw/master/screenshots/FileChooser_screenshot_6.png) +### Restrict navigation + +![Screenshot 7](https://github.com/crahan/ipyfilechooser/raw/master/screenshots/FileChooser_screenshot_7.png) ## Release notes @@ -117,7 +120,7 @@ fc.selected_filename - Better error handling with `ParentPathError`, `InvalidPathError`, and `InvalidFileNameError` - Better and more consistent handling of Windows drive letters and paths - Fix bug where resetting the filechooser would not reenable the select/change button -- Properly handle folder permission errors by raising a warning indicating the folder can not be opened +- Properly handle folder permission errors by raising a warning ### 0.5.0 diff --git a/screenshots/FileChooser_screenshot_7.png b/screenshots/FileChooser_screenshot_7.png new file mode 100644 index 0000000000000000000000000000000000000000..d2e034993f10a4b243b5a0048637cb5c3ad0c1dc GIT binary patch literal 20059 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 Tb7Ct89Wv(W>gTe~DWM4fJR}$c literal 0 HcmV?d00001 From bffb81b819060b8c720f5f91c4aa005a116ad778 Mon Sep 17 00:00:00 2001 From: Thomas Bouve Date: Thu, 16 Sep 2021 00:04:27 +0200 Subject: [PATCH 39/39] Update sample notebook --- ipyfilechooser_examples.ipynb | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/ipyfilechooser_examples.ipynb b/ipyfilechooser_examples.ipynb index e432164..02128d2 100644 --- a/ipyfilechooser_examples.ipynb +++ b/ipyfilechooser_examples.ipynb @@ -22,7 +22,6 @@ "# Title: FileChooser example\n", "# Show hidden files: no\n", "# Use the default path and filename as selection: yes\n", - "# Use folder icons: yes\n", "# Only show folders: no\n", "fdialog = FileChooser(\n", " os.getcwd(),\n", @@ -30,7 +29,6 @@ " title='FileChooser example',\n", " show_hidden=False,\n", " select_default=True,\n", - " use_dir_icons=True,\n", " show_only_dirs=False\n", ")\n", "\n", @@ -80,7 +78,28 @@ "# Show hidden files, change rows to 10, and hide folder icons\n", "fdialog.show_hidden = True\n", "fdialog.rows = 10\n", - "fdialog.use_dir_icons = False" + "fdialog.dir_icon = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Change folder icon to `os.sep` and append it to the folder name\n", + "fdialog.dir_icon = os.sep\n", + "fdialog.dir_icon_append = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Restrict navigation\n", + "fdialog.sandbox_path = '/Users/jdoe'" ] }, {