From daadec39681a5cb0ec5c1bfdd33f999071724c0c Mon Sep 17 00:00:00 2001 From: Maypher Date: Fri, 10 Jan 2025 13:44:36 -0300 Subject: [PATCH 1/4] The sanic utility function convert_request_to_files_dict casts files to io.BytesIO which is the filetype used by AIOHTTP. This commit fixes this issue by directly assigning the file instead of casting it. --- strawberry/sanic/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/strawberry/sanic/utils.py b/strawberry/sanic/utils.py index 1d78c09118..885a02c406 100644 --- a/strawberry/sanic/utils.py +++ b/strawberry/sanic/utils.py @@ -1,6 +1,5 @@ from __future__ import annotations -from io import BytesIO from typing import TYPE_CHECKING, Any, Optional, Union, cast from sanic.request import File @@ -10,7 +9,7 @@ def convert_request_to_files_dict(request: Request) -> dict[str, Any]: - """Converts the request.files dictionary to a dictionary of BytesIO objects. + """Converts the request.files dictionary to a dictionary of sanic Request objects. `request.files` has the following format, even if only a single file is uploaded: @@ -29,12 +28,12 @@ def convert_request_to_files_dict(request: Request) -> dict[str, Any]: if not request_files: return {} - files_dict: dict[str, Union[BytesIO, list[BytesIO]]] = {} + files_dict: dict[str, Union[File, list[File]]] = {} for field_name, file_list in request_files.items(): assert len(file_list) == 1 - files_dict[field_name] = BytesIO(file_list[0].body) + files_dict[field_name] = file_list[0] return files_dict From 995d72b3f1c9d7337e6abfeaf37c164232d1181b Mon Sep 17 00:00:00 2001 From: Maypher Date: Fri, 10 Jan 2025 15:59:53 -0300 Subject: [PATCH 2/4] Add tests --- tests/sanic/__init__.py | 0 tests/sanic/test_file_upload.py | 93 +++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 tests/sanic/__init__.py create mode 100644 tests/sanic/test_file_upload.py diff --git a/tests/sanic/__init__.py b/tests/sanic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/sanic/test_file_upload.py b/tests/sanic/test_file_upload.py new file mode 100644 index 0000000000..b5e7bddf58 --- /dev/null +++ b/tests/sanic/test_file_upload.py @@ -0,0 +1,93 @@ +import pytest +from sanic import Sanic +from sanic.request import File +from sanic import response +from sanic_testing.testing import SanicTestClient +from strawberry.sanic.views import GraphQLView +import strawberry +from strawberry.file_uploads import Upload +from io import BytesIO +from strawberry.sanic import utils +from typing import cast + + +@strawberry.type +class Query: + @strawberry.field + def index(self) -> str: + return "Hello there" + + +@strawberry.type +class Mutation: + @strawberry.mutation + def file_upload(self, file: Upload) -> str: + return cast(File, file).name + + +@pytest.fixture +def app(): + sanic_app = Sanic("sanic_testing") + + sanic_app.add_route( + GraphQLView.as_view( + schema=strawberry.Schema(query=Query, mutation=Mutation), + multipart_uploads_enabled=True, + ), + "/graphql", + ) + + return sanic_app + + +def test_file_cast(app: Sanic): + """Tests that the list of files in a sanic Request gets correctly turned into a dictionary""" + file_name = "test.txt" + + file_content = b"Hello, there!." + in_memory_file = BytesIO(file_content) + in_memory_file.name = file_name + + form_data = { + "operations": '{ "query": "mutation($file: Upload!){ fileUpload(file: $file) }", "variables": { "file": null } }', + "map": '{ "file": ["variables.file"] }', + } + + files = { + "file": in_memory_file, + } + + request, _ = cast(SanicTestClient, app.test_client).post( + "/graphql", data=form_data, files=files + ) + + files = utils.convert_request_to_files_dict(request) # type: ignore + file = files["file"] + + assert isinstance(file, File) + assert file.name == file_name + assert file.body == file_content + + +def test_endpoint(app: Sanic): + """Tests that the graphql api correctly handles file upload and processing""" + file_name = "test.txt" + + file_content = b"Hello, there!" + in_memory_file = BytesIO(file_content) + in_memory_file.name = file_name + + form_data = { + "operations": '{ "query": "mutation($file: Upload!){ fileUpload(file: $file) }", "variables": { "file": null } }', + "map": '{ "file": ["variables.file"] }', + } + + files = { + "file": in_memory_file, + } + + _, response = cast(SanicTestClient, app.test_client).post( + "/graphql", data=form_data, files=files + ) + + assert response.json["data"]["fileUpload"] == file_name # type: ignore From d3b065e4ef08865215192bce71311ebc56876a77 Mon Sep 17 00:00:00 2001 From: Maypher Date: Fri, 10 Jan 2025 17:11:52 -0300 Subject: [PATCH 3/4] Add release.md --- RELEASE.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..d879e7212d --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,4 @@ +Release type: patch + +Fix bug where files would be converted into io.BytesIO when using the sanic GraphQLView +instead of using the sanic File type From 08d64bd1aaaff27fee2d7c341612d0954fd16fd9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:23:53 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/sanic/test_file_upload.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/sanic/test_file_upload.py b/tests/sanic/test_file_upload.py index b5e7bddf58..24c3a38d1d 100644 --- a/tests/sanic/test_file_upload.py +++ b/tests/sanic/test_file_upload.py @@ -1,14 +1,15 @@ +from io import BytesIO +from typing import cast + import pytest -from sanic import Sanic -from sanic.request import File -from sanic import response from sanic_testing.testing import SanicTestClient -from strawberry.sanic.views import GraphQLView + import strawberry +from sanic import Sanic +from sanic.request import File from strawberry.file_uploads import Upload -from io import BytesIO from strawberry.sanic import utils -from typing import cast +from strawberry.sanic.views import GraphQLView @strawberry.type