From 5ad3882898116631ecf0fb316b87eb72d62f3878 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Wed, 26 Apr 2023 19:56:34 +0000 Subject: [PATCH] Handle upload bugfix (#886) --- pynecone/app.py | 6 ++- tests/conftest.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_app.py | 53 +++++++++++++++----- 3 files changed, 169 insertions(+), 13 deletions(-) diff --git a/pynecone/app.py b/pynecone/app.py index 94942e035..b4c387b18 100644 --- a/pynecone/app.py +++ b/pynecone/app.py @@ -504,10 +504,14 @@ def upload(app: App): # Get the state for the session. state = app.state_manager.get_state(token) + + # get the current state(parent state/substate) + path = handler.split(".")[:-1] + current_state = state.get_substate(path) handler_upload_param: Tuple = () # get handler function - func = getattr(state, handler.split(".")[-1]) + func = getattr(current_state, handler.split(".")[-1]) # check if there exists any handler args with annotation, List[UploadFile] for k, v in inspect.getfullargspec( diff --git a/tests/conftest.py b/tests/conftest.py index 8058ed01b..ff55cbd89 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -244,6 +244,129 @@ def upload_state(tmp_path): return FileUploadState +@pytest.fixture +def upload_sub_state(tmp_path): + """Create upload substate. + + Args: + tmp_path: pytest tmp_path + + Returns: + The state + + """ + + class FileState(pc.State): + """The base state.""" + + pass + + class FileUploadState(FileState): + """The substate for uploading a file.""" + + img_list: List[str] + + async def handle_upload2(self, files): + """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) + + 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 + + +@pytest.fixture +def upload_grand_sub_state(tmp_path): + """Create upload grand-state. + + Args: + tmp_path: pytest tmp_path + + Returns: + The state + + """ + + class BaseFileState(pc.State): + """The base state.""" + + pass + + class FileSubState(BaseFileState): + """The substate.""" + + pass + + class FileUploadState(FileSubState): + """The grand-substate for uploading a file.""" + + img_list: List[str] + + async def handle_upload2(self, files): + """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) + + 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 + + @pytest.fixture def base_config_values() -> Dict: """Get base config values. diff --git a/tests/test_app.py b/tests/test_app.py index 34fc070e2..f384a3154 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -412,11 +412,38 @@ async def test_dict_mutation_detection__plain_list( @pytest.mark.asyncio -async def test_upload_file(upload_state): +@pytest.mark.parametrize( + "fixture, expected", + [ + ( + "upload_state", + {"file_upload_state": {"img_list": ["image1.jpg", "image2.jpg"]}}, + ), + ( + "upload_sub_state", + { + "file_state.file_upload_state": { + "img_list": ["image1.jpg", "image2.jpg"] + } + }, + ), + ( + "upload_grand_sub_state", + { + "base_file_state.file_sub_state.file_upload_state": { + "img_list": ["image1.jpg", "image2.jpg"] + } + }, + ), + ], +) +async def test_upload_file(fixture, request, expected): """Test that file upload works correctly. Args: - upload_state: the state + fixture: The state. + request: Fixture request. + expected: Expected delta """ data = b"This is binary data" @@ -424,7 +451,7 @@ async def test_upload_file(upload_state): bio = io.BytesIO() bio.write(data) - app = App(state=upload_state) + app = App(state=request.getfixturevalue(fixture)) file1 = UploadFile( filename="token:file_upload_state.multi_handle_upload:True:image1.jpg", @@ -439,17 +466,19 @@ async def test_upload_file(upload_state): 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"]} - } + assert result.delta == expected @pytest.mark.asyncio -async def test_upload_file_without_annotation(upload_state): +@pytest.mark.parametrize( + "fixture", ["upload_state", "upload_sub_state", "upload_grand_sub_state"] +) +async def test_upload_file_without_annotation(fixture, request): """Test that an error is thrown when there's no param annotated with pc.UploadFile or List[UploadFile]. Args: - upload_state: the state + fixture: The state. + request: Fixture request. """ data = b"This is binary data" @@ -457,15 +486,15 @@ async def test_upload_file_without_annotation(upload_state): bio = io.BytesIO() bio.write(data) - app = App(state=upload_state) + app = App(state=request.getfixturevalue(fixture)) file1 = UploadFile( - filename="token:upload_state.handle_upload2:True:image1.jpg", + filename="token:file_upload_state.handle_upload2:True:image1.jpg", file=bio, content_type="image/jpeg", ) file2 = UploadFile( - filename="token:upload_state.handle_upload2:True:image2.jpg", + filename="token:file_upload_state.handle_upload2:True:image2.jpg", file=bio, content_type="image/jpeg", ) @@ -474,5 +503,5 @@ async def test_upload_file_without_annotation(upload_state): await fn([file1, file2]) assert ( err.value.args[0] - == "`upload_state.handle_upload2` handler should have a parameter annotated as List[pc.UploadFile]" + == "`file_upload_state.handle_upload2` handler should have a parameter annotated as List[pc.UploadFile]" )