diff --git a/README.md b/README.md
index 86b20c3..33c7724 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
@@ -99,9 +105,23 @@ 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
+### 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
+- Fix bug where resetting the filechooser would not reenable the select/change button
+- Properly handle folder permission errors by raising a warning
+
### 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/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 078b96d..12f6479 100644
--- a/ipyfilechooser/filechooser.py
+++ b/ipyfilechooser/filechooser.py
@@ -1,15 +1,18 @@
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
-from .utils import get_subpaths, get_dir_contents, match_item
+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 FileChooser(VBox, ValueWidget):
"""FileChooser class."""
_LBL_TEMPLATE = '{0}'
- _LBL_NOFILE = 'No file selected'
+ _LBL_NOFILE = 'No selection'
def __init__(
self,
@@ -20,23 +23,35 @@ 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 ',
+ dir_icon_append: bool = False,
show_only_dirs: bool = False,
filter_pattern: Optional[Sequence[str]] = None,
+ sandbox_path: Optional[str] = None,
layout: Layout = Layout(width='500px'),
**kwargs):
"""Initialize FileChooser object."""
- self._default_path = os.path.normpath(path)
+ # Check if path and sandbox_path align
+ if sandbox_path and not has_parent_path(normalize_path(path), normalize_path(sandbox_path)):
+ raise ParentPathError(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 = 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
self._select_default = select_default
- self._use_dir_icons = use_dir_icons
+ 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
self._callback: Optional[Callable] = None
# Widgets
@@ -149,115 +164,143 @@ 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 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')
- # In folder only mode zero out the filename
- if self._show_only_dirs:
- filename = ''
+ try:
+ # Fail early if the folder can not be read
+ _ = os.listdir(path)
- # Set form values
- self._pathlist.options = get_subpaths(path)
- self._pathlist.value = path
- self._filename.value = filename
+ # In folder only mode zero out the filename
+ if self._show_only_dirs:
+ filename = ''
- # file/folder real names
- dircontent_real_names = get_dir_contents(
- path,
- show_hidden=self._show_hidden,
- prepend_icons=False,
- show_only_dirs=self._show_only_dirs,
- filter_pattern=self._filter_pattern
- )
+ # Set form values
+ restricted_path = self._restrict_path(path)
+ subpaths = get_subpaths(restricted_path)
- # file/folder display names
- 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,
- filter_pattern=self._filter_pattern
- )
+ if os.path.splitdrive(subpaths[-1])[0]:
+ # Add missing Windows drive letters
+ drives = get_drive_letters()
+ subpaths.extend(list(set(drives) - set(subpaths)))
- # 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
+ self._pathlist.options = subpaths
+ self._pathlist.value = restricted_path
+ self._filename.value = filename
+
+ # 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:
+ # 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()
+ }
+
+ # Set _dircontent form value to display names
+ self._dircontent.options = 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
+
+ # 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:
+ # Deselect the unreadable folder and generate a warning
self._dircontent.value = None
+ warnings.warn(f'Permission denied for {path}', RuntimeWarning)
- # Reenable triggers again
+ # 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')
- # 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
- # - 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 = False
- check4 = 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
-
- # Ensure only allowed extensions are used
- if self._filter_pattern:
- check4 = not match_item(filename, self._filter_pattern)
-
- if (check1 and check2) or check3 or check4:
- self._select.disabled = True
- else:
- self._select.disabled = False
-
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._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._pathlist.value, self._map_disp_to_name[change['new']]))
+ new_path = os.path.realpath(os.path.join(
+ self._expand_path(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._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._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."""
@@ -294,7 +337,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._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)):
@@ -302,11 +345,12 @@ 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(selected, 'orange')
+ self._label.value = self._LBL_TEMPLATE.format(self._restrict_path(selected), 'orange')
else:
- self._label.value = self._LBL_TEMPLATE.format(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."""
@@ -314,28 +358,58 @@ class FileChooser(VBox, ValueWidget):
self._cancel.layout.display = 'none'
self._select.disabled = False
+ def _expand_path(self, path) -> str:
+ """Calculate the full path using the sandbox path."""
+ if self._sandbox_path:
+ path = os.path.join(self._sandbox_path, path.lstrip(os.sep))
+
+ return path
+
+ def _restrict_path(self, path) -> str:
+ """Calculate the sandboxed path using the sandbox 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:
+ # 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
+
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 self._sandbox_path and not has_parent_path(normalize_path(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):
+ 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:
- self._default_path = os.path.normpath(path)
+ self._default_path = normalize_path(path)
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:
@@ -343,7 +417,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._expand_path(self._pathlist.value), self._filename.value)
@property
def show_hidden(self) -> bool:
@@ -357,14 +431,25 @@ 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
+ 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
@@ -405,7 +490,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 self._sandbox_path and not has_parent_path(normalize_path(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)
@property
@@ -416,8 +505,29 @@ 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._pathlist.value, self._default_filename)
+ self._set_form_values(self._expand_path(self._pathlist.value), self._default_filename)
+
+ @property
+ def sandbox_path(self) -> Optional[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 sandbox_path and not has_parent_path(self._default_path, normalize_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
+
+ # Reset the dialog
+ self.reset()
@property
def show_only_dirs(self) -> bool:
@@ -486,27 +596,29 @@ 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", 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}"
+ properties += f", dir_icon_append={self._dir_icon_append}"
+
+ if self._sandbox_path is not None:
+ properties += f", sandbox_path='{self._sandbox_path}'"
+
+ 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 523e982..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]:
@@ -18,13 +19,6 @@ def get_subpaths(path: str) -> List[str]:
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
return paths
@@ -33,6 +27,26 @@ def has_parent(path: str) -> bool:
return os.path.basename(path) != ''
+def has_parent_path(path: str, parent_path: Optional[str]) -> bool:
+ """Verifies if path falls under parent_path."""
+ check = True
+
+ if parent_path:
+ check = os.path.commonpath([path, parent_path]) == parent_path
+
+ 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):
@@ -51,9 +65,11 @@ 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,
- filter_pattern: Optional[Sequence[str]] = None) -> List[str]:
+ 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."""
files = list()
dirs = list()
@@ -72,27 +88,52 @@ def get_dir_contents(
files.append(item)
else:
files.append(item)
- if has_parent(path):
- dirs.insert(0, '..')
- if prepend_icons:
- return prepend_dir_icons(sorted(dirs)) + sorted(files)
+ 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, dir_icon_append) + 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, dir_icon_append: bool = False) -> List[str]:
"""Prepend unicode folder icon to directory names."""
- return ['\U0001F4C1 ' + 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]:
- """Get drive letters."""
+ """Get all drive letters minus the drive used in path."""
+ drives: List[str] = []
+
if sys.platform == 'win32':
# Windows has drive letters
- return [
- '%s:\\' % d for d in string.ascii_uppercase
- if os.path.exists('%s:' % d)
- ]
- else:
- # Unix does not have drive letters
- return []
+ drives = [os.path.realpath(f'{d}:\\') for d in string.ascii_uppercase 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"""
+ 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."""
+ normalized_path = os.path.realpath(path)
+
+ if not os.path.isdir(normalized_path):
+ raise InvalidPathError(path)
+
+ return normalized_path
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'"
]
},
{
diff --git a/screenshots/FileChooser_screenshot_7.png b/screenshots/FileChooser_screenshot_7.png
new file mode 100644
index 0000000..d2e0349
Binary files /dev/null and b/screenshots/FileChooser_screenshot_7.png differ
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=(