Skip to content

Commit

Permalink
Merge pull request #151 from Colin-b/develop
Browse files Browse the repository at this point in the history
Release version 0.31.0
  • Loading branch information
Colin-b authored Sep 20, 2024
2 parents 36c5062 + 414fb36 commit 26a374e
Show file tree
Hide file tree
Showing 14 changed files with 428 additions and 290 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
pytest-major-version: ['7', '8']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13.0-rc.2']

steps:
- uses: actions/checkout@v4
Expand All @@ -21,7 +20,6 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install -e .[testing]
python -m pip install pytest~=${{ matrix.pytest-major-version }}.0
- name: Test
run: |
pytest --cov=pytest_httpx --cov-fail-under=100 --cov-report=term-missing --runpytest=subprocess
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/psf/black
rev: 24.1.1
rev: 24.8.0
hooks:
- id: black
28 changes: 27 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.31.0] - 2024-09-20
### Changed
- Tests will now fail at teardown by default if some requests were issued but were not matched.
- This behavior can be changed thanks to the new ``pytest.mark.httpx_mock(assert_all_requests_were_expected=False)`` option.
- The `httpx_mock` fixture is now configured using a marker (many thanks to [`Frazer McLean`](https://github.com/RazerM)).
```python
# Apply marker to whole module
pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False)

# Or to specific tests
@pytest.mark.httpx_mock(non_mocked_hosts=[...])
def test_foo(httpx_mock):
...
```
- The following options are available:
- `assert_all_responses_were_requested` (boolean), defaulting to `True`.
- `assert_all_requests_were_expected` (boolean), defaulting to `True`.
- `non_mocked_hosts` (iterable), defaulting to an empty list, meaning all hosts are mocked.
- `httpx_mock.reset` do not expect any parameter anymore and will only reset the mock state (no assertions will be performed).

### Removed
- `pytest` `7` is not supported anymore (`pytest` `8` has been out for 9 months already).
- `assert_all_responses_were_requested` fixture is not available anymore, use `pytest.mark.httpx_mock(assert_all_responses_were_requested=False)` instead.
- `non_mocked_hosts` fixture is not available anymore, use `pytest.mark.httpx_mock(non_mocked_hosts=[])` instead.

## [0.30.0] - 2024-02-21
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.27.\*
Expand Down Expand Up @@ -312,7 +337,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.30.0...HEAD
[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.0...HEAD
[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
[0.28.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.27.0...v0.28.0
Expand Down
59 changes: 41 additions & 18 deletions 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-208 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-211 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 @@ -56,14 +56,18 @@ async def test_something_async(httpx_mock):

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

This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture:
This behavior can be disabled thanks to the `httpx_mock` marker:

```python
import pytest

@pytest.fixture
def assert_all_responses_were_requested() -> bool:
return False
# For whole module
pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False)

# For specific test
@pytest.mark.httpx_mock(assert_all_responses_were_requested=True)
def test_something(httpx_mock):
...
```

Default response is a HTTP/1.1 200 (OK) without any body.
Expand Down Expand Up @@ -456,14 +460,18 @@ Callback should expect one parameter, the received [`httpx.Request`](https://www

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

This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture:
This behavior can be disabled thanks to the `httpx_mock` marker:

```python
import pytest

@pytest.fixture
def assert_all_responses_were_requested() -> bool:
return False
# For whole module
pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False)

# For specific test
@pytest.mark.httpx_mock(assert_all_responses_were_requested=True)
def test_something(httpx_mock):
...
```

Note that callbacks are considered as responses, and thus are [selected the same way](#how-response-is-selected).
Expand Down Expand Up @@ -552,6 +560,7 @@ import pytest
from pytest_httpx import HTTPXMock


@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_timeout(httpx_mock: HTTPXMock):
with httpx.Client() as client:
with pytest.raises(httpx.TimeoutException):
Expand All @@ -564,6 +573,20 @@ def test_timeout(httpx_mock: HTTPXMock):
The best way to ensure the content of your requests is still to use the `match_headers` and / or `match_content` parameters when adding a response.
In the same spirit, ensuring that no request was issued does not necessarily require any code.

Note that default behavior is to assert that all requests were expected. You can turn this off (at your own risk of not spotting regression in your code base) using the `httpx_mock` marker:

```python
import pytest

# For whole module
pytestmark = pytest.mark.httpx_mock(assert_all_requests_were_expected=False)

# For specific test
@pytest.mark.httpx_mock(assert_all_requests_were_expected=False)
def test_something(httpx_mock):
...
```

In any case, you always have the ability to retrieve the requests that were issued.

As in the following samples:
Expand Down Expand Up @@ -650,14 +673,18 @@ By default, `pytest-httpx` will mock every request.

But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through.

To do so, you can use the `non_mocked_hosts` fixture:
To do so, you can use the `httpx_mock` marker:

```python
import pytest

@pytest.fixture
def non_mocked_hosts() -> list:
return ["my_local_test_host", "my_other_test_host"]
# For whole module
pytestmark = pytest.mark.httpx_mock(non_mocked_hosts=["my_local_test_host", "my_other_test_host"])

# For specific test
@pytest.mark.httpx_mock(non_mocked_hosts=["my_local_test_host"])
def test_something(httpx_mock):
...
```

Every other requested hosts will be mocked as in the following example
Expand All @@ -666,11 +693,7 @@ Every other requested hosts will be mocked as in the following example
import pytest
import httpx

@pytest.fixture
def non_mocked_hosts() -> list:
return ["my_local_test_host"]


@pytest.mark.httpx_mock(non_mocked_hosts=["my_local_test_host"])
def test_partial_mock(httpx_mock):
httpx_mock.add_response()

Expand Down
14 changes: 7 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ maintainers = [
]
keywords = [
"httpx",
"mock",
"pytest",
"testing",
]
Expand All @@ -38,7 +37,7 @@ classifiers = [
]
dependencies = [
"httpx==0.27.*",
"pytest>=7,<9",
"pytest==8.*",
]
dynamic = ["version"]

Expand All @@ -51,16 +50,17 @@ issues = "https://github.com/Colin-b/pytest_httpx/issues"
[project.optional-dependencies]
testing = [
# Used to check coverage
"pytest-cov==4.*",
"pytest-cov==5.*",
# Used to run async tests
"pytest-asyncio==0.23.*",
"pytest-asyncio==0.24.*",
]

[project.entry-points.pytest11]
pytest_httpx = "pytest_httpx"

[tool.setuptools.packages.find]
exclude = ["tests*"]

[tool.setuptools.dynamic]
version = {attr = "pytest_httpx.version.__version__"}

[tool.pytest.ini_options]
# Silence deprecation warnings about option "asyncio_default_fixture_loop_scope"
asyncio_default_fixture_loop_scope = "function"
41 changes: 18 additions & 23 deletions pytest_httpx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from collections.abc import Generator
from typing import List

import httpx
import pytest
from pytest import MonkeyPatch
from pytest import Config, FixtureRequest, MonkeyPatch

from pytest_httpx._httpx_mock import HTTPXMock
from pytest_httpx._httpx_mock import HTTPXMock, HTTPXMockOptions
from pytest_httpx._httpx_internals import IteratorStream
from pytest_httpx.version import __version__

Expand All @@ -16,27 +15,13 @@
)


@pytest.fixture
def assert_all_responses_were_requested() -> bool:
return True


@pytest.fixture
def non_mocked_hosts() -> List[str]:
return []


@pytest.fixture
def httpx_mock(
monkeypatch: MonkeyPatch,
assert_all_responses_were_requested: bool,
non_mocked_hosts: List[str],
request: FixtureRequest,
) -> Generator[HTTPXMock, None, None]:
# Ensure redirections to www hosts are handled transparently.
missing_www = [
f"www.{host}" for host in non_mocked_hosts if not host.startswith("www.")
]
non_mocked_hosts += missing_www
marker = request.node.get_closest_marker("httpx_mock")
options = HTTPXMockOptions.from_marker(marker) if marker else HTTPXMockOptions()

mock = HTTPXMock()

Expand All @@ -46,7 +31,7 @@ def httpx_mock(
def mocked_handle_request(
transport: httpx.HTTPTransport, request: httpx.Request
) -> httpx.Response:
if request.url.host in non_mocked_hosts:
if request.url.host in options.non_mocked_hosts:
return real_handle_request(transport, request)
return mock._handle_request(transport, request)

Expand All @@ -62,7 +47,7 @@ def mocked_handle_request(
async def mocked_handle_async_request(
transport: httpx.AsyncHTTPTransport, request: httpx.Request
) -> httpx.Response:
if request.url.host in non_mocked_hosts:
if request.url.host in options.non_mocked_hosts:
return await real_handle_async_request(transport, request)
return await mock._handle_async_request(transport, request)

Expand All @@ -73,4 +58,14 @@ async def mocked_handle_async_request(
)

yield mock
mock.reset(assert_all_responses_were_requested)
try:
mock._assert_options(options)
finally:
mock.reset()


def pytest_configure(config: Config) -> None:
config.addinivalue_line(
"markers",
"httpx_mock(*, assert_all_responses_were_requested=True, assert_all_requests_were_expected=True, non_mocked_hosts=[]): Configure httpx_mock fixture.",
)
16 changes: 6 additions & 10 deletions pytest_httpx/_httpx_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@
from typing import (
Union,
Dict,
Sequence,
Tuple,
Iterable,
AsyncIterator,
Iterator,
Optional,
)
from collections.abc import Sequence, Iterable, AsyncIterator, Iterator

import httpcore
import httpx
Expand All @@ -19,19 +16,18 @@
# Those types are internally defined within httpx._types
HeaderTypes = Union[
httpx.Headers,
Dict[str, str],
Dict[bytes, bytes],
Sequence[Tuple[str, str]],
Sequence[Tuple[bytes, bytes]],
dict[str, str],
dict[bytes, bytes],
Sequence[tuple[str, str]],
Sequence[tuple[bytes, bytes]],
]


class IteratorStream(AsyncIteratorByteStream, IteratorByteStream):
def __init__(self, stream: Iterable[bytes]):
class Stream:
def __iter__(self) -> Iterator[bytes]:
for chunk in stream:
yield chunk
yield from stream

async def __aiter__(self) -> AsyncIterator[bytes]:
for chunk in stream:
Expand Down
Loading

0 comments on commit 26a374e

Please sign in to comment.