Skip to content

Commit

Permalink
Add support for remote filesystems on FileSelector (#7618)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Jan 16, 2025
1 parent 1786e22 commit 888bdb9
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 129 deletions.
32 changes: 32 additions & 0 deletions examples/reference/widgets/FileSelector.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,38 @@
"source": [
"files.value"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Remote filesystem\n",
"\n",
"By using the power of [`fsspec`](https://filesystem-spec.readthedocs.io/en/latest/) we can connect to remote filesystems. In the example below we use the `s3fs` package to connect to a remote S3 server"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import s3fs\n",
"\n",
"fs = s3fs.S3FileSystem(anon=True)\n",
"\n",
"s3_files = pn.widgets.FileSelector(directory=\"s3://datasets.holoviz.org\", fs=fs)\n",
"s3_files"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"s3_files.value"
]
}
],
"metadata": {
Expand Down
2 changes: 1 addition & 1 deletion panel/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def _write_bundled_files(name, files, explicit_dir=None, ext=None):
filename = str(filename)
if ext and not str(filename).endswith(ext):
filename += f'.{ext}'
if filename.endswith(('.ttf', '.wasm')):
if filename.endswith(('.ttf', '.wasm', '.png', '.gif')):
with open(filename, 'wb') as f:
f.write(response.content)
else:
Expand Down
2 changes: 1 addition & 1 deletion panel/io/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def parse_template(*args, **kwargs):
}

JS_URLS = {
'jQuery': f'{CDN_DIST}bundled/jquery/jquery.slim.min.js',
'jQuery': f'{CDN_DIST}bundled/jquery/jquery.min.js',
'bootstrap4': f'{CDN_DIST}bundled/bootstrap4/js/bootstrap.bundle.min.js',
'bootstrap5': f'{CDN_DIST}bundled/bootstrap5/js/bootstrap.bundle.min.js'
}
Expand Down
10 changes: 2 additions & 8 deletions panel/models/ace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@ import type {Ace} from "ace-code"
import type * as AceCode from "ace-code"
declare const ace: typeof AceCode

import {ID} from "./util"

declare type ModeList = {
getModeForPath(path: string): {mode: string}
}

function ID() {
// Math.random should be unique because of its seeding algorithm.
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
// after the decimal.
const id = Math.random().toString(36).substr(2, 9)
return `_${id}`
}

export class AcePlotView extends HTMLBoxView {
declare model: AcePlot

Expand Down
9 changes: 8 additions & 1 deletion panel/models/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {concat, uniq} from "@bokehjs/core/util/array"
import {isPlainObject, isArray} from "@bokehjs/core/util/types"
import {isArray, isPlainObject} from "@bokehjs/core/util/types"

export const get = (obj: any, path: string, defaultValue: any = undefined) => {
const travel = (regexp: RegExp) =>
Expand Down Expand Up @@ -108,6 +108,13 @@ export async function loadScript(type: string, src: string) {
})
}

export function ID() {
// Math.random should be unique because of its seeding algorithm.
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
// after the decimal.
return `_${ Math.random().toString(36).substring(2, 11)}`
}

export function convertUndefined(obj: any): any {
if (isArray(obj)) {
return obj.map(convertUndefined)
Expand Down
2 changes: 1 addition & 1 deletion panel/tests/ui/widgets/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ def test_multi_select_double_click(page):

page.locator('option').nth(1).dblclick()

wait_until(lambda: bool(clicks) and clicks[0].option == 'B')
wait_until(lambda: bool(clicks) and clicks[0].option == 'B', page)
46 changes: 45 additions & 1 deletion panel/tests/widgets/test_file_selector.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import os

from pathlib import Path

import pytest

try:
import s3fs
except Exception:
s3fs = None

from panel.models.widgets import DoubleClickEvent
from panel.widgets import FileSelector
from panel.widgets.file_selector import (
FileSelector, LocalFileProvider, RemoteFileProvider,
)

s3fs_available = pytest.mark.skipif(s3fs is None, reason='s3fs not available')

FILE_PATH = Path(__file__)


@pytest.fixture
Expand All @@ -21,6 +34,37 @@ def test_dir(tmp_path):

yield str(test_dir)

@pytest.fixture
async def s3_filesystem():
fs = s3fs.S3FileSystem(anon=True)
yield fs
s3 = await fs.get_s3()
await s3.close()

def test_local_file_provider_is_dir():
provider = LocalFileProvider()
assert not provider.isdir(FILE_PATH)
assert provider.isdir(FILE_PATH.parent)

def test_local_file_provider_ls():
provider = LocalFileProvider()
dirs, files = provider.ls(FILE_PATH.parent, '*test_file_selector*')
assert files == [str(FILE_PATH)]

@s3fs_available
def test_remote_file_provider_is_dir(s3_filesystem):
provider = RemoteFileProvider(fs=s3_filesystem)
assert not provider.isdir('s3://datasets.holoviz.org/stocks/v1/stocks.csv')
assert provider.isdir('s3://datasets.holoviz.org/stocks/v1/')

@s3fs_available
def test_remote_file_provider_ls(s3_filesystem):
provider = RemoteFileProvider(fs=s3_filesystem)
dirs, _ = provider.ls('s3://datasets.holoviz.org/stocks/')
assert dirs == ['s3://datasets.holoviz.org/stocks/v1/']
_, files = provider.ls('s3://datasets.holoviz.org/stocks/v1')
assert files == ['s3://datasets.holoviz.org/stocks/v1/stocks.csv']


def test_file_selector_init(test_dir):
selector = FileSelector(test_dir)
Expand Down
9 changes: 6 additions & 3 deletions panel/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,13 @@ def parse_timedelta(time_str: str) -> dt.timedelta | None:
return dt.timedelta(**time_params)


def fullpath(path: AnyStr | os.PathLike) -> AnyStr:
"""Expanduser and then abspath for a given path
def fullpath(path: AnyStr | os.PathLike) -> str:
"""
return os.path.abspath(os.path.expanduser(path))
Expanduser and then abspath for a given path.
"""
if '://' in str(path):
return str(path)
return str(os.path.abspath(os.path.expanduser(path)))


def base_version(version: str) -> str:
Expand Down
Loading

0 comments on commit 888bdb9

Please sign in to comment.