From 7d44858ec65c103c299d9757c4ef169e5f5b9f9b Mon Sep 17 00:00:00 2001 From: Huseyin Kaya Date: Tue, 19 Mar 2024 23:57:37 +0300 Subject: [PATCH] fix: backward compability file_drop --- solara/components/file_drop.py | 48 +++++++++++++++++++++------ solara/components/file_drop.vue | 21 +++++++----- solara/website/pages/api/file_drop.py | 25 ++++++++++---- solara/website/pages/apps/scatter.py | 2 +- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/solara/components/file_drop.py b/solara/components/file_drop.py index 7da3fa999..319ed13c5 100644 --- a/solara/components/file_drop.py +++ b/solara/components/file_drop.py @@ -1,6 +1,7 @@ import threading import typing -from typing import Callable, Optional, cast, List +from typing import Callable, Optional, cast, List, Union, overload +from typing_extensions import Literal import traitlets from ipyvue import Template @@ -26,24 +27,44 @@ class FileDropZone(FileInput): items = traitlets.List(default_value=[]).tag(sync=True) label = traitlets.Unicode().tag(sync=True) +@overload +@solara.component +def FileDrop( + label: str = ..., + on_total_progress: Optional[Callable[[float], None]] = ..., + on_file: Optional[Callable[[FileInfo], None]] = None, + lazy: bool = ..., + multiple: Literal[False] = ..., +): ... + +@overload +@solara.component +def FileDrop( + label: str = ..., + on_total_progress: Optional[Callable[[float], None]] = ..., + on_file: Optional[Callable[[List[FileInfo]], None]] = None, + lazy: bool = ..., + multiple: Literal[True] = ..., +): ... @solara.component def FileDrop( label="Drop file here", on_total_progress: Optional[Callable[[float], None]] = None, - on_file: Optional[Callable[[List[FileInfo]], None]] = None, + on_file: Optional[Callable[[Union[FileInfo,List[FileInfo]]], None]] = None, lazy: bool = True, + multiple: bool = False, ): - """Region a user can drop a file or multiple files into for file uploading. + """Region a user can drop file(s) into for file uploading. If lazy=True, no file contents will be loaded into memory, nor will any data be transferred by default. - A list of file objects is passed to the `on_file` callback, and data will be transferred - when needed. + If lazy=False, the file contents will be loaded into memory and passed to the `on_file` callback via the `FileInfo.data` attribute. + If multiple=False, a single `FileInfo` object is passed on the `on_file` callback. + If multiple=True, `on_file` receives a list of `FileInfo` objects. Directories are ignored. - If lazy=False, the file contents will be loaded into memory and passed to the `on_file` callback via the `.data` attribute. - The on_file callback takes the list of following argument type: + A file object is of the following argument type: ```python class FileInfo(typing.TypedDict): name: str # file name @@ -55,9 +76,13 @@ class FileInfo(typing.TypedDict): ## Arguments * `on_total_progress`: Will be called with the progress in % of the file(s) upload. - * `on_file`: Will be called with a `List[FileInfo]` objects, each of which contains the file `.name`, `.length` and a `.file_obj` object. + * `on_file`: Will be called with a `List[FileInfo]` or `FileInfo` when multiple=True or multiple=False, respectively. + Each `FileInfo` contains the file `.name`, `.length`, `.file_obj` object, and `.data` attributes. * `lazy`: Whether to load the file contents into memory or not. If `False`, - the file contents will be loaded into memory and passed to the `on_file` callback via the `.data` attribute. + the file contents will be loaded into memory via the `.data` attribute of file object(s). + * `multiple`: Whether to allow uploading multiple files. By default (multiple=False), only a single file + (the first one if multiple files are dropped) is passed on to `on_file` callback function. + If `multiple=True`, the list of dropped files is passed to `on_file`. """ file_info, set_file_info = solara.use_state(None) @@ -88,7 +113,10 @@ def handle_file(cancel: threading.Event): wired_files[i]["data"] = wired_files[i]["file_obj"].read() else: wired_files[i]["data"] = None - on_file(wired_files) + if multiple: + on_file(wired_files) + else: + on_file(wired_files[0]) result: solara.Result = hooks.use_thread(handle_file, [wired_files]) if result.error: diff --git a/solara/components/file_drop.vue b/solara/components/file_drop.vue index 801add70b..8e4d1e161 100644 --- a/solara/components/file_drop.vue +++ b/solara/components/file_drop.vue @@ -1,13 +1,16 @@ @@ -29,9 +32,9 @@ module.exports = { this.native_file_info = nativeFiles this.file_info = this.native_file_info.map( - ({name, isFile, size}) => ({ + ({name, size}) => ({ name, - isFile: !!isFile, // Ensure this is a boolean + isFile: true, size, })); }); diff --git a/solara/website/pages/api/file_drop.py b/solara/website/pages/api/file_drop.py index bc0b819e8..1eeb969d9 100644 --- a/solara/website/pages/api/file_drop.py +++ b/solara/website/pages/api/file_drop.py @@ -6,6 +6,7 @@ import solara from solara.components.file_drop import FileInfo from solara.website.utils import apidoc +from typing import List @solara.component @@ -13,17 +14,29 @@ def Page(): content, set_content = solara.use_state([]) filename, set_filename = solara.use_state([]) size, set_size = solara.use_state([]) + multiple_upload, set_multiple_upload = solara.use_state(False) - def on_file(file: FileInfo): - set_filename([f["name"] for f in file]) - set_size([f["size"] for f in file]) - set_content([f["file_obj"].read(100) for f in file]) + def process_input(input): + if isinstance(input, list): + process_multiple_files(input) + else: + process_single_file(input) + + def process_multiple_files(files: List[FileInfo]): + set_filename([f["name"] for f in files]) + set_size([f["size"] for f in files]) + set_content([f["file_obj"].read(100) for f in files]) + + def process_single_file(file: FileInfo): + process_multiple_files([file]) with solara.Div() as main: + solara.Checkbox(label="Multiple upload", value=multiple_upload, on_value=set_multiple_upload) solara.FileDrop( - label="Drag and drop a file or multiple files here to read the first 100 bytes", - on_file=on_file, + label="Drag and drop files(s) here to read the first 100 bytes", + on_file=process_input, lazy=True, # We will only read the first 100 bytes + multiple=multiple_upload ) if content: solara.Info(f'Number of uploaded files: {len(filename)}') diff --git a/solara/website/pages/apps/scatter.py b/solara/website/pages/apps/scatter.py index 5330baf76..f5e202660 100644 --- a/solara/website/pages/apps/scatter.py +++ b/solara/website/pages/apps/scatter.py @@ -37,7 +37,7 @@ def load_sample(): @staticmethod def load_from_file(file): - df = pd.read_csv(file[0]["file_obj"]) + df = pd.read_csv(file["file_obj"]) State.x.value = str(df.columns[0]) State.y.value = str(df.columns[1]) State.size.value = str(df.columns[2])