kopia lustrzana https://github.com/crahan/ipyfilechooser
Merge pull request #62 from crahan/dev
Sandboxing, custom folder icons, error handling, bug fixesmaster v0.6.0
commit
e4f82f4946
26
README.md
26
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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
from .filechooser import FileChooser
|
||||
|
||||
__version__ = '0.5.0'
|
||||
__version__ = '0.6.0'
|
||||
|
|
|
@ -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)
|
|
@ -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 = '<span style="color:{1};">{0}</span>'
|
||||
_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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
"# Title: <b>FileChooser example</b>\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='<b>FileChooser example</b>',\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'"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 20 KiB |
2
setup.py
2
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=(
|
||||
|
|
Ładowanie…
Reference in New Issue