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

Add jsTree based FileTreeSelector #6837

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8c2c88c
Add jsTree based FileTreeSelector
philippjfr May 15, 2024
4c79770
Fix style
philippjfr May 15, 2024
e45198c
Redesign loading of children
philippjfr May 16, 2024
4615d4e
Remove time.sleep
philippjfr May 16, 2024
da5b109
Allow navigating FileSelector with double click (#6843)
philippjfr May 17, 2024
5bedf2b
Allow RemoteFileSystem on FileSelector
philippjfr May 17, 2024
5c81e76
Allow cascading and add only_files parameter
philippjfr May 17, 2024
8f66682
Factor out base class
philippjfr May 17, 2024
1b92194
Fix lint
hoxbro May 22, 2024
554aa0e
Fix deprecated warnings
hoxbro May 22, 2024
b16e491
Try to fix tests
hoxbro May 22, 2024
c26be1a
Make it so you can pass in a FileSystem, instead of a Provider
hoxbro May 23, 2024
f7c9134
Update FileSelector with remote section
hoxbro May 23, 2024
11ac107
Add docs about FileTree
hoxbro May 23, 2024
264f795
Add s3fs to example feature
hoxbro May 23, 2024
80e5f6f
Lower-pin s3fs
hoxbro May 23, 2024
4cdb779
Update to JS_URLS
hoxbro May 23, 2024
d67c39e
Add max_depth option
hoxbro May 28, 2024
78ec71f
Add max_depth to _rename
hoxbro May 28, 2024
11e5ebb
Add disabled option
hoxbro May 28, 2024
cc90206
Add cascade_to_disabled to ts model
hoxbro May 28, 2024
f23e0f2
Remove commented out console.log
hoxbro May 28, 2024
0f3da03
Add FileTreeSelector
philippjfr Jun 11, 2024
37a2977
Keep state synced
philippjfr Jun 11, 2024
697046a
Further sync fixes
philippjfr Jun 11, 2024
748b08f
Improve style and allow navigation on click
philippjfr Jun 12, 2024
103ffde
Ensure sync does not trigger loop
philippjfr Jun 12, 2024
1fb25e9
Fix lint
hoxbro Jun 24, 2024
1c0cd0f
Merge branch 'main' into tree
hoxbro Jun 24, 2024
af7e75c
Ignore tree
hoxbro Jun 24, 2024
d4d2871
Merge branch 'main' into tree
hoxbro Jul 2, 2024
32896f7
Pin pydeck
hoxbro Jul 2, 2024
c946a85
Add first round of unit test
hoxbro Jul 2, 2024
3b5f675
Add skip if s3fs is not available
hoxbro Jul 2, 2024
cc12be8
Merge branch 'main' into tree
philippjfr Jan 20, 2025
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
Prev Previous commit
Next Next commit
Add FileTreeSelector
philippjfr committed Jun 11, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 0f3da03643e824fc23ab1e7b6c75f8222518a267
28 changes: 22 additions & 6 deletions panel/models/jstree.ts
Original file line number Diff line number Diff line change
@@ -28,6 +28,16 @@ export class NodeEvent extends ModelEvent {
}
}

function sync_state(node: any, tree: any) {
const node_json = tree.get_node(node.id)
if (node_json) {
node.state = node_json.state
}
for (const child of (node.children || [])) {
sync_state(child, tree)
}
}

export class jsTreeView extends LayoutDOMView {
declare model: jsTree
protected _container: HTMLDivElement
@@ -37,13 +47,19 @@ export class jsTreeView extends LayoutDOMView {
override connect_signals(): void {
super.connect_signals()
const {nodes, value, checkbox, show_icons, show_dots, multiple} = this.model.properties
this.on_change(nodes, () => this._jstree.jstree(true).load_node("#", () => {
this._jstree.jstree(true).refresh({
skip_loading: false,
forget_state: true,
this.on_change(nodes, () => {
const tree = this._jstree.jstree(true)
for (const node of this.model.nodes) {
sync_state(node, tree)
}
tree.load_node("#", () => {
tree.refresh({
skip_loading: false,
forget_state: false,
})
this._update_selection_from_value()
})
this._update_selection_from_value()
}))
})
this.on_change(value, () => this._update_selection_from_value())
this.on_change(checkbox, () => this.setCheckboxes())
this.on_change(show_icons, () => this._setShowIcons())
312 changes: 207 additions & 105 deletions panel/widgets/file_selector.py
Original file line number Diff line number Diff line change
@@ -106,6 +106,8 @@ def normalize(path, root=None):

class LocalFileProvider(BaseFileProvider):

sep = os.path.sep

def ls(self, path, file_pattern: str = "[!.]*"):
if not os.path.isdir(path):
return [], []
@@ -128,15 +130,17 @@ def normalize(path, root=None):

class RemoteFileProvider(BaseFileProvider):

sep = '/'

def __init__(self, fs: AbstractFileSystem):
self.fs = fs

def isdir(self, path):
return self.fs.isdir(path)

def ls(self, path: str, file_pattern: str = "[!.]*"):
if not path.endswith('/'):
path += '/'
if not path.endswith(self.sep):
path += self.sep
raw_ls = self.fs.ls(path, detail=True)
prefix = ''
if scheme:= urlparse(path).scheme:
@@ -184,7 +188,112 @@ def fs(self):
return self._provider.fs


class FileSelector(BaseFileSelector, CompositeWidget):
class BaseFileNavigator(BaseFileSelector, CompositeWidget):

_composite_type: ClassVar[type[ListPanel]] = Column

def __init__(self, directory: AnyStr | os.PathLike | None = None, **params):
super().__init__(directory=directory, **params)

layout = {p: getattr(self, p) for p in Layoutable.param
if p not in ('name', 'height', 'margin') and getattr(self, p) is not None}

self._back = Button(
name='◀', width=40, height=40, margin=(5, 10, 0, 0), disabled=True,
align='end', on_click=self._go_back
)
self._forward = Button(
name='▶', width=40, height=40, margin=(5, 10, 0, 0), disabled=True,
align='end', on_click=self._go_forward
)
self._up = Button(
name='⬆', width=40, height=40, margin=(5, 10, 0, 0), disabled=True,
align='end', on_click=self._go_up
)
self._directory = TextInput.from_param(
self.param.directory, margin=(5, 10, 0, 0), width_policy='max', height_policy='max'
)
self._go = Button(
name='⬇', disabled=True, width=40, height=40, margin=(5, 5, 0, 0),
align='end', on_click=self._update_files
)
self._reload = Button(
name='↻', width=40, height=40, margin=(5, 0, 0, 10), align='end',
on_click=self._update_files
)
self._nav_bar = Row(
self._back, self._forward, self._up, self._directory, self._go, self._reload,
**dict(layout, width=None, margin=0, width_policy='max')
)
self._composite[:] = [self._nav_bar, Divider(margin=0), self._selector]
self._directory.param.watch(self._dir_change, 'value')

# Set up state
self._stack = []
self._cwd = None
self._position = -1
self._update_files(True)

@property
def _root_directory(self):
return self.root_directory or self.directory

def _dir_change(self, event: param.parameterized.Event):
path = fullpath(self.directory)
if not path.startswith(self._root_directory):
self.directory = self._root_directory
return
elif path != self.directory:
self.directory = path
self._go.disabled = path == self._cwd

def _refresh(self):
self._update_files(refresh=True)

def _go_back(self, event: param.parameterized.Event):
self._position -= 1
self.directory = self._stack[self._position]
self._update_files()
self._forward.disabled = False
if self._position == 0:
self._back.disabled = True

def _go_forward(self, event: param.parameterized.Event):
self._position += 1
self.directory = self._stack[self._position]
self._update_files()

def _go_up(self, event: Optional[param.parameterized.Event] = None):
path = self._cwd.split(os.path.sep)
self.directory = os.path.sep.join(path[:-1]) or os.path.sep
self._update_files(True)

def _update_files(
self, event: Optional[param.parameterized.Event] = None, refresh: bool = False
):
path = self._provider.normalize(self._directory.value)
refresh = refresh or (event and getattr(event, 'obj', None) is self._reload)
if refresh:
path = self._cwd
elif not self._provider.isdir(path):
self._selector.options = ['Entered path is not valid']
self._selector.disabled = True
return
elif event is not None and (not self._stack or path != self._stack[-1]):
self._stack.append(path)
self._position += 1

self._cwd = path
if not refresh:
self._go.disabled = True
self._up.disabled = path == self._root_directory
if self._position == len(self._stack)-1:
self._forward.disabled = True
if 0 <= self._position and len(self._stack) > 1:
self._back.disabled = False


class FileSelector(BaseFileNavigator):
"""
The `FileSelector` widget allows browsing the filesystem on the
server and selecting one or more files in a directory.
@@ -219,48 +328,23 @@ def __init__(
from ..pane import Markdown
if params.get('width') and params.get('height') and 'sizing_mode' not in params:
params['sizing_mode'] = None
super().__init__(directory=directory, fs=fs, **params)

# Set up layout
layout = {p: getattr(self, p) for p in Layoutable.param
if p not in ('name', 'height', 'margin') and getattr(self, p) is not None}
sel_layout = dict(layout, sizing_mode='stretch_width', height=300, margin=0)
self._selector = CrossSelector(
filter_fn=lambda p, f: fnmatch(f, p), size=self.size, **sel_layout
filter_fn=lambda p, f: fnmatch(f, p), size=self.param.size, **sel_layout
)

self._back = Button(name='◀', width=40, height=40, margin=(5, 10, 0, 0), disabled=True, align='center')
self._forward = Button(name='▶', width=40, height=40, margin=(5, 10, 0, 0), disabled=True, align='center')
self._up = Button(name='⬆', width=40, height=40, margin=(5, 10, 0, 0), disabled=True, align='center')
self._directory = TextInput(value=self.directory, margin=(5, 10, 0, 0), width_policy='max', height_policy='max')
self._go = Button(name='⬇', disabled=True, width=40, height=40, margin=(5, 5, 0, 0), align='center')
self._reload = Button(name='↻', width=40, height=40, margin=(5, 0, 0, 10), align='center')
self._nav_bar = Row(
self._back, self._forward, self._up, self._directory, self._go, self._reload,
**dict(layout, width=None, margin=0, width_policy='max')
)
self._composite[:] = [self._nav_bar, Divider(margin=0), self._selector]
super().__init__(directory=directory, fs=fs, **params)

style = 'h4 { margin-block-start: 0; margin-block-end: 0;}'
self._selector._selected.insert(0, Markdown('#### Selected files', margin=0, stylesheets=[style]))
self._selector._unselected.insert(0, Markdown('#### File Browser', margin=0, stylesheets=[style]))
self.link(self._selector, size='size')

# Set up state
self._stack = []
self._cwd = None
self._position = -1
self._update_files(True)

# Set up callback
self._selector._lists[False].on_double_click(self._select_and_go)
self.link(self._directory, directory='value')
self._selector.param.watch(self._update_value, 'value')
self._go.on_click(self._update_files)
self._reload.on_click(self._update_files)
self._up.on_click(self._go_up)
self._back.on_click(self._go_back)
self._forward.on_click(self._go_forward)
self._directory.param.watch(self._dir_change, 'value')
self._selector._lists[False].param.watch(self._select, 'value')
self._selector._lists[False].param.watch(self._filter_denylist, 'options')
self._periodic = PeriodicCallback(callback=self._refresh, period=self.refresh_period or 0)
@@ -287,51 +371,16 @@ def _update_periodic(self, event: param.parameterized.Event):
elif self._periodic.running:
self._periodic.stop()

@property
def _root_directory(self):
return self.root_directory or self.directory

def _update_value(self, event: param.parameterized.Event):
value = [v for v in event.new if v != '..' and (not self.only_files or os.path.isfile(v))]
self._selector.value = value
self.value = value

def _dir_change(self, event: param.parameterized.Event):
path = fullpath(self._directory.value)
if not path.startswith(self._root_directory):
self._directory.value = self._root_directory
return
elif path != self._directory.value:
self._directory.value = path
self._go.disabled = path == self._cwd

def _refresh(self):
self._update_files(refresh=True)

def _update_files(
self, event: Optional[param.parameterized.Event] = None, refresh: bool = False
):
path = self._provider.normalize(self._directory.value)
refresh = refresh or (event and getattr(event, 'obj', None) is self._reload)
if refresh:
path = self._cwd
elif not self._provider.isdir(path):
self._selector.options = ['Entered path is not valid']
self._selector.disabled = True
return
elif event is not None and (not self._stack or path != self._stack[-1]):
self._stack.append(path)
self._position += 1

self._cwd = path
if not refresh:
self._go.disabled = True
self._up.disabled = path == self._root_directory
if self._position == len(self._stack)-1:
self._forward.disabled = True
if 0 <= self._position and len(self._stack) > 1:
self._back.disabled = False

super()._update_files(event, refresh)
selected = self.value
dirs, files = self._provider.ls(path, self.file_pattern)
for s in selected:
@@ -389,24 +438,6 @@ def _select(self, event: param.parameterized.Event):
else:
self._directory.value = self._cwd

def _go_back(self, event: param.parameterized.Event):
self._position -= 1
self._directory.value = self._stack[self._position]
self._update_files()
self._forward.disabled = False
if self._position == 0:
self._back.disabled = True

def _go_forward(self, event: param.parameterized.Event):
self._position += 1
self._directory.value = self._stack[self._position]
self._update_files()

def _go_up(self, event: Optional[param.parameterized.Event] = None):
path = self._cwd.split(os.path.sep)
self._directory.value = os.path.sep.join(path[:-1]) or os.path.sep
self._update_files(True)


class FileTree(BaseFileSelector, _TreeBase):
"""
@@ -424,28 +455,54 @@ class FileTree(BaseFileSelector, _TreeBase):
'directory': None,
'file_pattern': None,
'root_directory': None,
'provider': None,
'only_files': 'cascade',
'max_depth': None,
**_TreeBase._rename,
}

@param.depends('directory', watch=True, on_init=True)
def _set_data_from_directory(self, *event):
def __init__(
self,
directory: AnyStr | os.PathLike | None = None,
fs: AbstractFileSystem | None = None,
**params,
):
self._index = {}
super().__init__(directory, fs, **params)
self._internal_callbacks.append(
self.param.watch(self._set_data_from_directory, 'directory')
)
self._set_data_from_directory()

def _set_data_from_directory(self, *events):
children, _ = self._get_children(
Path(self.directory).name, self.directory, depth=1
)
self._nodes = [{
"id": self._provider.normalize(self.directory),
"id": self._provider.normalize(self.directory)+self._provider.sep,
"text": Path(self.directory).name,
"icon": "jstree-folder",
"type": "folder",
"state": {"opened": True},
"children": self._get_children(Path(self.directory).name, self.directory, depth=1)
"children": children
}]
self._reindex()
if events:
self._param_change(*events)

def _process_param_change(self, params):
if 'directory' in params:
params['nodes'] = self._nodes
del params['directory']
props = super()._process_param_change(params)
return props

def _process_property_change(self, msg):
props = super()._process_property_change(msg)
if "value" in props and self.only_files:
props["value"] = [node_id for node_id in props["value"] if self._index.get(node_id, {}).get("type") == "file"]
props["value"] = [
node_id for node_id in props["value"]
if self._index.get(node_id, {}).get("type") == "file"
]
return props

def _exceed_max_depth(self, path):
@@ -460,27 +517,45 @@ def _get_children(self, text: str, directory: str, depth=0, children_to_skip=(),
parent = str(directory) # TODO(hoxbro): Either the type hint is wrong or this is not needed
nodes = []
dirs, files = self._get_paths(directory, children_to_skip=children_to_skip)
parent_node = self._index.get(parent)
if parent_node:
removed = [sn for sn in parent_node['children'] if sn['id'] not in dirs+files]
parent_node['children'] = [sn for sn in parent_node['children'] if sn not in removed]
else:
removed = []
for subdir in dirs:
subdir_p = Path(subdir)
if depth > 0:
children = self._get_children(subdir_p.name, subdir, depth=depth - 1)
children, removed_children = self._get_children(
subdir_p.name, subdir, depth=depth-1
)
removed.extend(removed_children)
else:
children, removed_children = [], []
if subdir in self._index:
dir_spec = self._index[subdir]
dir_spec['children'] = [
sn for sn in dir_spec['children']
if sn not in removed_children
]
else:
children = None
dir_spec = self._to_json(
id_=subdir, label=subdir_p.name, parent=parent,
children=children, icon="jstree-folder", type='folder', **kwargs
)
dir_spec = self._to_json(
id_=subdir, label=subdir_p.name, parent=parent,
children=children, icon="jstree-folder", type='folder', **kwargs
)
if self._exceed_max_depth(subdir_p):
dir_spec["state"] = {"disabled": True}
nodes.append(dir_spec)
nodes.extend(
self._to_json(
id_=subfile, label=Path(subfile).name, parent=parent,
icon="jstree-file", type='file', **kwargs
)
for subfile in files
)
return nodes
for subfile in files:
if subfile in self._index:
file_node = self._index[subfile]
else:
file_node = self._to_json(
id_=subfile, label=Path(subfile).name, parent=parent,
icon="jstree-file", type='file', **kwargs
)
nodes.append(file_node)
return nodes, removed

def _get_paths(self, directory, children_to_skip=()):
dirs_, files = self._provider.ls(str(directory))
@@ -491,3 +566,30 @@ def _get_paths(self, directory, children_to_skip=()):
dirs.append(d)
files = [f for f in files if f not in children_to_skip]
return sorted(dirs), sorted(files)


class FileTreeSelector(BaseFileNavigator):
"""
The `FileTreeSelector` widget allows browsing the filesystem on the
server and selecting one or more files in a directory using a tree
based UI.
Reference: https://panel.holoviz.org/reference/widgets/FileTreeSelector.html
:Example:
>>> FileTreeSelector(directory='~', file_pattern='*.png')
"""

def __init__(
self,
directory: AnyStr | os.PathLike | None = None,
fs: AbstractFileSystem | None = None,
**params,
):
layout = {p: getattr(self, p) for p in Layoutable.param
if p not in ('name', 'height', 'margin') and getattr(self, p) is not None}
sel_layout = dict(layout, sizing_mode='stretch_width', height=300, margin=0)
self._selector = FileTree(directory=directory, fs=fs, **sel_layout)
super().__init__(directory=directory, fs=fs, **params)
self._selector.directory = self.param.directory
20 changes: 12 additions & 8 deletions panel/widgets/tree.py
Original file line number Diff line number Diff line change
@@ -73,11 +73,9 @@ def _to_json(
id_, label, parent: str = None, children: Optional[list] = None,
icon: str = None, **kwargs
):
jsn = dict(id=id_, text=label, **kwargs)
jsn = dict(id=id_, text=label, children=children or [], **kwargs)
if parent:
jsn["parent"] = parent
if children:
jsn["children"] = children
if icon:
jsn["icon"] = icon
else:
@@ -137,22 +135,28 @@ def _add_children_on_node_open(self, event: NodeEvent, **kwargs):
children_nodes = opened_node["children_nodes"]
new_nodes = []
for node in children_nodes:
children = self._get_children(
children, _ = self._get_children(
node["text"],
node["id"],
**{"children_to_skip": nodes_already_sent, **kwargs}
)
new_nodes.extend(children)
for child in children:
self._index[child['id']] = child
parent = self._index[node['id']]
if 'children' not in parent:
parent['children'] = []
parent['children'].extend(children)
for child in children:
if child['id'] in self._index:
continue
new_nodes.extend(children)
self._index[child['id']] = child
parent['children'].append(child)
self._new_nodes = new_nodes

@abstractmethod
def _get_children(self, node_name, node_id, **kwargs):
"""
Returns the list of children of a node and any children that
were removed from the node.
"""
raise NotImplementedError()



Unchanged files with check annotations Beta

autoreload(path)
state.cache['num'] = 0
serve_component(page, path)

Check failure on line 30 in panel/tests/ui/io/test_reload.py

GitHub Actions / ui:test-ui:ubuntu-latest

test_reload_app_on_touch[/home/runner/work/panel/panel/panel/tests/ui/io/app.ipynb] TimeoutError: wait_until timed out in 5000 milliseconds
expect(page.locator('.counter')).to_have_text('0')
pathlib.Path(module.name).touch()
time.sleep(0.1)
wait_until(lambda: expect(page.locator('.markdown')).to_have_text('bar'), page)

Check failure on line 80 in panel/tests/ui/io/test_reload.py

GitHub Actions / ui:test-ui:macos-latest

test_reload_app_on_local_module_change TimeoutError: wait_until timed out in 5000 milliseconds
md2_handle.drag_to(md2_handle, target_position={'x': 0, 'y': -50}, force=True)
wait_until(lambda: list(tmpl.layout) == [id(md2), id(md1)], page)

Check failure on line 179 in panel/tests/ui/template/test_editabletemplate.py

GitHub Actions / ui:test-ui:ubuntu-latest

test_editable_template_drag_item TimeoutError: wait_until timed out in 5000 milliseconds
def test_editable_template_undo_drag_item(page):
tmpl = EditableTemplate()
widget.sorters = [{'field': 'int', 'dir': 'desc'}]
sheader = page.locator('[aria-sort="descending"]:visible')
expect(sheader).to_have_count(1)

Check failure on line 2405 in panel/tests/ui/widgets/test_tabulator.py

GitHub Actions / ui:test-ui:ubuntu-latest

test_tabulator_sorters_set_after_init AssertionError: Locator expected to have count '1' Actual value: 0 Call log: LocatorAssertions.to_have_count with timeout 5000ms - waiting for locator("[aria-sort=\"descending\"]:visible") - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0" - locator resolved to 0 elements - unexpected value "0"
assert sheader.get_attribute('tabulator-field') == 'int'
expected_df_sorted = df_mixed.sort_values('int', ascending=False)
assert selector._go.disabled
assert selector._forward.disabled
assert not selector._back.disabled
assert selector._selector.options == {

Check failure on line 47 in panel/tests/widgets/test_file_selector.py

GitHub Actions / core:test-core:ubuntu-latest

test_file_selector_address_bar AssertionError: assert {'a': '/tmp/p...ir/subdir1/b'} == {'a': '/tmp/p... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/tmp/pytest-of-runner/pytest-0/popen-gw0/test_file_selector_address_bar0/test_dir/subdir1/a', 'b': '/tmp/pytest-of-runner/pytest-0/popen-gw0/test_file_selector_address_bar0/test_dir/subdir1/b', - '⬆ ..': '..', }

Check failure on line 47 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-310:ubuntu-latest

test_file_selector_address_bar AssertionError: assert {'a': '/tmp/p...ir/subdir1/b'} == {'a': '/tmp/p... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/tmp/pytest-of-runner/pytest-0/popen-gw2/test_file_selector_address_bar0/test_dir/subdir1/a', 'b': '/tmp/pytest-of-runner/pytest-0/popen-gw2/test_file_selector_address_bar0/test_dir/subdir1/b', - '⬆ ..': '..', }

Check failure on line 47 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-312:ubuntu-latest

test_file_selector_address_bar AssertionError: assert {'a': '/tmp/p...ir/subdir1/b'} == {'a': '/tmp/p... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/tmp/pytest-of-runner/pytest-0/popen-gw3/test_file_selector_address_bar0/test_dir/subdir1/a', 'b': '/tmp/pytest-of-runner/pytest-0/popen-gw3/test_file_selector_address_bar0/test_dir/subdir1/b', - '⬆ ..': '..', }

Check failure on line 47 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-310:macos-latest

test_file_selector_address_bar AssertionError: assert {'a': '/priva...ir/subdir1/b'} == {'a': '/priva... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/private/var/folders/lr/439_fwvd3m76p9vy50d57kcc0000gn/T/pytest-of-runner/pytest-0/popen-gw0/test_file_selector_address_bar0/test_dir/subdir1/a', 'b': '/private/var/folders/lr/439_fwvd3m76p9vy50d57kcc0000gn/T/pytest-of-runner/pytest-0/popen-gw0/test_file_selector_address_bar0/test_dir/subdir1/b', - '⬆ ..': '..', }

Check failure on line 47 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-312:macos-latest

test_file_selector_address_bar AssertionError: assert {'a': '/priva...ir/subdir1/b'} == {'a': '/priva... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/private/var/folders/lr/439_fwvd3m76p9vy50d57kcc0000gn/T/pytest-of-runner/pytest-0/popen-gw1/test_file_selector_address_bar0/test_dir/subdir1/a', 'b': '/private/var/folders/lr/439_fwvd3m76p9vy50d57kcc0000gn/T/pytest-of-runner/pytest-0/popen-gw1/test_file_selector_address_bar0/test_dir/subdir1/b', - '⬆ ..': '..', }
'⬆ ..': '..',
'a': os.path.join(test_dir, 'subdir1', 'a'),
'b': os.path.join(test_dir, 'subdir1', 'b'),
selector._directory.value = os.path.join(test_dir, 'subdir1')
selector._go.clicks = 1
assert selector._selector._lists[False].options == ['⬆ ..', 'a']

Check failure on line 139 in panel/tests/widgets/test_file_selector.py

GitHub Actions / core:test-core:ubuntu-latest

test_file_selector_file_pattern AssertionError: assert ['a'] == ['⬆ ..', 'a'] At index 0 diff: 'a' != '⬆ ..' Right contains one more item: 'a' Full diff: [ - '⬆ ..', 'a', ]

Check failure on line 139 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-310:ubuntu-latest

test_file_selector_file_pattern AssertionError: assert ['a'] == ['⬆ ..', 'a'] At index 0 diff: 'a' != '⬆ ..' Right contains one more item: 'a' Full diff: [ - '⬆ ..', 'a', ]

Check failure on line 139 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-312:ubuntu-latest

test_file_selector_file_pattern AssertionError: assert ['a'] == ['⬆ ..', 'a'] At index 0 diff: 'a' != '⬆ ..' Right contains one more item: 'a' Full diff: [ - '⬆ ..', 'a', ]

Check failure on line 139 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-310:macos-latest

test_file_selector_file_pattern AssertionError: assert ['a'] == ['⬆ ..', 'a'] At index 0 diff: 'a' != '⬆ ..' Right contains one more item: 'a' Full diff: [ - '⬆ ..', 'a', ]

Check failure on line 139 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-312:macos-latest

test_file_selector_file_pattern AssertionError: assert ['a'] == ['⬆ ..', 'a'] At index 0 diff: 'a' != '⬆ ..' Right contains one more item: 'a' Full diff: [ - '⬆ ..', 'a', ]
def test_file_selector_double_click_up(test_dir):
selector._go.clicks = 1
selector._selector._lists[False].value = ['⬆ ..']
selector._selector._buttons[True].param.trigger('clicks')

Check failure on line 161 in panel/tests/widgets/test_file_selector.py

GitHub Actions / core:test-core:ubuntu-latest

test_file_selector_cannot_select_up KeyError: '⬆ ..'

Check failure on line 161 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-310:ubuntu-latest

test_file_selector_cannot_select_up KeyError: '⬆ ..'

Check failure on line 161 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-312:ubuntu-latest

test_file_selector_cannot_select_up KeyError: '⬆ ..'

Check failure on line 161 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-310:macos-latest

test_file_selector_cannot_select_up KeyError: '⬆ ..'

Check failure on line 161 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-312:macos-latest

test_file_selector_cannot_select_up KeyError: '⬆ ..'
assert not selector._selector._lists[False].value
assert not selector._selector._lists[True].options
selector._selector._lists[True].value = ['\U0001f4c1'+os.path.join('..', 'subdir2')]
selector._selector._buttons[False].clicks = 1
assert selector._selector.options == {

Check failure on line 187 in panel/tests/widgets/test_file_selector.py

GitHub Actions / core:test-core:ubuntu-latest

test_file_selector_multiple_across_dirs AssertionError: assert {'a': '/tmp/p...ir/subdir1/b'} == {'a': '/tmp/p... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/tmp/pytest-of-runner/pytest-0/popen-gw0/test_file_selector_multiple_ac0/test_dir/subdir1/a', 'b': '/tmp/pytest-of-runner/pytest-0/popen-gw0/test_file_selector_multiple_ac0/test_dir/subdir1/b', - '⬆ ..': '..', }

Check failure on line 187 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-310:ubuntu-latest

test_file_selector_multiple_across_dirs AssertionError: assert {'a': '/tmp/p...ir/subdir1/b'} == {'a': '/tmp/p... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/tmp/pytest-of-runner/pytest-0/popen-gw2/test_file_selector_multiple_ac0/test_dir/subdir1/a', 'b': '/tmp/pytest-of-runner/pytest-0/popen-gw2/test_file_selector_multiple_ac0/test_dir/subdir1/b', - '⬆ ..': '..', }

Check failure on line 187 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-312:ubuntu-latest

test_file_selector_multiple_across_dirs AssertionError: assert {'a': '/tmp/p...ir/subdir1/b'} == {'a': '/tmp/p... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/tmp/pytest-of-runner/pytest-0/popen-gw1/test_file_selector_multiple_ac0/test_dir/subdir1/a', 'b': '/tmp/pytest-of-runner/pytest-0/popen-gw1/test_file_selector_multiple_ac0/test_dir/subdir1/b', - '⬆ ..': '..', }

Check failure on line 187 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-310:macos-latest

test_file_selector_multiple_across_dirs AssertionError: assert {'a': '/priva...ir/subdir1/b'} == {'a': '/priva... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/private/var/folders/lr/439_fwvd3m76p9vy50d57kcc0000gn/T/pytest-of-runner/pytest-0/popen-gw2/test_file_selector_multiple_ac0/test_dir/subdir1/a', 'b': '/private/var/folders/lr/439_fwvd3m76p9vy50d57kcc0000gn/T/pytest-of-runner/pytest-0/popen-gw2/test_file_selector_multiple_ac0/test_dir/subdir1/b', - '⬆ ..': '..', }

Check failure on line 187 in panel/tests/widgets/test_file_selector.py

GitHub Actions / unit:test-312:macos-latest

test_file_selector_multiple_across_dirs AssertionError: assert {'a': '/priva...ir/subdir1/b'} == {'a': '/priva... '⬆ ..': '..'} Omitting 2 identical items, use -vv to show Right contains 1 more item: {'⬆ ..': '..'} Full diff: { 'a': '/private/var/folders/lr/439_fwvd3m76p9vy50d57kcc0000gn/T/pytest-of-runner/pytest-0/popen-gw0/test_file_selector_multiple_ac0/test_dir/subdir1/a', 'b': '/private/var/folders/lr/439_fwvd3m76p9vy50d57kcc0000gn/T/pytest-of-runner/pytest-0/popen-gw0/test_file_selector_multiple_ac0/test_dir/subdir1/b', - '⬆ ..': '..', }
'⬆ ..': '..',
'a': os.path.join(test_dir, 'subdir1', 'a'),
'b': os.path.join(test_dir, 'subdir1', 'b'),
def test_png_native_size(embed, page):
png = PNG(PNG_FILE, embed=embed)
bbox = get_bbox(page, png)
assert bbox['width'] == 800

Check failure on line 34 in panel/tests/ui/pane/test_image.py

GitHub Actions / ui:test-ui:macos-latest

test_png_native_size[False] assert 0 == 800
assert bbox['height'] == 600
@pytest.mark.parametrize('embed', [False, True])
row = Row(png, width=800)
bbox = get_bbox(page, row)
assert bbox['width'] == 780
assert bbox['height'] == 585

Check failure on line 65 in panel/tests/ui/pane/test_image.py

GitHub Actions / ui:test-ui:macos-latest

test_png_scale_width[False-stretch_width] assert 0 == 585
@pytest.mark.parametrize('sizing_mode', ['scale_height', 'stretch_height'])
@pytest.mark.parametrize('embed', [False, True])