Skip to content

Commit

Permalink
feat: added FileDropMultiple
Browse files Browse the repository at this point in the history
  • Loading branch information
hkayabilisim committed Mar 22, 2024
1 parent 6b2ea6d commit fa6cd23
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 76 deletions.
2 changes: 1 addition & 1 deletion solara/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from .figure_altair import FigureAltair, AltairChart # noqa: #F401 F403
from .meta import Meta # noqa: #F401 F403
from .columns import Columns, ColumnsResponsive # noqa: #F401 F403
from .file_drop import FileDrop # noqa: #F401 F403
from .file_drop import FileDrop, FileDropMultiple # noqa: #F401 F403
from .file_download import FileDownload # noqa: #F401 F403
from .tooltip import Tooltip # noqa: #F401 F403
from .card import Card, CardActions # noqa: #F401 F403
Expand Down
120 changes: 66 additions & 54 deletions solara/components/file_drop.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import threading
import typing
from typing import Callable, List, Optional, Union, cast, overload
from typing import Callable, List, Optional, cast

import ipyvuetify
import traitlets
from ipyvue import Template
from ipyvuetify.extra import FileInput
from ipywidgets import widget_serialization
from typing_extensions import Literal, TypedDict
from typing_extensions import TypedDict

import solara
import solara.hooks as hooks
Expand All @@ -29,54 +29,18 @@ class FileDropZone(FileInput):
multiple = traitlets.Bool(True).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] = ...,
) -> ipyvuetify.extra.FileInput:
...


@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] = ...,
) -> ipyvuetify.extra.FileInput:
...
@overload
@solara.component
def FileDrop(
label=...,
on_total_progress: Optional[Callable[[float], None]] = ...,
on_file: Optional[Callable[[Union[FileInfo, List[FileInfo]]], None]] = ...,
lazy: bool = ...,
multiple: bool = ...,
) -> ipyvuetify.extra.FileInput:
...

@solara.component
def FileDrop(
label="Drop file here",
on_total_progress: Optional[Callable[[float], None]] = None,
on_file: Optional[Callable[[Union[FileInfo, List[FileInfo]]], None]] = None,
on_file: Optional[Callable[[FileInfo], None]] = None,
lazy: bool = True,
multiple: bool = False,
):
"""Region a user can drop file(s) into for file uploading.
) -> ipyvuetify.extra.FileInput:
"""Region a user can drop a file into for file uploading.
If lazy=True, no file contents will be loaded into memory,
If lazy=True, no file content will be loaded into memory,
nor will any data be transferred by default.
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, file content will be loaded into memory and passed to the `on_file` callback via the `FileInfo.data` attribute.
A file object is of the following argument type:
Expand All @@ -90,20 +54,71 @@ 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]` or `FileInfo` when multiple=True or multiple=False, respectively.
Each `FileInfo` contains the file `.name`, `.length`, `.file_obj` object, and `.data` attributes.
* `on_total_progress`: Will be called with the progress in % of the file upload.
* `on_file`: Will be called with a `FileInfo` object, which contains the file `.name`, `.length` and a `.file_obj` object.
* `lazy`: Whether to load the file contents into memory or not. If `False`,
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)
wired_files, set_wired_files = solara.use_state(cast(Optional[typing.List[FileInfo]], None))

file_drop = FileDropZone.element(label=label, on_total_progress=on_total_progress, on_file_info=set_file_info, multiple=multiple) # type: ignore
file_drop = FileDropZone.element(label=label, on_total_progress=on_total_progress, on_file_info=set_file_info, multiple=False) # type: ignore

def wire_files():
if not file_info:
return

real = cast(FileDropZone, solara.get_widget(file_drop))

# workaround for @observe being cleared
real.version += 1
real.reset_stats()

set_wired_files(cast(typing.List[FileInfo], real.get_files()))

solara.use_side_effect(wire_files, [file_info])

def handle_file(cancel: threading.Event):
if not wired_files:
return
if on_file:
if not lazy:
wired_files[0]["data"] = wired_files[0]["file_obj"].read()
else:
wired_files[0]["data"] = None
on_file(wired_files[0])

result: solara.Result = hooks.use_thread(handle_file, [wired_files])
if result.error:
raise result.error

return file_drop


@solara.component
def FileDropMultiple(
label="Drop files here",
on_total_progress: Optional[Callable[[float], None]] = None,
on_file: Optional[Callable[[List[FileInfo]], None]] = None,
lazy: bool = True,
) -> ipyvuetify.extra.FileInput:
"""Region a user can drop multiple files into for file uploading.
Almost identical to `FileDrop` except that `on_file` is called
with a list of `FileInfo` objects.
## 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]`.
Each `FileInfo` contains the file `.name`, `.length`, `.file_obj` object, and `.data` attributes.
* `lazy`: Whether to load the file contents into memory or not.
"""
file_info, set_file_info = solara.use_state(None)
wired_files, set_wired_files = solara.use_state(cast(Optional[typing.List[FileInfo]], None))

file_drop = FileDropZone.element(label=label, on_total_progress=on_total_progress, on_file_info=set_file_info, multiple=True) # type: ignore

def wire_files():
if not file_info:
Expand All @@ -128,10 +143,7 @@ def handle_file(cancel: threading.Event):
wired_files[i]["data"] = wired_files[i]["file_obj"].read()
else:
wired_files[i]["data"] = None
if multiple:
on_file(wired_files)
else:
on_file(wired_files[0])
on_file(wired_files)

result: solara.Result = hooks.use_thread(handle_file, [wired_files])
if result.error:
Expand Down
74 changes: 53 additions & 21 deletions solara/website/pages/documentation/components/input/file_drop.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,75 @@
"""
# FileDrop
# FileDrop components
FileDrop comes in two flavours:
* `FileDrop` for a single file upload
* `FileDropMultiple` which allows for multiple file upload
"""
import textwrap
from typing import List, Union, cast
from typing import List, cast

import solara
from solara.components.file_drop import FileInfo
from solara.website.utils import apidoc


@solara.component
def Page():
def FileDropMultipleDemo():
content, set_content = solara.use_state(cast(List[bytes], []))
filename, set_filename = solara.use_state(cast(List[str], []))
size, set_size = solara.use_state(cast(List[int], []))
multiple_upload, set_multiple_upload = solara.use_state(False)

def process_files(files: Union[FileInfo, List[FileInfo]]):
if not isinstance(files, list):
files = [files]
def on_file(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])

with solara.Div() as main:
solara.Checkbox(label="Multiple upload", value=multiple_upload, on_value=set_multiple_upload)
solara.FileDrop(
label="Drag and drop files(s) here to read the first 100 bytes.",
on_file=process_files,
lazy=True, # We will only read the first 100 bytes
multiple=multiple_upload,
)
if content:
solara.Info(f"Number of uploaded files: {len(filename)}")
for f, s, c in zip(filename, size, content):
solara.Info(f"File {f} has total length: {s}\n, first 100 bytes:")
solara.Preformatted("\n".join(textwrap.wrap(repr(c))))
solara.FileDropMultiple(
label="Drag and drop files(s) here to read the first 100 bytes.",
on_file=on_file,
lazy=True, # We will only read the first 100 bytes
)
if content:
solara.Info(f"Number of uploaded files: {len(filename)}")
for f, s, c in zip(filename, size, content):
solara.Info(f"File {f} has total length: {s}\n, first 100 bytes:")
solara.Preformatted("\n".join(textwrap.wrap(repr(c))))


return main
@solara.component
def FileDropDemo():
content, set_content = solara.use_state(b"")
filename, set_filename = solara.use_state("")
size, set_size = solara.use_state(0)

def on_file(f: FileInfo):
set_filename(f["name"])
set_size(f["size"])
set_content(f["file_obj"].read(100))

solara.FileDrop(
label="Drag and drop a file here to read the first 100 bytes.",
on_file=on_file,
lazy=True, # We will only read the first 100 bytes
)
if content:
solara.Info(f"File {filename} has total length: {size}\n, first 100 bytes:")
solara.Preformatted("\n".join(textwrap.wrap(repr(content))))


@solara.component
def Page():
with solara.Row():
with solara.Card(title="FileDrop"):
FileDropDemo()
with solara.Card(title="FileDropMultiple"):
FileDropMultipleDemo()


__doc__ += "# FileDrop"
__doc__ += apidoc(solara.FileDrop.f) # type: ignore
__doc__ += "# FileDropMultiple"
__doc__ += apidoc(solara.FileDropMultiple.f) # type: ignore

0 comments on commit fa6cd23

Please sign in to comment.