From 6ee7a36f3462107f618712c6ba198662b3b2c168 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Sep 2025 13:02:40 +0100 Subject: [PATCH] Update FileStream API --- src/ahttpx/_content.py | 3 ++- src/ahttpx/_streams.py | 44 +++++++++++++++--------------------------- src/httpx/_content.py | 3 ++- src/httpx/_streams.py | 44 +++++++++++++++--------------------------- tests/test_streams.py | 6 +++--- 5 files changed, 39 insertions(+), 61 deletions(-) diff --git a/src/ahttpx/_content.py b/src/ahttpx/_content.py index e2c4aaa935..45774fbfb8 100644 --- a/src/ahttpx/_content.py +++ b/src/ahttpx/_content.py @@ -178,7 +178,8 @@ def size(self) -> int: return os.path.getsize(self._path) def encode(self) -> Stream: - return FileStream(self._path) + fin = open(self._path, 'rb') + return FileStream(self._path, fin) def content_type(self) -> str: _, ext = os.path.splitext(self._path) diff --git a/src/ahttpx/_streams.py b/src/ahttpx/_streams.py index d5e5ad0d4c..3cf779898c 100644 --- a/src/ahttpx/_streams.py +++ b/src/ahttpx/_streams.py @@ -1,4 +1,5 @@ import io +import typing import types import os @@ -75,32 +76,19 @@ def output_bytes(self) -> bytes: class FileStream(Stream): - def __init__(self, path): + def __init__(self, path: str, fin: typing.Any) -> None: self._path = path - self._fileobj = None - self._size = None + self._fin = fin async def read(self, size: int=-1) -> bytes: - if self._fileobj is None: - raise ValueError('I/O operation on unopened file') - return self._fileobj.read(size) - - async def open(self): - self._fileobj = open(self._path, 'rb') - self._size = os.path.getsize(self._path) - return self + return self._fin.read(size) async def close(self) -> None: - if self._fileobj is not None: - self._fileobj.close() + self._fin.close() @property def size(self) -> int | None: - return self._size - - async def __aenter__(self): - await self.open() - return self + return os.path.getsize(self._path) class HTTPStream(Stream): @@ -152,7 +140,7 @@ def __init__(self, form: list[tuple[str, str]], files: list[tuple[str, str]], bo # Mutable state... self._form_progress = list(self._form) self._files_progress = list(self._files) - self._filestream: FileStream | None = None + self._fin: typing.Any = None self._complete = False self._buffer = io.BytesIO() @@ -196,10 +184,10 @@ async def _read_next_section(self) -> bytes: f"\r\n" f"{value}\r\n" ).encode("utf-8") - elif self._files_progress and self._filestream is None: + elif self._files_progress and self._fin is None: # return start of a file item key, value = self._files_progress.pop(0) - self._filestream = await FileStream(value).open() + self._fin = open(value, 'rb') name = key.translate({10: "%0A", 13: "%0D", 34: "%22"}) filename = os.path.basename(value) return ( @@ -207,15 +195,15 @@ async def _read_next_section(self) -> bytes: f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n' f"\r\n" ).encode("utf-8") - elif self._filestream is not None: - chunk = await self._filestream.read(64*1024) + elif self._fin is not None: + chunk = await self._fin.read(64*1024) if chunk != b'': # return some bytes from file return chunk else: # return end of file item - await self._filestream.close() - self._filestream = None + await self._fin.close() + self._fin = None return b"\r\n" elif not self._complete: # return final section of multipart @@ -225,9 +213,9 @@ async def _read_next_section(self) -> bytes: return b"" async def close(self) -> None: - if self._filestream is not None: - await self._filestream.close() - self._filestream = None + if self._fin is not None: + await self._fin.close() + self._fin = None self._buffer.close() @property diff --git a/src/httpx/_content.py b/src/httpx/_content.py index e2c4aaa935..45774fbfb8 100644 --- a/src/httpx/_content.py +++ b/src/httpx/_content.py @@ -178,7 +178,8 @@ def size(self) -> int: return os.path.getsize(self._path) def encode(self) -> Stream: - return FileStream(self._path) + fin = open(self._path, 'rb') + return FileStream(self._path, fin) def content_type(self) -> str: _, ext = os.path.splitext(self._path) diff --git a/src/httpx/_streams.py b/src/httpx/_streams.py index 1fc6cde073..c88a63f6f8 100644 --- a/src/httpx/_streams.py +++ b/src/httpx/_streams.py @@ -1,4 +1,5 @@ import io +import typing import types import os @@ -75,32 +76,19 @@ def output_bytes(self) -> bytes: class FileStream(Stream): - def __init__(self, path): + def __init__(self, path: str, fin: typing.Any) -> None: self._path = path - self._fileobj = None - self._size = None + self._fin = fin def read(self, size: int=-1) -> bytes: - if self._fileobj is None: - raise ValueError('I/O operation on unopened file') - return self._fileobj.read(size) - - def open(self): - self._fileobj = open(self._path, 'rb') - self._size = os.path.getsize(self._path) - return self + return self._fin.read(size) def close(self) -> None: - if self._fileobj is not None: - self._fileobj.close() + self._fin.close() @property def size(self) -> int | None: - return self._size - - def __enter__(self): - self.open() - return self + return os.path.getsize(self._path) class HTTPStream(Stream): @@ -152,7 +140,7 @@ def __init__(self, form: list[tuple[str, str]], files: list[tuple[str, str]], bo # Mutable state... self._form_progress = list(self._form) self._files_progress = list(self._files) - self._filestream: FileStream | None = None + self._fin: typing.Any = None self._complete = False self._buffer = io.BytesIO() @@ -196,10 +184,10 @@ def _read_next_section(self) -> bytes: f"\r\n" f"{value}\r\n" ).encode("utf-8") - elif self._files_progress and self._filestream is None: + elif self._files_progress and self._fin is None: # return start of a file item key, value = self._files_progress.pop(0) - self._filestream = FileStream(value).open() + self._fin = open(value, 'rb') name = key.translate({10: "%0A", 13: "%0D", 34: "%22"}) filename = os.path.basename(value) return ( @@ -207,15 +195,15 @@ def _read_next_section(self) -> bytes: f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n' f"\r\n" ).encode("utf-8") - elif self._filestream is not None: - chunk = self._filestream.read(64*1024) + elif self._fin is not None: + chunk = self._fin.read(64*1024) if chunk != b'': # return some bytes from file return chunk else: # return end of file item - self._filestream.close() - self._filestream = None + self._fin.close() + self._fin = None return b"\r\n" elif not self._complete: # return final section of multipart @@ -225,9 +213,9 @@ def _read_next_section(self) -> bytes: return b"" def close(self) -> None: - if self._filestream is not None: - self._filestream.close() - self._filestream = None + if self._fin is not None: + self._fin.close() + self._fin = None self._buffer.close() @property diff --git a/tests/test_streams.py b/tests/test_streams.py index 8053761056..70c7244099 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -30,17 +30,17 @@ def test_filestream(tmp_path): path = tmp_path / "example.txt" path.write_bytes(b"hello world") - with httpx.FileStream(path) as s: + with httpx.File(path).encode() as s: assert s.size == 11 assert s.read() == b'hello world' - with httpx.FileStream(path) as s: + with httpx.File(path).encode() as s: assert s.read(5) == b'hello' assert s.read(5) == b' worl' assert s.read(5) == b'd' assert s.read(5) == b'' - with httpx.FileStream(path) as s: + with httpx.File(path).encode() as s: assert s.read(5) == b'hello'