Skip to content

Commit

Permalink
Merge pull request #86 from Colin-b/develop
Browse files Browse the repository at this point in the history
Release 0.21.1
  • Loading branch information
Colin-b authored Oct 20, 2022
2 parents ef1637c + 31cc17e commit 123f8cc
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 22 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.21.1] - 2022-10-20
### Fixed
- `httpx_mock.add_callback` now handles async callbacks.

## [0.21.0] - 2022-05-24
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.23.\*
Expand Down Expand Up @@ -223,7 +227,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.21.0...HEAD
[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.1...HEAD
[0.21.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.0...v0.21.1
[0.21.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.20.0...v0.21.0
[0.20.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.19.0...v0.20.0
[0.19.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.18.0...v0.19.0
Expand Down
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Build status" src="https://github.com/Colin-b/pytest_httpx/workflows/Release/badge.svg"></a>
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-153 passed-blue"></a>
<a href="https://github.com/Colin-b/pytest_httpx/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-162 passed-blue"></a>
<a href="https://pypi.org/project/pytest-httpx/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/pytest_httpx"></a>
</p>

Expand Down Expand Up @@ -444,6 +444,38 @@ def test_dynamic_response(httpx_mock: HTTPXMock):

```

Alternatively, callbacks can also be asynchronous.

As in the following sample simulating network latency on some responses only.

```python
import asyncio
import httpx
import pytest
from pytest_httpx import HTTPXMock


@pytest.mark.asyncio
async def test_dynamic_async_response(httpx_mock: HTTPXMock):
async def simulate_network_latency(request: httpx.Request):
await asyncio.sleep(1)
return httpx.Response(
status_code=200, json={"url": str(request.url)},
)

httpx_mock.add_callback(simulate_network_latency)
httpx_mock.add_response()

async with httpx.AsyncClient() as client:
responses = await asyncio.gather(
# Response will be received after one second
client.get("https://test_url"),
# Response will instantly be received (1 second before the first request)
client.get("https://test_url")
)

```

### Raising exceptions

You can simulate HTTPX exception throwing by raising an exception in your callback or use `httpx_mock.add_exception` with the exception instance.
Expand Down
61 changes: 50 additions & 11 deletions pytest_httpx/_httpx_mock.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import inspect
import re
from typing import List, Union, Optional, Callable, Tuple, Pattern, Any, Dict
from typing import List, Union, Optional, Callable, Tuple, Pattern, Any, Dict, Awaitable
from urllib.parse import parse_qs

import httpx
Expand Down Expand Up @@ -89,7 +90,12 @@ def __init__(self) -> None:
self._callbacks: List[
Tuple[
_RequestMatcher,
Callable[[httpx.Request], Optional[httpx.Response]],
Callable[
[httpx.Request],
Union[
Optional[httpx.Response], Awaitable[Optional[httpx.Response]]
],
],
]
] = []

Expand Down Expand Up @@ -135,7 +141,12 @@ def add_response(
self.add_callback(lambda request: response, **matchers)

def add_callback(
self, callback: Callable[[httpx.Request], Optional[httpx.Response]], **matchers
self,
callback: Callable[
[httpx.Request],
Union[Optional[httpx.Response], Awaitable[Optional[httpx.Response]]],
],
**matchers,
) -> None:
"""
Mock the action that will take place if a request match.
Expand Down Expand Up @@ -180,12 +191,26 @@ def _handle_request(
response = callback(request)

if response:
# Allow to read the response on client side
response.is_stream_consumed = False
response.is_closed = False
if hasattr(response, "_content"):
del response._content
return response
return _unread(response)

raise httpx.TimeoutException(
self._explain_that_no_response_was_found(request), request=request
)

async def _handle_async_request(
self,
request: httpx.Request,
) -> httpx.Response:
self._requests.append(request)

callback = self._get_callback(request)
if callback:
response = callback(request)

if response:
if inspect.isawaitable(response):
response = await response
return _unread(response)

raise httpx.TimeoutException(
self._explain_that_no_response_was_found(request), request=request
Expand Down Expand Up @@ -221,7 +246,12 @@ def _explain_that_no_response_was_found(self, request: httpx.Request) -> str:

def _get_callback(
self, request: httpx.Request
) -> Optional[Callable[[httpx.Request], Optional[httpx.Response]]]:
) -> Optional[
Callable[
[httpx.Request],
Union[Optional[httpx.Response], Awaitable[Optional[httpx.Response]]],
]
]:
callbacks = [
(matcher, callback)
for matcher, callback in self._callbacks
Expand Down Expand Up @@ -304,4 +334,13 @@ def __init__(self, mock: HTTPXMock):
self.mock = mock

async def handle_async_request(self, *args, **kwargs) -> httpx.Response:
return self.mock._handle_request(*args, **kwargs)
return await self.mock._handle_async_request(*args, **kwargs)


def _unread(response: httpx.Response) -> httpx.Response:
# Allow to read the response on client side
response.is_stream_consumed = False
response.is_closed = False
if hasattr(response, "_content"):
del response._content
return response
2 changes: 1 addition & 1 deletion pytest_httpx/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.21.0"
__version__ = "0.21.1"
Loading

0 comments on commit 123f8cc

Please sign in to comment.