From cb820a2a2f2475fde62083f04412d3f3d710ce6f Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Sun, 22 Sep 2024 15:49:21 +0200
Subject: [PATCH 1/3] Allow to match on client provided async iterable content
---
CHANGELOG.md | 7 +++-
README.md | 2 +-
pytest_httpx/_httpx_mock.py | 11 ++++--
pytest_httpx/_request_matcher.py | 5 +--
tests/test_httpx_async.py | 61 +++++++++++++++++++++++++++++++-
tests/test_httpx_sync.py | 57 +++++++++++++++++++++++++++++
6 files changed, 136 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 841a254..8f9f53a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.31.1] - 2024-09-22
+### Fixed
+- It is now possible to match on content provided as async iterable by the client.
+
## [0.31.0] - 2024-09-20
### Changed
- Tests will now fail at teardown by default if some requests were issued but were not matched.
@@ -337,7 +341,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- First release, should be considered as unstable for now as design might change.
-[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.0...HEAD
+[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.1...HEAD
+[0.31.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.0...v0.31.1
[0.31.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.30.0...v0.31.0
[0.30.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.29.0...v0.30.0
[0.29.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.28.0...v0.29.0
diff --git a/README.md b/README.md
index 3095b3b..4c73816 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 33ac08b..9e17d4c 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -1,8 +1,8 @@
import copy
import inspect
from operator import methodcaller
-from typing import Union, Optional, Callable, Any, NoReturn
-from collections.abc import Awaitable
+from typing import Union, Optional, Callable, Any, NoReturn, AsyncIterable
+from collections.abc import Awaitable, Iterable
import httpx
from pytest import Mark
@@ -157,6 +157,8 @@ def _handle_request(
real_transport: httpx.HTTPTransport,
request: httpx.Request,
) -> httpx.Response:
+ # Store the content in request for future matching
+ request.read()
self._requests.append((real_transport, request))
callback = self._get_callback(real_transport, request)
@@ -173,6 +175,11 @@ async def _handle_async_request(
real_transport: httpx.AsyncHTTPTransport,
request: httpx.Request,
) -> httpx.Response:
+ # Store the content in request for future matching
+ if isinstance(request.stream, AsyncIterable):
+ await request.aread()
+ else:
+ request.read()
self._requests.append((real_transport, request))
callback = self._get_callback(real_transport, request)
diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py
index 04c4ab4..1143ca3 100644
--- a/pytest_httpx/_request_matcher.py
+++ b/pytest_httpx/_request_matcher.py
@@ -98,11 +98,12 @@ def _headers_match(self, request: httpx.Request) -> bool:
def _content_match(self, request: httpx.Request) -> bool:
if self.content is None and self.json is None:
return True
+
if self.content is not None:
- return request.read() == self.content
+ return request.content == self.content
try:
# httpx._content.encode_json hard codes utf-8 encoding.
- return json.loads(request.read().decode("utf-8")) == self.json
+ return json.loads(request.content.decode("utf-8")) == self.json
except json.decoder.JSONDecodeError:
return False
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index 1d5d721..ee3cc61 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -2,6 +2,7 @@
import math
import re
import time
+from collections.abc import AsyncIterable
import httpx
import pytest
@@ -2007,7 +2008,7 @@ async def test_streams_are_not_cascading_resulting_in_maximum_recursion(
) -> None:
httpx_mock.add_response(json={"abc": "def"})
async with httpx.AsyncClient() as client:
- tasks = [client.get("https://example.com/") for _ in range(950)]
+ tasks = [client.get("https://test_url") for _ in range(950)]
await asyncio.gather(*tasks)
# No need to assert anything, this test case ensure that no error was raised by the gather
@@ -2033,3 +2034,61 @@ async def handle_async_request(
response = await client.post("https://test_url", content=b"This is the body")
assert response.read() == b""
assert response.headers["x-prefix"] == "test"
+
+
+@pytest.mark.asyncio
+async def test_response_selection_content_matching_with_async_iterable(
+ httpx_mock: HTTPXMock,
+) -> None:
+ httpx_mock.add_response(match_content=b"full content 1", content=b"matched 1")
+ httpx_mock.add_response(match_content=b"full content 2", content=b"matched 2")
+
+ async def stream_content_1() -> AsyncIterable[bytes]:
+ yield b"full"
+ yield b" "
+ yield b"content"
+ yield b" 1"
+
+ async def stream_content_2() -> AsyncIterable[bytes]:
+ yield b"full"
+ yield b" "
+ yield b"content"
+ yield b" 2"
+
+ async with httpx.AsyncClient() as client:
+ response_2 = await client.put("https://test_url", content=stream_content_2())
+ response_1 = await client.put("https://test_url", content=stream_content_1())
+ assert response_1.content == b"matched 1"
+ assert response_2.content == b"matched 2"
+
+
+@pytest.mark.asyncio
+async def test_request_selection_content_matching_with_async_iterable(
+ httpx_mock: HTTPXMock,
+) -> None:
+ httpx_mock.add_response(match_content=b"full content 1")
+ httpx_mock.add_response(match_content=b"full content 2")
+
+ async def stream_content_1() -> AsyncIterable[bytes]:
+ yield b"full"
+ yield b" "
+ yield b"content"
+ yield b" 1"
+
+ async def stream_content_2() -> AsyncIterable[bytes]:
+ yield b"full"
+ yield b" "
+ yield b"content"
+ yield b" 2"
+
+ async with httpx.AsyncClient() as client:
+ await client.put("https://test_url_2", content=stream_content_2())
+ await client.put("https://test_url_1", content=stream_content_1())
+ assert (
+ httpx_mock.get_request(match_content=b"full content 1").url
+ == "https://test_url_1"
+ )
+ assert (
+ httpx_mock.get_request(match_content=b"full content 2").url
+ == "https://test_url_2"
+ )
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index 3cbfb3f..a4c6047 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -1,4 +1,5 @@
import re
+from collections.abc import Iterable
from unittest.mock import ANY
import httpx
@@ -1718,3 +1719,59 @@ def handle_request(
response = client.post("https://test_url", content=b"This is the body")
assert response.read() == b""
assert response.headers["x-prefix"] == "test"
+
+
+def test_response_selection_content_matching_with_iterable(
+ httpx_mock: HTTPXMock,
+) -> None:
+ httpx_mock.add_response(match_content=b"full content 1", content=b"matched 1")
+ httpx_mock.add_response(match_content=b"full content 2", content=b"matched 2")
+
+ def stream_content_1() -> Iterable[bytes]:
+ yield b"full"
+ yield b" "
+ yield b"content"
+ yield b" 1"
+
+ def stream_content_2() -> Iterable[bytes]:
+ yield b"full"
+ yield b" "
+ yield b"content"
+ yield b" 2"
+
+ with httpx.Client() as client:
+ response_2 = client.put("https://test_url", content=stream_content_2())
+ response_1 = client.put("https://test_url", content=stream_content_1())
+ assert response_1.content == b"matched 1"
+ assert response_2.content == b"matched 2"
+
+
+def test_request_selection_content_matching_with_iterable(
+ httpx_mock: HTTPXMock,
+) -> None:
+ httpx_mock.add_response(match_content=b"full content 1")
+ httpx_mock.add_response(match_content=b"full content 2")
+
+ def stream_content_1() -> Iterable[bytes]:
+ yield b"full"
+ yield b" "
+ yield b"content"
+ yield b" 1"
+
+ def stream_content_2() -> Iterable[bytes]:
+ yield b"full"
+ yield b" "
+ yield b"content"
+ yield b" 2"
+
+ with httpx.Client() as client:
+ client.put("https://test_url_2", content=stream_content_2())
+ client.put("https://test_url_1", content=stream_content_1())
+ assert (
+ httpx_mock.get_request(match_content=b"full content 1").url
+ == "https://test_url_1"
+ )
+ assert (
+ httpx_mock.get_request(match_content=b"full content 2").url
+ == "https://test_url_2"
+ )
From 29eb9c31bdb83b7d7c0ea9a4434dd71df8586c1e Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Sun, 22 Sep 2024 15:49:31 +0200
Subject: [PATCH 2/3] Bump version
---
pytest_httpx/version.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pytest_httpx/version.py b/pytest_httpx/version.py
index 63fd052..91963a7 100644
--- a/pytest_httpx/version.py
+++ b/pytest_httpx/version.py
@@ -3,4 +3,4 @@
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
-__version__ = "0.31.0"
+__version__ = "0.31.1"
From 6eddd0565d4a592e58313376fb48352bbef66d45 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Sun, 22 Sep 2024 15:51:09 +0200
Subject: [PATCH 3/3] Remove dead code
---
pytest_httpx/_httpx_mock.py | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 9e17d4c..9465418 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -176,10 +176,7 @@ async def _handle_async_request(
request: httpx.Request,
) -> httpx.Response:
# Store the content in request for future matching
- if isinstance(request.stream, AsyncIterable):
- await request.aread()
- else:
- request.read()
+ await request.aread()
self._requests.append((real_transport, request))
callback = self._get_callback(real_transport, request)