Skip to content

Commit

Permalink
Added FileSelector widget (#909)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Jan 20, 2020
1 parent cae7f46 commit 28e6fff
Show file tree
Hide file tree
Showing 8 changed files with 540 additions and 52 deletions.
2 changes: 2 additions & 0 deletions examples/reference/widgets/CrossSelector.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"\n",
"##### Core\n",
"\n",
"* **``definition_order``** (boolean, default=True): Whether to preserve definition order after filtering. Disable to allow the order of selection to define the order of the selected list.\n",
"* **``filter_fn``** (function): The filter function applied when querying using the text fields, defaults to re.search. Function is two arguments, the query or pattern and the item label.\n",
"* **``options``** (list or dict): List or dictionary of available options\n",
"* **``value``** (boolean): Currently selected options\n",
"\n",
Expand Down
104 changes: 104 additions & 0 deletions examples/reference/widgets/FileSelector.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"pn.extension()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``FileSelector`` widget allows browsing the filesystem on the server and selecting one or more files in a directory.\n",
"\n",
"For more information about listening to widget events and laying out widgets refer to the [widgets user guide](../../user_guide/Widgets.ipynb). Alternatively you can learn how to build GUIs by declaring parameters independently of any specific widgets in the [param user guide](../../user_guide/Param.ipynb). To express interactivity entirely using Javascript without the need for a Python server take a look at the [links user guide](../../user_guide/Param.ipynb).\n",
"\n",
"#### Parameters:\n",
"\n",
"For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n",
"\n",
"##### Core\n",
"\n",
"* **``directory``** (str): The directory to browse (cannot access files above this directory).\n",
"* **``file_pattern``** (str, default='*'): A glob-like query expression to limit the displayed files.\n",
"* **``only_files``** (bool, default=False): Whether to only allow selecting files.\n",
"* **``show_hidden``** (bool, default=False): Whether to show hidden files and directories (starting with a period).\n",
"* **``value``** (list[str]): A list of file names.\n",
"\n",
"##### Display\n",
"\n",
"* **``name``** (str): The title of the widget\n",
"\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``FileSelector`` widget allows exploring the specified directory on the server's filesystem and any directories contained within it. The widget consists of the navigation bar with a number of buttons and the address bar:\n",
"\n",
"* Back (`◀`): Goes to the previous directory\n",
"* Forward (`▶`): Returns to the last directory after navigating back\n",
"* Up (`⬆`): Goes one directory up\n",
"* Address bar: Display the directory to navigate to\n",
"* Enter (`⬇`): Navigates to the directory in the address bar\n",
"\n",
"The actual file selector displays the contents of the current directory, to navigate to a subfolder click on a directory in the file selector and then hit the down arrow (`⬇`) in the navigation bar. Files and folders may be selected by selecting them in the browser on the left and moving them to the right with the arrow buttons:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"files = pn.widgets.FileSelector('~')\n",
"\n",
"files"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To get the currently selected files simply access the `value` parameter:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"files.value"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.5"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
167 changes: 167 additions & 0 deletions panel/tests/widgets/test_file_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from __future__ import absolute_import, division, unicode_literals

import os
import shutil

from collections import OrderedDict

import pytest

from panel.widgets import FileSelector

@pytest.yield_fixture
def test_dir():
test_dir = os.path.expanduser('~/test_dir')
os.mkdir(test_dir)
os.mkdir(os.path.expanduser('~/test_dir/subdir1'))
with open(os.path.expanduser('~/test_dir/subdir1/a'), 'a'):
pass
with open(os.path.expanduser('~/test_dir/subdir1/b'), 'a'):
pass
os.mkdir(os.path.expanduser('~/test_dir/subdir2'))
yield test_dir
shutil.rmtree(os.path.expanduser('~/test_dir'))


def test_file_selector_init(test_dir):
selector = FileSelector(test_dir)

assert selector._selector.options == {
'\U0001f4c1subdir1': os.path.join(test_dir, 'subdir1'),
'\U0001f4c1subdir2': os.path.join(test_dir, 'subdir2')
}


def test_file_selector_address_bar(test_dir):
selector = FileSelector(test_dir)

selector._directory.value = os.path.join(test_dir, 'subdir1')

assert not selector._go.disabled

selector._go.clicks = 1

assert selector._cwd == os.path.join(test_dir, 'subdir1')
assert selector._go.disabled
assert selector._forward.disabled
assert not selector._back.disabled
assert selector._selector.options == {
'a': os.path.join(test_dir, 'subdir1', 'a'),
'b': os.path.join(test_dir, 'subdir1', 'b')
}

selector._up.clicks = 1

selector._selector._lists[False].value = ['subdir1']

assert selector._directory.value == os.path.join(test_dir, 'subdir1')

selector._selector._lists[False].value = []

assert selector._directory.value == test_dir


def test_file_selector_back_and_forward(test_dir):
selector = FileSelector(test_dir)

selector._directory.value = os.path.join(test_dir, 'subdir1')
selector._go.clicks = 1

assert selector._cwd == os.path.join(test_dir, 'subdir1')
assert not selector._back.disabled
assert selector._forward.disabled

selector._back.clicks = 1

assert selector._cwd == test_dir
assert selector._back.disabled
assert not selector._forward.disabled

selector._forward.clicks = 1

assert selector._cwd == os.path.join(test_dir, 'subdir1')


def test_file_selector_up(test_dir):
selector = FileSelector(test_dir)

selector._directory.value = os.path.join(test_dir, 'subdir1')
selector._go.clicks = 1

assert selector._cwd == os.path.join(test_dir, 'subdir1')

selector._up.clicks = 1

assert selector._cwd == test_dir


def test_file_selector_select_files(test_dir):
selector = FileSelector(test_dir)

selector._directory.value = os.path.join(test_dir, 'subdir1')
selector._go.clicks = 1

selector._selector._lists[False].value = ['a']
selector._selector._buttons[True].clicks = 1

assert selector.value == [os.path.join(test_dir, 'subdir1', 'a')]

selector._selector._lists[False].value = ['b']
selector._selector._buttons[True].clicks = 2

assert selector.value == [
os.path.join(test_dir, 'subdir1', 'a'),
os.path.join(test_dir, 'subdir1', 'b')
]

selector._selector._lists[True].value = ['a', 'b']
selector._selector._buttons[False].clicks = 2

assert selector.value == []


def test_file_selector_only_files(test_dir):
selector = FileSelector(test_dir, only_files=True)

selector._selector._lists[False].value = ['\U0001f4c1subdir1']
selector._selector._buttons[True].clicks = 1

assert selector.value == []
assert selector._selector._lists[False].options == ['\U0001f4c1subdir1', '\U0001f4c1subdir2']


def test_file_selector_file_pattern(test_dir):
selector = FileSelector(test_dir, file_pattern='a')

selector._directory.value = os.path.join(test_dir, 'subdir1')
selector._go.clicks = 1

assert selector._selector._lists[False].options == ['a']


def test_file_selector_multiple_across_dirs(test_dir):
selector = FileSelector(test_dir)

selector._selector._lists[False].value = ['\U0001f4c1subdir2']
selector._selector._buttons[True].clicks = 1

assert selector.value == [os.path.join(test_dir, 'subdir2')]

selector._directory.value = os.path.join(test_dir, 'subdir1')
selector._go.clicks = 1

selector._selector._lists[False].value = ['a']
selector._selector._buttons[True].clicks = 2

assert selector.value == [os.path.join(test_dir, 'subdir2'),
os.path.join(test_dir, 'subdir1', 'a')]

selector._selector._lists[True].value = ['\U0001f4c1'+os.path.join('..', 'subdir2')]
selector._selector._buttons[False].clicks = 1

assert selector._selector.options == OrderedDict([
('a', os.path.join(test_dir, 'subdir1', 'a')),
('b', os.path.join(test_dir, 'subdir1', 'b'))
])
assert selector._selector._lists[False].options == ['b']
assert selector.value == [os.path.join(test_dir, 'subdir1', 'a')]
2 changes: 2 additions & 0 deletions panel/viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ class Layoutable(param.Parameterized):
def __init__(self, **params):
if (params.get('width', None) is not None and
params.get('height', None) is not None and
params.get('width_policy') is None and
params.get('height_policy') is None and
'sizing_mode' not in params):
params['sizing_mode'] = 'fixed'
elif not self.param.sizing_mode.constant and not self.param.sizing_mode.readonly:
Expand Down
3 changes: 2 additions & 1 deletion panel/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
"""
from __future__ import absolute_import, division, unicode_literals

from .ace import Ace # noqa
from .base import Widget, CompositeWidget # noqa
from .button import Button, Toggle # noqa
from .file_selector import FileSelector # noqa
from .input import (# noqa
ColorPicker, Checkbox, DatetimeInput, DatePicker, FileInput,
LiteralInput, StaticText, TextInput, Spinner, PasswordInput,
Expand All @@ -22,4 +24,3 @@
MultiSelect, RadioButtonGroup, RadioBoxGroup, Select, ToggleGroup
)
from .tables import DataFrame # noqa
from .ace import Ace # noqa
10 changes: 9 additions & 1 deletion panel/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,14 @@ class CompositeWidget(Widget):
def __init__(self, **params):
super(CompositeWidget, self).__init__(**params)
layout = {p: getattr(self, p) for p in Layoutable.param
if p != 'name' and getattr(self, p) is not None}
if getattr(self, p) is not None}
self._composite = self._composite_type(**layout)
self._models = self._composite._models
self.param.watch(self._update_layout_params, list(Layoutable.param))

def _update_layout_params(self, *events):
for event in events:
setattr(self._composite, event.name, event.new)

def select(self, selector=None):
"""
Expand Down Expand Up @@ -169,3 +174,6 @@ def _get_model(self, doc, root=None, parent=None, comm=None):

def __contains__(self, object):
return object in self._composite.objects

def _synced_params(self):
return []
Loading

0 comments on commit 28e6fff

Please sign in to comment.