Skip to content

Commit d29a5a8

Browse files
authored
Merge pull request Colin-b#23 from Colin-b/develop
Release 0.5.0
2 parents 4c8de2e + 864313b commit d29a5a8

File tree

10 files changed

+301
-97
lines changed

10 files changed

+301
-97
lines changed

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [0.5.0] - 2020-07-31
10+
### Changed
11+
- requires [`pytest`](https://docs.pytest.org/en/latest/) 6.
12+
- `assert_and_reset` mock method has been renamed to `reset` and now takes a boolean parameter to specify if assertion should be performed.
13+
14+
### Added
15+
- It is now possible to disable the assertion that all registered responses were requested thanks to the `assert_all_responses_were_requested` fixture. Refer to documentation for more details.
16+
17+
### Removed
18+
- It is not possible to provide an URL encoded response anymore by providing a dictionary in `data` parameter.
19+
920
## [0.4.0] - 2020-06-05
1021
### Changed
1122
- `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture does not need to be explicitly imported anymore (many thanks to [`Thomas LÉVEIL`](https://github.com/thomasleveil)).
@@ -73,7 +84,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7384
### Added
7485
- First release, should be considered as unstable for now as design might change.
7586

76-
[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.4.0...HEAD
87+
[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.5.0...HEAD
88+
[0.5.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.4.0...v0.5.0
7789
[0.4.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.3.0...v0.4.0
7890
[0.3.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.2.1...v0.3.0
7991
[0.2.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.2.0...v0.2.1

README.md

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<a href="https://travis-ci.com/Colin-b/pytest_httpx"><img alt="Build status" src="https://api.travis-ci.com/Colin-b/pytest_httpx.svg?branch=master"></a>
66
<a href="https://travis-ci.com/Colin-b/pytest_httpx"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
77
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
8-
<a href="https://travis-ci.com/Colin-b/pytest_httpx"><img alt="Number of tests" src="https://img.shields.io/badge/tests-72 passed-blue"></a>
8+
<a href="https://travis-ci.com/Colin-b/pytest_httpx"><img alt="Number of tests" src="https://img.shields.io/badge/tests-79 passed-blue"></a>
99
<a href="https://pypi.org/project/pytest-httpx/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/pytest_httpx"></a>
1010
</p>
1111

@@ -30,7 +30,6 @@ Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixt
3030

3131
You can register responses for both sync and async [`HTTPX`](https://www.python-httpx.org) requests.
3232

33-
3433
```python
3534
import pytest
3635
import httpx
@@ -53,6 +52,16 @@ async def test_something_async(httpx_mock):
5352

5453
If all registered responses are not sent back during test execution, the test case will fail at teardown.
5554

55+
This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture:
56+
57+
```python
58+
import pytest
59+
60+
@pytest.fixture
61+
def assert_all_responses_were_requested() -> bool:
62+
return False
63+
```
64+
5665
Default response is a HTTP/1.1 200 (OK) without any body.
5766

5867
### How response is selected
@@ -208,9 +217,38 @@ def test_bytes_body(httpx_mock: HTTPXMock):
208217

209218
```
210219

220+
### Reply by streaming data
221+
222+
Use `data` parameter to stream chunks that you specify.
223+
As long as your data is an iterable it will stream your data.
224+
225+
```python
226+
import httpx
227+
import pytest
228+
from pytest_httpx import HTTPXMock
229+
230+
231+
def test_sync_streaming(httpx_mock: HTTPXMock):
232+
httpx_mock.add_response(data=[b"part 1", b"part 2"])
233+
234+
with httpx.Client() as client:
235+
with client.stream(method="GET", url="http://test_url") as response:
236+
assert list(response.iter_raw()) == [b"part 1", b"part 2"]
237+
238+
239+
@pytest.mark.asyncio
240+
async def test_async_streaming(httpx_mock: HTTPXMock):
241+
httpx_mock.add_response(data=[b"part 1", b"part 2"])
242+
243+
async with httpx.AsyncClient() as client:
244+
async with client.stream(method="GET", url="http://test_url") as response:
245+
assert list(response.iter_raw()) == [b"part 1", b"part 2"]
246+
247+
```
248+
211249
### Add multipart response
212250

213-
Use `data` parameter as a dictionary or `files` parameter (or both) to send multipart response.
251+
Use `files` parameter (and optionally `data` parameter as a dictionary) to send multipart response.
214252

215253
You can specify `boundary` parameter to specify the multipart boundary to use.
216254

@@ -298,6 +336,16 @@ Callback should expect at least two parameters:
298336

299337
If all callbacks are not executed during test execution, the test case will fail at teardown.
300338

339+
This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture:
340+
341+
```python
342+
import pytest
343+
344+
@pytest.fixture
345+
def assert_all_responses_were_requested() -> bool:
346+
return False
347+
```
348+
301349
### Dynamic responses
302350

303351
Callback should return a httpcore response (as a tuple), you can use `pytest_httpx.to_response` function to create such a tuple.

pytest_httpx/__init__.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
import httpx
22
import pytest
33

4-
from pytest_httpx._httpx_mock import HTTPXMock, to_response, _PytestSyncTransport, _PytestAsyncTransport
4+
from pytest_httpx._httpx_mock import (
5+
HTTPXMock,
6+
to_response,
7+
_PytestSyncTransport,
8+
_PytestAsyncTransport,
9+
)
510
from pytest_httpx.version import __version__
611

712

813
@pytest.fixture
9-
def httpx_mock(monkeypatch) -> HTTPXMock:
14+
def assert_all_responses_were_requested() -> bool:
15+
return True
16+
17+
18+
@pytest.fixture
19+
def httpx_mock(monkeypatch, assert_all_responses_were_requested: bool) -> HTTPXMock:
1020
mock = HTTPXMock()
1121
# Mock synchronous requests
1222
monkeypatch.setattr(
13-
httpx.Client, "transport_for_url", lambda self, url: _PytestSyncTransport(mock),
23+
httpx.Client, "transport_for_url", lambda self, url: _PytestSyncTransport(mock)
1424
)
1525
# Mock asynchronous requests
1626
monkeypatch.setattr(
@@ -19,4 +29,4 @@ def httpx_mock(monkeypatch) -> HTTPXMock:
1929
lambda self, url: _PytestAsyncTransport(mock),
2030
)
2131
yield mock
22-
mock.assert_and_reset()
32+
mock.reset(assert_all_responses_were_requested)

pytest_httpx/_httpx_internals.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import Union, Any, Iterator, AsyncIterator
2+
from json import dumps
3+
4+
import httpcore
5+
6+
7+
class ByteStream(httpcore.AsyncByteStream, httpcore.SyncByteStream):
8+
def __init__(self, data: bytes):
9+
httpcore.AsyncByteStream.__init__(self)
10+
httpcore.SyncByteStream.__init__(self)
11+
self.data = data
12+
13+
def __iter__(self) -> Iterator[bytes]:
14+
yield self.data
15+
16+
async def __aiter__(self) -> AsyncIterator[bytes]:
17+
yield self.data
18+
19+
20+
class IteratorStream(httpcore.AsyncByteStream, httpcore.SyncByteStream):
21+
def __init__(self, iterator):
22+
httpcore.AsyncByteStream.__init__(self, aiterator=iterator)
23+
httpcore.SyncByteStream.__init__(self, iterator=iterator)
24+
25+
26+
def stream(
27+
data, files, json: Any, boundary: bytes
28+
) -> Union[httpcore.AsyncByteStream, httpcore.SyncByteStream]:
29+
if files:
30+
# TODO Get rid of this internal import
31+
# import is performed at runtime when needed to reduce impact of internal changes in httpx
32+
from httpx._content_streams import MultipartStream
33+
34+
return MultipartStream(data=data or {}, files=files, boundary=boundary)
35+
36+
if json is not None:
37+
data = dumps(json).encode("utf-8")
38+
elif isinstance(data, str):
39+
data = data.encode("utf-8")
40+
elif data is None:
41+
data = b""
42+
43+
if isinstance(data, bytes):
44+
return ByteStream(data)
45+
46+
return IteratorStream(data)

pytest_httpx/_httpx_mock.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33

44
import httpcore
55
import httpx
6-
import pytest
76

8-
# TODO Stop using internals from httpx, see https://github.com/encode/httpx/issues/872
9-
from httpx._content_streams import encode
7+
from pytest_httpx._httpx_internals import stream
108

119

1210
# Those types are internally defined within httpcore._types
@@ -125,7 +123,8 @@ def add_response(
125123
:param files: Multipart files.
126124
:param json: HTTP body of the response (if JSON should be used as content type) if data is not provided.
127125
:param boundary: Multipart boundary if files is provided.
128-
:param url: Full URL identifying the request(s) to match. Can be a str, a re.Pattern instance or a httpx.URL instance.
126+
:param url: Full URL identifying the request(s) to match.
127+
Can be a str, a re.Pattern instance or a httpx.URL instance.
129128
:param method: HTTP method identifying the request(s) to match.
130129
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
131130
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
@@ -144,7 +143,8 @@ def add_callback(self, callback: Callable, **matchers):
144143
* request: The received httpx.Request.
145144
* timeout: The timeout linked to the request.
146145
It should return a valid httpcore response tuple, you can use pytest_httpx.to_response function to create one.
147-
:param url: Full URL identifying the request(s) to match. Can be a str, a re.Pattern instance or a httpx.URL instance.
146+
:param url: Full URL identifying the request(s) to match.
147+
Can be a str, a re.Pattern instance or a httpx.URL instance.
148148
:param method: HTTP method identifying the request(s) to match.
149149
:param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary.
150150
:param match_content: Full HTTP body identifying the request(s) to match. Must be bytes.
@@ -223,7 +223,8 @@ def get_requests(self, **matchers) -> List[httpx.Request]:
223223
"""
224224
Return all requests sent that match (empty list if no requests were matched).
225225
226-
:param url: Full URL identifying the requests to retrieve. Can be a str, a re.Pattern instance or a httpx.URL instance.
226+
:param url: Full URL identifying the requests to retrieve.
227+
Can be a str, a re.Pattern instance or a httpx.URL instance.
227228
:param method: HTTP method identifying the requests to retrieve. Must be a upper cased string value.
228229
:param match_headers: HTTP headers identifying the requests to retrieve. Must be a dictionary.
229230
:param match_content: Full HTTP body identifying the requests to retrieve. Must be bytes.
@@ -235,7 +236,8 @@ def get_request(self, **matchers) -> Optional[httpx.Request]:
235236
"""
236237
Return the single request that match (or None).
237238
238-
:param url: Full URL identifying the request to retrieve. Can be a str, a re.Pattern instance or a httpx.URL instance.
239+
:param url: Full URL identifying the request to retrieve.
240+
Can be a str, a re.Pattern instance or a httpx.URL instance.
239241
:param method: HTTP method identifying the request to retrieve. Must be a upper cased string value.
240242
:param match_headers: HTTP headers identifying the request to retrieve. Must be a dictionary.
241243
:param match_content: Full HTTP body identifying the request to retrieve. Must be bytes.
@@ -247,27 +249,31 @@ def get_request(self, **matchers) -> Optional[httpx.Request]:
247249
), f"More than one request ({len(requests)}) matched, use get_requests instead."
248250
return requests[0] if requests else None
249251

250-
def assert_and_reset(self):
251-
self._assert_responses_sent()
252-
self._assert_callbacks_executed()
252+
def reset(self, assert_all_responses_were_requested: bool):
253+
responses_not_called = self._reset_responses()
254+
callbacks_not_executed = self._reset_callbacks()
253255

254-
def _assert_responses_sent(self):
256+
if assert_all_responses_were_requested:
257+
assert (
258+
not responses_not_called
259+
), f"The following responses are mocked but not requested: {responses_not_called}"
260+
assert (
261+
not callbacks_not_executed
262+
), f"The following callbacks are registered but not executed: {callbacks_not_executed}"
263+
264+
def _reset_responses(self):
255265
responses_not_called = [
256266
response for matcher, response in self._responses if not matcher.nb_calls
257267
]
258268
self._responses.clear()
259-
assert (
260-
not responses_not_called
261-
), f"The following responses are mocked but not requested: {responses_not_called}"
269+
return responses_not_called
262270

263-
def _assert_callbacks_executed(self):
271+
def _reset_callbacks(self):
264272
callbacks_not_executed = [
265273
callback for matcher, callback in self._callbacks if not matcher.nb_calls
266274
]
267275
self._callbacks.clear()
268-
assert (
269-
not callbacks_not_executed
270-
), f"The following callbacks are registered but not executed: {callbacks_not_executed}"
276+
return callbacks_not_executed
271277

272278

273279
class _PytestSyncTransport(httpcore.SyncHTTPTransport):
@@ -311,12 +317,10 @@ def to_response(
311317
:param json: HTTP body of the response (if JSON should be used as content type) if data is not provided.
312318
:param boundary: Multipart boundary if files is provided.
313319
"""
314-
return (
315-
http_version.encode(),
316-
status_code,
317-
b"",
320+
headers = (
318321
[(header.encode(), value.encode()) for header, value in headers.items()]
319322
if headers
320-
else [],
321-
encode(data=data, files=files, json=json, boundary=boundary),
323+
else []
322324
)
325+
body = stream(data=data, files=files, json=json, boundary=boundary)
326+
return (http_version.encode(), status_code, b"", headers, body)

pytest_httpx/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
44
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
55
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
6-
__version__ = "0.4.0"
6+
__version__ = "0.5.0"

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@
3535
],
3636
keywords=["pytest", "testing", "mock", "httpx"],
3737
packages=find_packages(exclude=["tests*"]),
38-
entry_points={'pytest11': ['pytest_httpx = pytest_httpx']},
39-
install_requires=["httpx==0.13.*", "pytest>=5.4.*,<6.*"],
38+
entry_points={"pytest11": ["pytest_httpx = pytest_httpx"]},
39+
install_requires=["httpx==0.13.*", "pytest==6.*"],
4040
extras_require={
4141
"testing": [
4242
# Used to run async test functions
43-
"pytest-asyncio==0.12.*",
43+
"pytest-asyncio==0.14.*",
4444
# Used to check coverage
4545
"pytest-cov==2.*",
4646
]

0 commit comments

Comments
 (0)