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=(