From b621cae23f3c0a1c78ecefdde1ec140de11e38ee Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Fri, 14 Jan 2022 10:48:44 -0800 Subject: [PATCH 01/18] Make the file argument to UploadFile required --- starlette/datastructures.py | 10 +++------- starlette/formparsers.py | 7 +++++-- starlette/requests.py | 6 ++++-- tests/test_datastructures.py | 14 -------------- tests/test_requests.py | 24 ++++++++++++++++++++++++ 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/starlette/datastructures.py b/starlette/datastructures.py index 2c5c4b016..0272206fd 100644 --- a/starlette/datastructures.py +++ b/starlette/datastructures.py @@ -1,4 +1,3 @@ -import tempfile import typing from collections import namedtuple from collections.abc import Sequence @@ -414,28 +413,25 @@ class UploadFile: An uploaded file included as part of the request data. """ - spool_max_size = 1024 * 1024 file: typing.BinaryIO headers: "Headers" def __init__( self, filename: str, - file: typing.Optional[typing.BinaryIO] = None, + file: typing.BinaryIO, content_type: str = "", *, headers: "typing.Optional[Headers]" = None, ) -> None: self.filename = filename self.content_type = content_type - if file is None: - self.file = tempfile.SpooledTemporaryFile(max_size=self.spool_max_size) # type: ignore # noqa: E501 - else: - self.file = file + self.file = file self.headers = headers or Headers() @property def _in_memory(self) -> bool: + # check for SpooledTemporaryFile._rolled rolled_to_disk = getattr(self.file, "_rolled", True) return not rolled_to_disk diff --git a/starlette/formparsers.py b/starlette/formparsers.py index decaf0bfd..0d6e65c6e 100644 --- a/starlette/formparsers.py +++ b/starlette/formparsers.py @@ -1,5 +1,6 @@ import typing from enum import Enum +from tempfile import SpooledTemporaryFile from urllib.parse import unquote_plus from starlette.datastructures import FormData, Headers, UploadFile @@ -153,7 +154,7 @@ def on_end(self) -> None: message = (MultiPartMessage.END, b"") self.messages.append(message) - async def parse(self) -> FormData: + async def parse(self, max_file_size_in_memory: int) -> FormData: # Parse the Content-Type header to get the multipart boundary. content_type, params = parse_options_header(self.headers["Content-Type"]) charset = params.get(b"charset", "utf-8") @@ -211,11 +212,13 @@ async def parse(self) -> FormData: header_field = b"" header_value = b"" elif message_type == MultiPartMessage.HEADERS_FINISHED: - disposition, options = parse_options_header(content_disposition) + _, options = parse_options_header(content_disposition) field_name = _user_safe_decode(options[b"name"], charset) if b"filename" in options: filename = _user_safe_decode(options[b"filename"], charset) + file_ = SpooledTemporaryFile(max_size=max_file_size_in_memory) file = UploadFile( + file=file_, # type: ignore[arg-type] filename=filename, content_type=content_type.decode("latin-1"), headers=Headers(raw=item_headers), diff --git a/starlette/requests.py b/starlette/requests.py index a33367e1d..5ee4a3ef8 100644 --- a/starlette/requests.py +++ b/starlette/requests.py @@ -239,7 +239,7 @@ async def json(self) -> typing.Any: self._json = json.loads(body) return self._json - async def form(self) -> FormData: + async def form(self, max_file_size_in_memory: int = 1024 * 1024) -> FormData: if not hasattr(self, "_form"): assert ( parse_options_header is not None @@ -248,7 +248,9 @@ async def form(self) -> FormData: content_type, options = parse_options_header(content_type_header) if content_type == b"multipart/form-data": multipart_parser = MultiPartParser(self.headers, self.stream()) - self._form = await multipart_parser.parse() + self._form = await multipart_parser.parse( + max_file_size_in_memory=max_file_size_in_memory + ) elif content_type == b"application/x-www-form-urlencoded": form_parser = FormParser(self.headers, self.stream()) self._form = await form_parser.parse() diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index b110aa8bd..34aeec560 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -213,20 +213,6 @@ def test_queryparams(): assert QueryParams(q) == q -class BigUploadFile(UploadFile): - spool_max_size = 1024 - - -@pytest.mark.anyio -async def test_upload_file(): - big_file = BigUploadFile("big-file") - await big_file.write(b"big-data" * 512) - await big_file.write(b"big-data") - await big_file.seek(0) - assert await big_file.read(1024) == b"big-data" * 128 - await big_file.close() - - @pytest.mark.anyio async def test_upload_file_file_input(): """Test passing file/stream into the UploadFile constructor""" diff --git a/tests/test_requests.py b/tests/test_requests.py index d7c69fbeb..9277041b6 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,5 +1,7 @@ +from tempfile import SpooledTemporaryFile import anyio import pytest +from starlette.datastructures import UploadFile from starlette.requests import ClientDisconnect, Request, State from starlette.responses import JSONResponse, Response @@ -118,6 +120,28 @@ async def app(scope, receive, send): assert response.json() == {"form": {"abc": "123 @"}} +def test_request_multipart_file_max_size(test_client_factory): + async def app(scope, receive, send): + request = Request(scope, receive) + form = await request.form(max_file_size_in_memory=1) + uploadfile = form["file"] + assert isinstance(uploadfile, UploadFile) + file = uploadfile.file + response = JSONResponse({"rolled": getattr(file, "_rolled", True)}) + await response(scope, receive, send) + + client = test_client_factory(app) + + response = client.post("/", files=[("file", ("file.txt", b"more than 1 byte"))]) + assert response.status_code == 200, response.content + assert response.json() == {"rolled": True} + + response = client.post("/", files=[("file", ("file.txt", b"1"))]) + assert response.status_code == 200, response.content + assert response.json() == {"rolled": False} + + + def test_request_body_then_stream(test_client_factory): async def app(scope, receive, send): request = Request(scope, receive) From fab8d41153d87775c20d59da7b9e1c27866a9907 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Fri, 14 Jan 2022 10:50:21 -0800 Subject: [PATCH 02/18] lint --- tests/test_requests.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index 9277041b6..325e1d5c2 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,8 +1,7 @@ -from tempfile import SpooledTemporaryFile import anyio import pytest -from starlette.datastructures import UploadFile +from starlette.datastructures import UploadFile from starlette.requests import ClientDisconnect, Request, State from starlette.responses import JSONResponse, Response @@ -141,7 +140,6 @@ async def app(scope, receive, send): assert response.json() == {"rolled": False} - def test_request_body_then_stream(test_client_factory): async def app(scope, receive, send): request = Request(scope, receive) From d5d339f453c7c4d9f09845b27df2b645817dfe07 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Fri, 14 Jan 2022 10:54:22 -0800 Subject: [PATCH 03/18] add test for rolled uploadfile --- tests/test_datastructures.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 34aeec560..1b74b062d 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,4 +1,6 @@ import io +from tempfile import SpooledTemporaryFile +from typing import BinaryIO import pytest @@ -225,6 +227,19 @@ async def test_upload_file_file_input(): assert await file.read() == b"data and more data!" +@pytest.mark.anyio +async def test_uploadfile_rolled(): + stream: BinaryIO = SpooledTemporaryFile(max_size=1) # type: ignore[assignment] + file = UploadFile(filename="file", file=stream) + assert await file.read() == b"" + await file.write(b"data") + assert await file.read() == b"" + await file.seek(0) + assert await file.read() == b"data" + await file.close() + + + def test_formdata(): stream = io.BytesIO(b"data") upload = UploadFile(filename="file", file=stream) From ab973259f3a7b0aa04f20d34cf19b5f2926c2d24 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Fri, 14 Jan 2022 10:54:58 -0800 Subject: [PATCH 04/18] add tests for rolled/unrolled uploadfile --- tests/test_datastructures.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 1b74b062d..bde6a67dc 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -239,6 +239,17 @@ async def test_uploadfile_rolled(): await file.close() +@pytest.mark.anyio +async def test_uploadfile_not_rolled(): + stream: BinaryIO = SpooledTemporaryFile(max_size=1024) # type: ignore[assignment] + file = UploadFile(filename="file", file=stream) + assert await file.read() == b"" + await file.write(b"data") + assert await file.read() == b"" + await file.seek(0) + assert await file.read() == b"data" + await file.close() + def test_formdata(): stream = io.BytesIO(b"data") From 3bf4d153b75eb0d74f937830362a6eb1c3c2504d Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Fri, 14 Jan 2022 10:58:26 -0800 Subject: [PATCH 05/18] close form --- tests/test_requests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_requests.py b/tests/test_requests.py index 325e1d5c2..ed2262c8a 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -127,6 +127,7 @@ async def app(scope, receive, send): assert isinstance(uploadfile, UploadFile) file = uploadfile.file response = JSONResponse({"rolled": getattr(file, "_rolled", True)}) + await form.close() await response(scope, receive, send) client = test_client_factory(app) From a7b8e05371e916b106001a8dc071ec78f3a34c1b Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Fri, 14 Jan 2022 10:58:34 -0800 Subject: [PATCH 06/18] close request --- tests/test_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index ed2262c8a..f6fd7d1ba 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -127,7 +127,7 @@ async def app(scope, receive, send): assert isinstance(uploadfile, UploadFile) file = uploadfile.file response = JSONResponse({"rolled": getattr(file, "_rolled", True)}) - await form.close() + await request.close() await response(scope, receive, send) client = test_client_factory(app) From 315b292a404eb5b8e05f517cb082be9cfdc2044f Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 17 Jan 2022 10:46:40 -0800 Subject: [PATCH 07/18] remove max_file_size arg in favor of class var --- starlette/formparsers.py | 6 ++++-- starlette/requests.py | 6 ++---- tests/test_requests.py | 23 ----------------------- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/starlette/formparsers.py b/starlette/formparsers.py index 0d6e65c6e..72b73b851 100644 --- a/starlette/formparsers.py +++ b/starlette/formparsers.py @@ -112,6 +112,8 @@ async def parse(self) -> FormData: class MultiPartParser: + max_file_size = 1024 * 1024 + def __init__( self, headers: Headers, stream: typing.AsyncGenerator[bytes, None] ) -> None: @@ -154,7 +156,7 @@ def on_end(self) -> None: message = (MultiPartMessage.END, b"") self.messages.append(message) - async def parse(self, max_file_size_in_memory: int) -> FormData: + async def parse(self) -> FormData: # Parse the Content-Type header to get the multipart boundary. content_type, params = parse_options_header(self.headers["Content-Type"]) charset = params.get(b"charset", "utf-8") @@ -216,7 +218,7 @@ async def parse(self, max_file_size_in_memory: int) -> FormData: field_name = _user_safe_decode(options[b"name"], charset) if b"filename" in options: filename = _user_safe_decode(options[b"filename"], charset) - file_ = SpooledTemporaryFile(max_size=max_file_size_in_memory) + file_ = SpooledTemporaryFile(max_size=self.max_file_size) file = UploadFile( file=file_, # type: ignore[arg-type] filename=filename, diff --git a/starlette/requests.py b/starlette/requests.py index 5ee4a3ef8..a33367e1d 100644 --- a/starlette/requests.py +++ b/starlette/requests.py @@ -239,7 +239,7 @@ async def json(self) -> typing.Any: self._json = json.loads(body) return self._json - async def form(self, max_file_size_in_memory: int = 1024 * 1024) -> FormData: + async def form(self) -> FormData: if not hasattr(self, "_form"): assert ( parse_options_header is not None @@ -248,9 +248,7 @@ async def form(self, max_file_size_in_memory: int = 1024 * 1024) -> FormData: content_type, options = parse_options_header(content_type_header) if content_type == b"multipart/form-data": multipart_parser = MultiPartParser(self.headers, self.stream()) - self._form = await multipart_parser.parse( - max_file_size_in_memory=max_file_size_in_memory - ) + self._form = await multipart_parser.parse() elif content_type == b"application/x-www-form-urlencoded": form_parser = FormParser(self.headers, self.stream()) self._form = await form_parser.parse() diff --git a/tests/test_requests.py b/tests/test_requests.py index f6fd7d1ba..d7c69fbeb 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,7 +1,6 @@ import anyio import pytest -from starlette.datastructures import UploadFile from starlette.requests import ClientDisconnect, Request, State from starlette.responses import JSONResponse, Response @@ -119,28 +118,6 @@ async def app(scope, receive, send): assert response.json() == {"form": {"abc": "123 @"}} -def test_request_multipart_file_max_size(test_client_factory): - async def app(scope, receive, send): - request = Request(scope, receive) - form = await request.form(max_file_size_in_memory=1) - uploadfile = form["file"] - assert isinstance(uploadfile, UploadFile) - file = uploadfile.file - response = JSONResponse({"rolled": getattr(file, "_rolled", True)}) - await request.close() - await response(scope, receive, send) - - client = test_client_factory(app) - - response = client.post("/", files=[("file", ("file.txt", b"more than 1 byte"))]) - assert response.status_code == 200, response.content - assert response.json() == {"rolled": True} - - response = client.post("/", files=[("file", ("file.txt", b"1"))]) - assert response.status_code == 200, response.content - assert response.json() == {"rolled": False} - - def test_request_body_then_stream(test_client_factory): async def app(scope, receive, send): request = Request(scope, receive) From c012aa118726a58248049d804cfff8b191a51af5 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 18 Jan 2022 10:52:39 -0800 Subject: [PATCH 08/18] make file the only required argument --- starlette/datastructures.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/starlette/datastructures.py b/starlette/datastructures.py index 0272206fd..32df8d29f 100644 --- a/starlette/datastructures.py +++ b/starlette/datastructures.py @@ -414,14 +414,13 @@ class UploadFile: """ file: typing.BinaryIO - headers: "Headers" def __init__( self, - filename: str, file: typing.BinaryIO, - content_type: str = "", *, + filename: typing.Optional[str] = None, + content_type: typing.Optional[str] = None, headers: "typing.Optional[Headers]" = None, ) -> None: self.filename = filename From 1b9568e956581437e8f22ff9dcb516d03fe3636c Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 19 Jan 2022 08:59:15 -0800 Subject: [PATCH 09/18] pull content-type from headers --- starlette/datastructures.py | 8 ++++---- starlette/formparsers.py | 7 +------ tests/test_formparsers.py | 6 +++--- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/starlette/datastructures.py b/starlette/datastructures.py index 32df8d29f..c63cb41fa 100644 --- a/starlette/datastructures.py +++ b/starlette/datastructures.py @@ -413,21 +413,21 @@ class UploadFile: An uploaded file included as part of the request data. """ - file: typing.BinaryIO - def __init__( self, file: typing.BinaryIO, *, filename: typing.Optional[str] = None, - content_type: typing.Optional[str] = None, headers: "typing.Optional[Headers]" = None, ) -> None: self.filename = filename - self.content_type = content_type self.file = file self.headers = headers or Headers() + @property + def content_type(self) -> typing.Optional[str]: + return self.headers.get("content-type", None) + @property def _in_memory(self) -> bool: # check for SpooledTemporaryFile._rolled diff --git a/starlette/formparsers.py b/starlette/formparsers.py index 72b73b851..25e58d1b6 100644 --- a/starlette/formparsers.py +++ b/starlette/formparsers.py @@ -158,7 +158,7 @@ def on_end(self) -> None: async def parse(self) -> FormData: # Parse the Content-Type header to get the multipart boundary. - content_type, params = parse_options_header(self.headers["Content-Type"]) + _, params = parse_options_header(self.headers["Content-Type"]) charset = params.get(b"charset", "utf-8") if type(charset) == bytes: charset = charset.decode("latin-1") @@ -181,7 +181,6 @@ async def parse(self) -> FormData: header_field = b"" header_value = b"" content_disposition = None - content_type = b"" field_name = "" data = b"" file: typing.Optional[UploadFile] = None @@ -197,7 +196,6 @@ async def parse(self) -> FormData: for message_type, message_bytes in messages: if message_type == MultiPartMessage.PART_BEGIN: content_disposition = None - content_type = b"" data = b"" item_headers = [] elif message_type == MultiPartMessage.HEADER_FIELD: @@ -208,8 +206,6 @@ async def parse(self) -> FormData: field = header_field.lower() if field == b"content-disposition": content_disposition = header_value - elif field == b"content-type": - content_type = header_value item_headers.append((field, header_value)) header_field = b"" header_value = b"" @@ -222,7 +218,6 @@ async def parse(self) -> FormData: file = UploadFile( file=file_, # type: ignore[arg-type] filename=filename, - content_type=content_type.decode("latin-1"), headers=Headers(raw=item_headers), ) else: diff --git a/tests/test_formparsers.py b/tests/test_formparsers.py index 05f0f053c..915666087 100644 --- a/tests/test_formparsers.py +++ b/tests/test_formparsers.py @@ -108,7 +108,7 @@ def test_multipart_request_files(tmpdir, test_client_factory): "test": { "filename": "test.txt", "content": "", - "content_type": "", + "content_type": None, } } @@ -148,7 +148,7 @@ def test_multipart_request_multiple_files(tmpdir, test_client_factory): "test1": { "filename": "test1.txt", "content": "", - "content_type": "", + "content_type": None, }, "test2": { "filename": "test2.txt", @@ -216,7 +216,7 @@ def test_multi_items(tmpdir, test_client_factory): { "filename": "test1.txt", "content": "", - "content_type": "", + "content_type": None, }, { "filename": "test2.txt", From 655c4be5e00beccb9244b08206c8415a982b01bd Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 19 Jan 2022 09:03:52 -0800 Subject: [PATCH 10/18] rename local from file_ to tempfile --- starlette/formparsers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/starlette/formparsers.py b/starlette/formparsers.py index 25e58d1b6..5ba0f90ec 100644 --- a/starlette/formparsers.py +++ b/starlette/formparsers.py @@ -214,9 +214,9 @@ async def parse(self) -> FormData: field_name = _user_safe_decode(options[b"name"], charset) if b"filename" in options: filename = _user_safe_decode(options[b"filename"], charset) - file_ = SpooledTemporaryFile(max_size=self.max_file_size) + tempfile = SpooledTemporaryFile(max_size=self.max_file_size) file = UploadFile( - file=file_, # type: ignore[arg-type] + file=tempfile, # type: ignore[arg-type] filename=filename, headers=Headers(raw=item_headers), ) From 1c13abb5313d9f122b012a873b428f5d02d77999 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 27 Jan 2022 09:45:57 -0600 Subject: [PATCH 11/18] join tests into one via parametrization --- tests/test_datastructures.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index bde6a67dc..2ea9a72ed 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -228,26 +228,22 @@ async def test_upload_file_file_input(): @pytest.mark.anyio -async def test_uploadfile_rolled(): - stream: BinaryIO = SpooledTemporaryFile(max_size=1) # type: ignore[assignment] +@pytest.mark.parametrize( + "max_size", [1, 1024] +) +async def test_uploadfile_rolling(max_size: int) -> None: + """Test that we can r/w to a file before and after it rolls to disk""" + stream: BinaryIO = SpooledTemporaryFile(max_size=max_size) # type: ignore[assignment] file = UploadFile(filename="file", file=stream) assert await file.read() == b"" await file.write(b"data") assert await file.read() == b"" await file.seek(0) assert await file.read() == b"data" - await file.close() - - -@pytest.mark.anyio -async def test_uploadfile_not_rolled(): - stream: BinaryIO = SpooledTemporaryFile(max_size=1024) # type: ignore[assignment] - file = UploadFile(filename="file", file=stream) - assert await file.read() == b"" - await file.write(b"data") + await file.write(b" more") assert await file.read() == b"" await file.seek(0) - assert await file.read() == b"data" + assert await file.read() == b"data more" await file.close() From 6f6b3a0fe3bac9c0e8e9fe90d139894362f35830 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 27 Jan 2022 09:47:14 -0600 Subject: [PATCH 12/18] clarify docstring and parametrization ids --- tests/test_datastructures.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 2ea9a72ed..0dbbd548f 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -229,10 +229,13 @@ async def test_upload_file_file_input(): @pytest.mark.anyio @pytest.mark.parametrize( - "max_size", [1, 1024] + "max_size", [1, 1024], + ids=["rolled", "unrolled"] ) async def test_uploadfile_rolling(max_size: int) -> None: - """Test that we can r/w to a file before and after it rolls to disk""" + """Test that we can r/w to a SpooledTemporaryFile + managed by UploadFile before and after it rolls to disk + """ stream: BinaryIO = SpooledTemporaryFile(max_size=max_size) # type: ignore[assignment] file = UploadFile(filename="file", file=stream) assert await file.read() == b"" From 1e51edd30da20ad96e3c08a97a299b0d0c8d17b0 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 31 Jan 2022 10:44:48 -0600 Subject: [PATCH 13/18] lint --- tests/test_datastructures.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 0dbbd548f..2c8a5d9bf 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -228,10 +228,7 @@ async def test_upload_file_file_input(): @pytest.mark.anyio -@pytest.mark.parametrize( - "max_size", [1, 1024], - ids=["rolled", "unrolled"] -) +@pytest.mark.parametrize("max_size", [1, 1024], ids=["rolled", "unrolled"]) async def test_uploadfile_rolling(max_size: int) -> None: """Test that we can r/w to a SpooledTemporaryFile managed by UploadFile before and after it rolls to disk From 75ca723c8b6bda5139029a647f236c5c6d0da3e1 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 31 Jan 2022 10:50:56 -0600 Subject: [PATCH 14/18] wrap line --- tests/test_datastructures.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 2c8a5d9bf..7067392c3 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -233,7 +233,9 @@ async def test_uploadfile_rolling(max_size: int) -> None: """Test that we can r/w to a SpooledTemporaryFile managed by UploadFile before and after it rolls to disk """ - stream: BinaryIO = SpooledTemporaryFile(max_size=max_size) # type: ignore[assignment] + stream: BinaryIO = SpooledTemporaryFile( # type: ignore[assignment] + max_size=max_size + ) file = UploadFile(filename="file", file=stream) assert await file.read() == b"" await file.write(b"data") From 1baf994fc396763fdf3a7de2f2275aab89abe7c4 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:16:02 -0500 Subject: [PATCH 15/18] Update test_formparsers.py --- tests/test_formparsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_formparsers.py b/tests/test_formparsers.py index 0c615175c..4792424ab 100644 --- a/tests/test_formparsers.py +++ b/tests/test_formparsers.py @@ -220,7 +220,7 @@ def test_multi_items(tmpdir, test_client_factory): "abc", { "filename": "test1.txt", - "content": "" + "content": "", "content_type": "text/plain", }, { From 1fb8b088ccf65fa0be829944588fc72ffe07fe88 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:18:59 -0500 Subject: [PATCH 16/18] Update datastructures.py --- starlette/datastructures.py | 1 - 1 file changed, 1 deletion(-) diff --git a/starlette/datastructures.py b/starlette/datastructures.py index 71717973f..04c13b816 100644 --- a/starlette/datastructures.py +++ b/starlette/datastructures.py @@ -436,7 +436,6 @@ def __init__( ) -> None: self.filename = filename self.file = file - self.content_type = content_type self.headers = headers or Headers() @property From d9cfaa22edee1814d1d5871d609d9307ed0871d1 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sat, 4 Feb 2023 02:06:40 -0800 Subject: [PATCH 17/18] Update starlette/datastructures.py Co-authored-by: Marcelo Trylesinski --- starlette/datastructures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/starlette/datastructures.py b/starlette/datastructures.py index eee3834e0..6d2264b71 100644 --- a/starlette/datastructures.py +++ b/starlette/datastructures.py @@ -439,8 +439,8 @@ def __init__( self.headers = headers or Headers() @property - def content_type(self) -> typing.Optional[str]: - return self.headers.get("content-type", None) + def content_type(self) -> str: + return self.headers.get("content-type", "") @property def _in_memory(self) -> bool: From 9831c89a398ec633b2b6d75be563446040061f07 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 4 Feb 2023 18:53:26 +0100 Subject: [PATCH 18/18] Use default None instead of empty string on UploadFile.content_type if missing --- starlette/datastructures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/starlette/datastructures.py b/starlette/datastructures.py index 6d2264b71..eee3834e0 100644 --- a/starlette/datastructures.py +++ b/starlette/datastructures.py @@ -439,8 +439,8 @@ def __init__( self.headers = headers or Headers() @property - def content_type(self) -> str: - return self.headers.get("content-type", "") + def content_type(self) -> typing.Optional[str]: + return self.headers.get("content-type", None) @property def _in_memory(self) -> bool: