Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added FileSelector widget #909

Merged
merged 26 commits into from
Jan 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/reference/widgets/CrossSelector.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@
"\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",
"##### Display\n",
"\n",
"* **``disabled``** (boolean): Whether the widget is editable\n",
"* **``name``** (str): The title of the widget\n",
"* **``size``** (int): The number of items to show in the selected and unselected lists\n",
"\n",
"___"
]
Expand Down
105 changes: 105 additions & 0 deletions examples/reference/widgets/FileSelector.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{
"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",
"* **``size``** (int): The number of items to show in the selected and unselected lists\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