Skip to content

Commit 17c403c

Browse files
committed
use TypeGuard instead of runtime_checkable
@runtime_checkable doesn't work with trio async file since python3.12 because hasattr in isinstance was replaced with `inspect.getattr_static()`
1 parent 17fbdb2 commit 17c403c

3 files changed

Lines changed: 29 additions & 12 deletions

File tree

httpx/_content.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
from ._multipart import MultipartStream
1818
from ._types import (
1919
AsyncByteStream,
20-
AsyncFile,
2120
RequestContent,
2221
RequestData,
2322
RequestFiles,
2423
ResponseContent,
2524
SyncByteStream,
25+
is_async_readable_binary_file,
2626
)
2727
from ._utils import peek_filelike_length, primitive_value_to_str
2828

@@ -84,7 +84,7 @@ async def __aiter__(self) -> AsyncIterator[bytes]:
8484
while chunk:
8585
yield chunk
8686
chunk = await self._stream.aread(self.CHUNK_SIZE)
87-
elif isinstance(self._stream, AsyncFile):
87+
elif is_async_readable_binary_file(self._stream):
8888
chunk = await self._stream.read(self.CHUNK_SIZE)
8989
while chunk:
9090
yield chunk
@@ -133,7 +133,7 @@ def encode_content(
133133
return headers, IteratorByteStream(content) # type: ignore
134134

135135
elif isinstance(content, AsyncIterable):
136-
if isinstance(content, AsyncFile) and (
136+
if is_async_readable_binary_file(content) and (
137137
content_length_or_none := peek_filelike_length(content)
138138
):
139139
headers = {"Content-Length": str(content_length_or_none)}

httpx/_types.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
Type definitions for type checking purposes.
33
"""
44

5+
import inspect
56
from http.cookiejar import CookieJar
67
from typing import (
78
IO,
89
TYPE_CHECKING,
910
Any,
10-
AnyStr,
1111
AsyncIterable,
1212
AsyncIterator,
1313
Callable,
@@ -21,9 +21,10 @@
2121
Sequence,
2222
Tuple,
2323
Union,
24-
runtime_checkable,
2524
)
2625

26+
from typing_extensions import TypeGuard
27+
2728
if TYPE_CHECKING: # pragma: no cover
2829
from ._auth import Auth # noqa: F401
2930
from ._config import Proxy, Timeout # noqa: F401
@@ -117,8 +118,22 @@ async def aclose(self) -> None:
117118
pass
118119

119120

120-
@runtime_checkable
121-
class AsyncFile(Protocol):
122-
async def read(self, size: int = -1) -> AnyStr: ...
121+
class AsyncReadableBinaryFile(Protocol):
122+
async def __aiter__(self) -> AsyncIterator[bytes]: ...
123+
124+
async def read(self, size: int = -1) -> bytes: ...
123125

124126
def fileno(self) -> int: ...
127+
128+
129+
def is_async_readable_binary_file(fp: Any) -> TypeGuard[AsyncReadableBinaryFile]:
130+
return (
131+
isinstance(fp, AsyncIterable)
132+
and hasattr(fp, "read")
133+
and inspect.iscoroutinefunction(fp.read)
134+
and hasattr(fp, "fileno")
135+
and callable(fp.fileno)
136+
and not inspect.iscoroutinefunction(fp.fileno)
137+
and hasattr(fp, "mode")
138+
and "b" in fp.mode
139+
)

tests/test_content.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import httpx
1010
from httpx._content import AsyncIteratorByteStream
11-
from httpx._types import AsyncFile
11+
from httpx._types import AsyncReadableBinaryFile, is_async_readable_binary_file
1212

1313
method = "POST"
1414
url = "https://www.example.com"
@@ -537,7 +537,9 @@ def echo_request_content(request: httpx.Request) -> httpx.Response:
537537
to_upload = tmp_path / "upload.txt"
538538
to_upload.write_bytes(content_bytes)
539539

540-
async def checks(client: httpx.AsyncClient, async_file: AsyncFile) -> None:
540+
async def checks(
541+
client: httpx.AsyncClient, async_file: AsyncReadableBinaryFile
542+
) -> None:
541543
read_called = 0
542544
fileno_called = 0
543545
original_read = async_file.read
@@ -572,7 +574,7 @@ def mock_fileno(*args):
572574
transport=httpx.MockTransport(echo_request_content)
573575
) as client,
574576
):
575-
assert isinstance(async_file, AsyncFile)
577+
assert is_async_readable_binary_file(async_file)
576578
await checks(client, async_file)
577579

578580
if anyio_backend != "trio": # aiofiles doesn't work with trio
@@ -582,5 +584,5 @@ def mock_fileno(*args):
582584
transport=httpx.MockTransport(echo_request_content)
583585
) as client,
584586
):
585-
assert isinstance(aio_file, AsyncFile)
587+
assert is_async_readable_binary_file(aio_file)
586588
await checks(client, aio_file)

0 commit comments

Comments
 (0)