|
1 | 1 | import io |
2 | 2 | import typing |
3 | 3 |
|
| 4 | +import aiofiles |
| 5 | +import anyio |
4 | 6 | import pytest |
| 7 | +import trio |
5 | 8 |
|
6 | 9 | import httpx |
| 10 | +from httpx._content import AsyncIteratorByteStream |
| 11 | +from httpx._types import AsyncFile |
7 | 12 |
|
8 | 13 | method = "POST" |
9 | 14 | url = "https://www.example.com" |
@@ -516,3 +521,66 @@ def test_allow_nan_false(): |
516 | 521 | ValueError, match="Out of range float values are not JSON compliant" |
517 | 522 | ): |
518 | 523 | httpx.Response(200, json=data_with_inf) |
| 524 | + |
| 525 | + |
| 526 | +@pytest.mark.parametrize("client_method", ["put", "post"]) |
| 527 | +@pytest.mark.anyio |
| 528 | +async def test_chunked_async_file_content( |
| 529 | + tmp_path, anyio_backend, monkeypatch, client_method |
| 530 | +): |
| 531 | + total_chunks = 3 |
| 532 | + |
| 533 | + def echo_request_content(request: httpx.Request) -> httpx.Response: |
| 534 | + return httpx.Response(200, content=request.content) |
| 535 | + |
| 536 | + content_bytes = b"".join([b"a" * AsyncIteratorByteStream.CHUNK_SIZE] * total_chunks) |
| 537 | + to_upload = tmp_path / "upload.txt" |
| 538 | + to_upload.write_bytes(content_bytes) |
| 539 | + |
| 540 | + async def checks(client: httpx.AsyncClient, async_file: AsyncFile) -> None: |
| 541 | + read_called = 0 |
| 542 | + fileno_called = 0 |
| 543 | + original_read = async_file.read |
| 544 | + original_fileno = async_file.fileno |
| 545 | + |
| 546 | + async def mock_read(*args, **kwargs): |
| 547 | + nonlocal read_called |
| 548 | + read_called += 1 |
| 549 | + return await original_read(*args, **kwargs) |
| 550 | + |
| 551 | + def mock_fileno(*args): |
| 552 | + nonlocal fileno_called |
| 553 | + fileno_called += 1 |
| 554 | + return original_fileno(*args) |
| 555 | + |
| 556 | + monkeypatch.setattr(async_file, "read", mock_read) |
| 557 | + monkeypatch.setattr(async_file, "fileno", mock_fileno) |
| 558 | + response = await getattr(client, client_method)( |
| 559 | + url="http://127.0.0.1:8000/", content=async_file |
| 560 | + ) |
| 561 | + assert response.status_code == 200 |
| 562 | + assert response.content == content_bytes |
| 563 | + assert response.request.headers["Content-Length"] == str(len(content_bytes)) |
| 564 | + assert read_called == total_chunks + 1 |
| 565 | + assert fileno_called == 1 |
| 566 | + |
| 567 | + async with ( |
| 568 | + await anyio.open_file(to_upload, mode="rb") |
| 569 | + if anyio_backend != "trio" |
| 570 | + else await trio.open_file(to_upload, mode="rb") as async_file, |
| 571 | + httpx.AsyncClient( |
| 572 | + transport=httpx.MockTransport(echo_request_content) |
| 573 | + ) as client, |
| 574 | + ): |
| 575 | + assert isinstance(async_file, AsyncFile) |
| 576 | + await checks(client, async_file) |
| 577 | + |
| 578 | + if anyio_backend != "trio": # aiofiles doesn't work with trio |
| 579 | + async with ( |
| 580 | + aiofiles.open(to_upload, mode="rb") as aio_file, |
| 581 | + httpx.AsyncClient( |
| 582 | + transport=httpx.MockTransport(echo_request_content) |
| 583 | + ) as client, |
| 584 | + ): |
| 585 | + assert isinstance(aio_file, AsyncFile) |
| 586 | + await checks(client, aio_file) |
0 commit comments