Allow upload event handler to have arbitrary arg name (#755)

This commit is contained in:
Elijah Ahianyo 2023-04-02 22:40:05 +00:00 committed by GitHub
parent 0e9c39d103
commit 50d480da1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 169 additions and 43 deletions

View File

@ -202,7 +202,6 @@ export const uploadFiles = async (
setResult,
files,
handler,
multiUpload,
endpoint
) => {
// If we are already processing an event, or there are no upload files, return.
@ -213,7 +212,6 @@ export const uploadFiles = async (
// Set processing to true to block other events from being processed.
setResult({ ...result, processing: true });
const name = multiUpload ? "files" : "file"
const headers = {
"Content-Type": files[0].type,
};
@ -221,7 +219,7 @@ export const uploadFiles = async (
// Add the token and handler to the file name.
for (let i = 0; i < files.length; i++) {
formdata.append("files", files[i], getToken() + ":" + handler + ":" + name + ":" + files[i].name);
formdata.append("files", files[i], getToken() + ":" + handler + ":" + files[i].name);
}
// Send the file to the server.

View File

@ -1,6 +1,7 @@
"""The main Pynecone app."""
from typing import Any, Callable, Coroutine, Dict, List, Optional, Type, Union
import inspect
from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Type, Union
from fastapi import FastAPI, UploadFile
from fastapi.middleware import cors
@ -23,7 +24,7 @@ from pynecone.route import (
verify_route_validity,
)
from pynecone.state import DefaultState, Delta, State, StateManager, StateUpdate
from pynecone.utils import format
from pynecone.utils import format, types
# Define custom types.
ComponentCallable = Callable[[], Component]
@ -473,18 +474,46 @@ def upload(app: App):
Returns:
The state update after processing the event.
Raises:
ValueError: if there are no args with supported annotation.
"""
token, handler, key = files[0].filename.split(":")[:3]
token, handler = files[0].filename.split(":")[:2]
for file in files:
file.filename = file.filename.split(":")[-1]
# Get the state for the session.
state = app.state_manager.get_state(token)
# Event payload should have `files` as key for multi-uploads and `file` otherwise
handler_upload_param: Tuple = ()
# get handler function
func = getattr(state, handler.split(".")[-1])
# check if there exists any handler args with annotation UploadFile or List[UploadFile]
for k, v in inspect.getfullargspec(
func.fn if isinstance(func, EventHandler) else func
).annotations.items():
if (
types.is_generic_alias(v)
and types._issubclass(v.__args__[0], UploadFile)
or types._issubclass(v, UploadFile)
):
handler_upload_param = (k, v)
break
if not handler_upload_param:
raise ValueError(
f"`{handler}` handler should have a parameter annotated with one of the following: List["
f"pc.UploadFile], pc.UploadFile "
)
# check if handler supports multi-upload
multi_upload = types._issubclass(handler_upload_param[1], List)
event = Event(
token=token,
name=handler,
payload={key: files[0] if key == "file" else files},
payload={handler_upload_param[0]: files if multi_upload else files[0]},
)
update = await state.process(event)
return update

View File

@ -92,7 +92,6 @@ class DataTable(Gridjs):
)
def _render(self) -> Tag:
if isinstance(self.data, Var):
self.columns = BaseVar(
name=f"{self.data.name}.columns"

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import inspect
import json
import os
import re
@ -299,15 +298,9 @@ def format_upload_event(event_spec: EventSpec) -> str:
"""
from pynecone.compiler import templates
multi_upload = any(
types._issubclass(arg_type, List)
for arg_type in inspect.getfullargspec(
event_spec.handler.fn
).annotations.values()
)
state, name = get_event_handler_parts(event_spec.handler)
parent_state = state.split(".")[0]
return f'uploadFiles({parent_state}, {templates.RESULT}, {templates.SET_RESULT}, {parent_state}.files, "{state}.{name}",{str(multi_upload).lower()} ,UPLOAD)'
return f'uploadFiles({parent_state}, {templates.RESULT}, {templates.SET_RESULT}, {parent_state}.files, "{state}.{name}",UPLOAD)'
def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]:

View File

@ -143,21 +143,13 @@ class UploadState(pc.State):
img: str
img_list: List[str]
async def handle_upload(self, file: pc.UploadFile):
async def handle_upload1(self, file: pc.UploadFile):
"""Handle the upload of a file.
Args:
file: The uploaded file.
"""
upload_data = await file.read()
outfile = f".web/public/{file.filename}"
# Save the file.
with open(outfile, "wb") as file_object:
file_object.write(upload_data)
# Update the img var.
self.img = file.filename
pass
async def multi_handle_upload(self, files: List[pc.UploadFile]):
"""Handle the upload of a file.
@ -165,16 +157,7 @@ class UploadState(pc.State):
Args:
files: The uploaded files.
"""
for file in files:
upload_data = await file.read()
outfile = f".web/public/{file.filename}"
# Save the file.
with open(outfile, "wb") as file_object:
file_object.write(upload_data)
# Update the img var.
self.img_list.append(file.filename)
pass
class BaseState(pc.State):
@ -204,7 +187,7 @@ def upload_event_spec():
Returns:
Event Spec.
"""
return EventSpec(handler=UploadState.handle_upload, upload=True) # type: ignore
return EventSpec(handler=UploadState.handle_upload1, upload=True) # type: ignore
@pytest.fixture
@ -225,3 +208,57 @@ def multi_upload_event_spec():
Event Spec.
"""
return EventSpec(handler=UploadState.multi_handle_upload, upload=True) # type: ignore
@pytest.fixture
def upload_state(tmp_path):
"""Create upload state.
Args:
tmp_path: pytest tmp_path
Returns:
The state
"""
class FileUploadState(pc.State):
"""The base state for uploading a file."""
img: str
img_list: List[str]
async def handle_upload2(self, file):
"""Handle the upload of a file.
Args:
file: The uploaded file.
"""
upload_data = await file.read()
outfile = f"{tmp_path}/{file.filename}"
# Save the file.
with open(outfile, "wb") as file_object:
file_object.write(upload_data)
# Update the img var.
self.img = file.filename
async def multi_handle_upload(self, files: List[pc.UploadFile]):
"""Handle the upload of a file.
Args:
files: The uploaded files.
"""
for file in files:
upload_data = await file.read()
outfile = f"{tmp_path}/{file.filename}"
# Save the file.
with open(outfile, "wb") as file_object:
file_object.write(upload_data)
# Update the img var.
self.img_list.append(file.filename)
return FileUploadState

View File

@ -1,13 +1,15 @@
import io
import os.path
from typing import List, Tuple, Type
import pytest
from fastapi import UploadFile
from pynecone.app import App, DefaultState
from pynecone.app import App, DefaultState, upload
from pynecone.components import Box
from pynecone.event import Event
from pynecone.middleware import HydrateMiddleware
from pynecone.state import State
from pynecone.state import State, StateUpdate
from pynecone.style import Style
@ -407,3 +409,71 @@ async def test_dict_mutation_detection__plain_list(
)
assert result.delta == expected_delta
@pytest.mark.asyncio
async def test_upload_file(upload_state):
"""Test that file upload works correctly.
Args:
upload_state: the state
"""
data = b"This is binary data"
# Create a binary IO object and write data to it
bio = io.BytesIO()
bio.write(data)
app = App(state=upload_state)
file1 = UploadFile(
filename="token:file_upload_state.multi_handle_upload:True:image1.jpg",
file=bio,
content_type="image/jpeg",
)
file2 = UploadFile(
filename="token:file_upload_state.multi_handle_upload:True:image2.jpg",
file=bio,
content_type="image/jpeg",
)
fn = upload(app)
result = await fn([file1, file2]) # type: ignore
assert isinstance(result, StateUpdate)
assert result.delta == {
"file_upload_state": {"img_list": ["image1.jpg", "image2.jpg"]}
}
@pytest.mark.asyncio
async def test_upload_file_without_annotation(upload_state):
"""Test that an error is thrown when there's no param annotated with pc.UploadFile or List[UploadFile].
Args:
upload_state: the state
"""
data = b"This is binary data"
# Create a binary IO object and write data to it
bio = io.BytesIO()
bio.write(data)
app = App(state=upload_state)
file1 = UploadFile(
filename="token:upload_state.handle_upload2:True:image1.jpg",
file=bio,
content_type="image/jpeg",
)
file2 = UploadFile(
filename="token:upload_state.handle_upload2:True:image2.jpg",
file=bio,
content_type="image/jpeg",
)
fn = upload(app)
with pytest.raises(ValueError) as err:
await fn([file1, file2])
assert (
err.value.args[0]
== "`upload_state.handle_upload2` handler should have a parameter annotated with one of the following: "
"List[pc.UploadFile], pc.UploadFile "
)

View File

@ -296,7 +296,7 @@ def test_format_upload_event(upload_event_spec):
assert (
format.format_upload_event(upload_event_spec)
== "uploadFiles(upload_state, result, setResult, "
'upload_state.files, "upload_state.handle_upload",false ,'
'upload_state.files, "upload_state.handle_upload1",'
"UPLOAD)"
)
@ -310,7 +310,7 @@ def test_format_sub_state_event(upload_sub_state_event_spec):
assert (
format.format_upload_event(upload_sub_state_event_spec)
== "uploadFiles(base_state, result, setResult, base_state.files, "
'"base_state.sub_upload_state.handle_upload",false ,UPLOAD)'
'"base_state.sub_upload_state.handle_upload",UPLOAD)'
)
@ -323,6 +323,6 @@ def test_format_multi_upload_event(multi_upload_event_spec):
assert (
format.format_upload_event(multi_upload_event_spec)
== "uploadFiles(upload_state, result, setResult, "
'upload_state.files, "upload_state.multi_handle_upload",true ,'
'upload_state.files, "upload_state.multi_handle_upload",'
"UPLOAD)"
)