-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix: Use anyio.SpooledTemporaryFile in UploadFile #2925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
1eb9655
873e2af
c87a49a
73b68cd
5bbb404
0756e42
43df948
72e2145
d3b0439
979840b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import typing | ||
| from contextlib import AsyncExitStack | ||
| from dataclasses import dataclass, field | ||
| from enum import Enum | ||
| from tempfile import SpooledTemporaryFile | ||
| from urllib.parse import unquote_plus | ||
|
|
||
| from anyio import SpooledTemporaryFile | ||
|
|
||
| from starlette.datastructures import FormData, Headers, UploadFile | ||
|
|
||
| if typing.TYPE_CHECKING: | ||
|
|
@@ -150,7 +152,7 @@ def __init__( | |
| self._charset = "" | ||
| self._file_parts_to_write: list[tuple[MultipartPart, bytes]] = [] | ||
| self._file_parts_to_finish: list[MultipartPart] = [] | ||
| self._files_to_close_on_error: list[SpooledTemporaryFile[bytes]] = [] | ||
| self._files_to_close_on_error: AsyncExitStack = AsyncExitStack() | ||
| self.max_part_size = max_part_size | ||
|
|
||
| def on_part_begin(self) -> None: | ||
|
|
@@ -206,9 +208,9 @@ def on_headers_finished(self) -> None: | |
| raise MultiPartException(f"Too many files. Maximum number of files is {self.max_files}.") | ||
| filename = _user_safe_decode(options[b"filename"], self._charset) | ||
| tempfile = SpooledTemporaryFile(max_size=self.spool_max_size) | ||
| self._files_to_close_on_error.append(tempfile) | ||
| self._files_to_close_on_error.push_async_callback(tempfile.aclose) | ||
| self._current_part.file = UploadFile( | ||
| file=tempfile, # type: ignore[arg-type] | ||
| file=tempfile, | ||
| size=0, | ||
| filename=filename, | ||
| headers=Headers(raw=self._current_part.item_headers), | ||
|
|
@@ -265,10 +267,9 @@ async def parse(self) -> FormData: | |
| await part.file.seek(0) | ||
| self._file_parts_to_write.clear() | ||
| self._file_parts_to_finish.clear() | ||
| except MultiPartException as exc: | ||
| except BaseException as exc: | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did this change?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I changed it to But perhaps I misunderstood — please let me know if I’m thinking about this the wrong way!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CancelledError being the most likely one |
||
| # Close all the files if there was an error. | ||
| for file in self._files_to_close_on_error: | ||
| file.close() | ||
11kkw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| await self._files_to_close_on_error.aclose() | ||
| raise exc | ||
|
|
||
| parser.finalize() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a base class analogous to BinaryIO? This was not a spoiled temporary file type before on purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because that shouldn't matter for the UploadFile.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that makes sense — the original use of BinaryIO was definitely meant to keep things implementation-agnostic, and I get why switching directly to SpooledTemporaryFile might feel too specific.
If you're open to it, I think one possible middle ground could be introducing a small AsyncFile protocol — something simple that defines the expected async methods (read, write, seek, etc). That way, we can preserve the flexibility while still ensuring everything stays fully async.
Happy to adjust the PR in that direction if it sounds reasonable to you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds reasonable, but maybe that structure already exists in anyio? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it seems like anyio provides a type called AsyncFile for async file operations
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AsyncFile is a concrete implementation not an abc, probably uh "AsyncFileIO[bytes]" protocol would make sense here.
If it's a protocol we can move it or have it in multiple places and have everything still work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, I don't think the file parameter should be exposed here. It seems to give users the power to customize the file, but in fact the other parts of UploadFile rely heavily on the implementation of file. If we need to make some abstractions to allow users to customize the file parsing results of multipart, we can use a design similar to baize. But at least in this PR, I think there is no problem with modifying the type in this way.