Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
48bed0c
feat: add support for `match_params` allowing partial params matching
pbabics Nov 19, 2024
28dc48a
Update pytest_httpx/_request_matcher.py
pbabics Nov 19, 2024
f432419
Merge branch 'develop' into feat/params-match-partial-support
Colin-b May 14, 2025
c5f1046
Introduce match_params
Colin-b May 14, 2025
a0d812d
Merge remote-tracking branch 'origin/develop' into develop
Colin-b May 14, 2025
619b2ae
Add more documentation on match_params
Colin-b May 14, 2025
1a22feb
Bump black to latest version
Colin-b May 14, 2025
607e411
Bump black to latest version
Colin-b May 14, 2025
2ad98c5
Keep license year up to date
Colin-b May 14, 2025
632069d
Fix match_params a bit more
Colin-b May 14, 2025
835ac95
Fix Set-Cookie header mdn reference
emmanuel-ferdman May 24, 2025
2fdcd46
Merge pull request #180 from emmanuel-ferdman/develop
Colin-b May 25, 2025
59a18e0
Accept BaseException in add_exception
joce Oct 28, 2025
786b861
Add test for asyncio.CancelledError support
joce Oct 28, 2025
7108d06
Fix SonarCloud issues
joce Oct 29, 2025
9c1cb67
Parametrize tests to minimize dups and please SonarCloud.
joce Oct 29, 2025
6b0106d
Bump dependencies, update pytest config for pytest 9
zweizeichen Nov 11, 2025
9186f7c
Drop Python 3.9 support (EOL)
zweizeichen Nov 14, 2025
3dd8952
Relax version constraint of python-asyncio
zweizeichen Nov 14, 2025
c4cca6b
Bump pytest-cov to current release
zweizeichen Nov 14, 2025
9def72c
Revert pytest-cov change for now, it caused issues
zweizeichen Nov 14, 2025
5d44bf4
Bump workflow action versions
zweizeichen Nov 14, 2025
546f245
Include 3.14 in testing
zweizeichen Nov 14, 2025
92394a7
Merge pull request #189 from helmholtzcloud/develop
Colin-b Nov 15, 2025
acce297
Test multi values in same param
Colin-b Dec 2, 2025
5089275
Test params not matching
Colin-b Dec 2, 2025
8295e1d
Prevent invalid match_params setup
Colin-b Dec 2, 2025
34b20ca
Prevent invalid match_params setup
Colin-b Dec 2, 2025
e5ec7b6
Prevent invalid match_params setup
Colin-b Dec 2, 2025
9a3a9dc
Prevent invalid match_params setup
Colin-b Dec 2, 2025
c3dd346
Accept BaseException in add_exception
Colin-b Dec 2, 2025
ced1c3a
Document the fact that add_exception can receive BaseException derive…
Colin-b Dec 2, 2025
7b612dd
Keep the number of test cases up to date
Colin-b Dec 2, 2025
308b8dc
Handle base exception
Colin-b Dec 2, 2025
4d1c417
Release version 0.35.1
Colin-b Dec 2, 2025
63b1ff1
Switch version to 0.36.0 and document breaking changes
Colin-b Dec 2, 2025
e02259c
Use subprocess coverage
Colin-b Dec 2, 2025
dbc20d7
Release version 0.36.0 today
Colin-b Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- name: Create packages
run: |
python -m pip install build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']

steps:
- uses: actions/checkout@v4
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.10.0
rev: 25.11.0
hooks:
- id: black
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.36.0] - 2025-12-02
### Changed
- `pytest` required version is now `9`.

### Added
- Explicit support for python `3.14`.
- `match_params` parameter is now available on responses and callbacks registration, as well as request(s) retrieval. Allowing to provide query parameters as a dict instead of being part of the matched URL.
- This parameter allows to perform partial query params matching ([refer to documentation](README.md#matching-on-query-parameters) for more information).

### Fixed
- URL with more than one value for the same parameter were not matched properly (matching was performed on the first value).
- `httpx_mock.add_exception` is now properly documented (accepts `BaseException` instead of `Exception`).

### Removed
- `pytest` `8` is not supported anymore.
- python `3.9` is not supported anymore.

## [0.35.0] - 2024-11-28
### Changed
- Requires [`httpx`](https://www.python-httpx.org)==0.28.\*
Expand Down Expand Up @@ -408,7 +425,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.35.0...HEAD
[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.36.0...HEAD
[0.36.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.35.0...v0.36.0
[0.35.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.34.0...v0.35.0
[0.34.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.33.0...v0.34.0
[0.33.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.32.0...v0.33.0
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Colin Bounouar
Copyright (c) 2025 Colin Bounouar

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
30 changes: 28 additions & 2 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-272 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-295 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 @@ -107,6 +107,32 @@ def test_url_as_httpx_url(httpx_mock: HTTPXMock):
response = client.get("https://test_url?a=1&b=2")
```

#### Matching on query parameters

Use `match_params` to partially match query parameters without having to provide a regular expression as `url`.

If this parameter is provided, `url` parameter must not contain any query parameter.

All query parameters have to be provided (as `str`). You can however use `unittest.mock.ANY` to do partial matching.

```python
import httpx
from pytest_httpx import HTTPXMock
from unittest.mock import ANY

def test_partial_params_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(url="https://test_url", match_params={"a": "1", "b": ANY})

with httpx.Client() as client:
response = client.get("https://test_url?a=1&b=2")

def test_partial_multi_params_matching(httpx_mock: HTTPXMock):
httpx_mock.add_response(url="https://test_url", match_params={"a": ["1", "3"], "b": ["2", ANY]})

with httpx.Client() as client:
response = client.get("https://test_url?a=1&b=2&a=3&b=4")
```

#### Matching on HTTP method

Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) to reply to.
Expand Down Expand Up @@ -473,7 +499,7 @@ def test_headers_as_httpx_headers(httpx_mock: HTTPXMock):

Cookies are sent in the `set-cookie` HTTP header.

You can then send cookies in the response by setting the `set-cookie` header with [the value following key=value format]((https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)).
You can then send cookies in the response by setting the `set-cookie` header with [the value following key=value format](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).

```python
import httpx
Expand Down
15 changes: 9 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
name = "pytest-httpx"
description = "Send responses to httpx."
readme = "README.md"
requires-python = ">=3.9"
requires-python = ">=3.10"
license = {file = "LICENSE"}
authors = [
{ name = "Colin Bounouar", email = "colin.bounouar.dev@gmail.com" },
Expand All @@ -27,18 +27,18 @@ classifiers = [
"Natural Language :: English",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Software Development :: Build Tools",
"Typing :: Typed",
]
dependencies = [
"httpx==0.28.*",
"pytest==8.*",
"pytest==9.*",
]
dynamic = ["version"]

Expand All @@ -51,9 +51,9 @@ issues = "https://github.com/Colin-b/pytest_httpx/issues"
[project.optional-dependencies]
testing = [
# Used to check coverage
"pytest-cov==6.*",
"pytest-cov==7.*",
# Used to run async tests
"pytest-asyncio==0.24.*",
"pytest-asyncio==1.*",
]

[project.entry-points.pytest11]
Expand All @@ -62,6 +62,9 @@ pytest_httpx = "pytest_httpx"
[tool.setuptools.dynamic]
version = {attr = "pytest_httpx.version.__version__"}

[tool.pytest.ini_options]
[tool.pytest]
# Silence deprecation warnings about option "asyncio_default_fixture_loop_scope"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.run]
patch = ["subprocess"]
2 changes: 1 addition & 1 deletion pytest_httpx/_httpx_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def _to_httpx_url(url: httpcore.URL, headers: list[tuple[bytes, bytes]]) -> http


def _proxy_url(
real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport]
real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport],
) -> Optional[httpx.URL]:
if isinstance(
real_pool := real_transport._pool, (httpcore.HTTPProxy, httpcore.AsyncHTTPProxy)
Expand Down
18 changes: 12 additions & 6 deletions pytest_httpx/_httpx_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def add_response(
:param html: HTTP body of the response (as HTML string content).
:param stream: HTTP body of the response (as httpx.SyncByteStream or httpx.AsyncByteStream) as stream content.
:param json: HTTP body of the response (if JSON should be used as content type) if data is not provided.
:param url: Full URL identifying the request(s) to match.
:param url: Full URL identifying the request(s) to match. Use in addition to match_params if you do not want to provide query parameters as part of the URL.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the request(s) to match.
:param proxy_url: Full proxy URL identifying the request(s) to match.
Expand All @@ -68,6 +68,8 @@ def add_response(
:param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable.
:param match_data: Multipart data (excluding files) identifying the request(s) to match. Must be a dictionary.
:param match_files: Multipart files identifying the request(s) to match. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param match_extensions: Extensions identifying the request(s) to match. Must be a dictionary.
:param match_params: Query string parameters identifying the request(s) to match (if not provided as part of URL already). Must be a dictionary with str keys (parameter name) and str values (or a list of str values if parameter is provided more than once).
:param is_optional: True will mark this response as optional, False will expect a request matching it. Must be a boolean. Default to the opposite of assert_all_responses_were_requested option value (itself defaulting to True, meaning this parameter default to False).
:param is_reusable: True will allow re-using this response even if it already matched, False prevent re-using it. Must be a boolean. Default to the can_send_already_matched_responses option value (itself defaulting to False).
"""
Expand Down Expand Up @@ -101,7 +103,7 @@ def add_callback(

:param callback: The callable that will be called upon reception of the matched request.
It must expect one parameter, the received httpx.Request and should return a httpx.Response.
:param url: Full URL identifying the request(s) to match.
:param url: Full URL identifying the request(s) to match. Use in addition to match_params if you do not want to provide query parameters as part of the URL.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the request(s) to match.
:param proxy_url: Full proxy URL identifying the request(s) to match.
Expand All @@ -112,17 +114,18 @@ def add_callback(
:param match_data: Multipart data (excluding files) identifying the request(s) to match. Must be a dictionary.
:param match_files: Multipart files identifying the request(s) to match. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param match_extensions: Extensions identifying the request(s) to match. Must be a dictionary.
:param match_params: Query string parameters identifying the request(s) to match (if not provided as part of URL already). Must be a dictionary with str keys (parameter name) and str values (or a list of str values if parameter is provided more than once).
:param is_optional: True will mark this callback as optional, False will expect a request matching it. Must be a boolean. Default to the opposite of assert_all_responses_were_requested option value (itself defaulting to True, meaning this parameter default to False).
:param is_reusable: True will allow re-using this callback even if it already matched, False prevent re-using it. Must be a boolean. Default to the can_send_already_matched_responses option value (itself defaulting to False).
"""
self._callbacks.append((_RequestMatcher(self._options, **matchers), callback))

def add_exception(self, exception: Exception, **matchers: Any) -> None:
def add_exception(self, exception: BaseException, **matchers: Any) -> None:
"""
Raise an exception if a request match.

:param exception: The exception that will be raised upon reception of the matched request.
:param url: Full URL identifying the request(s) to match.
:param url: Full URL identifying the request(s) to match. Use in addition to match_params if you do not want to provide query parameters as part of the URL.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the request(s) to match.
:param proxy_url: Full proxy URL identifying the request(s) to match.
Expand All @@ -133,6 +136,7 @@ def add_exception(self, exception: Exception, **matchers: Any) -> None:
:param match_data: Multipart data (excluding files) identifying the request(s) to match. Must be a dictionary.
:param match_files: Multipart files identifying the request(s) to match. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param match_extensions: Extensions identifying the request(s) to match. Must be a dictionary.
:param match_params: Query string parameters identifying the request(s) to match (if not provided as part of URL already). Must be a dictionary with str keys (parameter name) and str values (or a list of str values if parameter is provided more than once).
:param is_optional: True will mark this exception response as optional, False will expect a request matching it. Must be a boolean. Default to the opposite of assert_all_responses_were_requested option value (itself defaulting to True, meaning this parameter default to False).
:param is_reusable: True will allow re-using this exception response even if it already matched, False prevent re-using it. Must be a boolean. Default to the can_send_already_matched_responses option value (itself defaulting to False).
"""
Expand Down Expand Up @@ -261,7 +265,7 @@ def get_requests(self, **matchers: Any) -> list[httpx.Request]:
"""
Return all requests sent that match (empty list if no requests were matched).

:param url: Full URL identifying the requests to retrieve.
:param url: Full URL identifying the requests to retrieve. Use in addition to match_params if you do not want to provide query parameters as part of the URL.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the requests to retrieve. Must be an upper-cased string value.
:param proxy_url: Full proxy URL identifying the requests to retrieve.
Expand All @@ -272,6 +276,7 @@ def get_requests(self, **matchers: Any) -> list[httpx.Request]:
:param match_data: Multipart data (excluding files) identifying the requests to retrieve. Must be a dictionary.
:param match_files: Multipart files identifying the requests to retrieve. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param match_extensions: Extensions identifying the requests to retrieve. Must be a dictionary.
:param match_params: Query string parameters identifying the requests to retrieve (if not provided as part of URL already). Must be a dictionary with str keys (parameter name) and str values (or a list of str values if parameter is provided more than once).
"""
matcher = _RequestMatcher(self._options, **matchers)
return [
Expand All @@ -284,7 +289,7 @@ def get_request(self, **matchers: Any) -> Optional[httpx.Request]:
"""
Return the single request that match (or None).

:param url: Full URL identifying the request to retrieve.
:param url: Full URL identifying the request to retrieve. Use in addition to match_params if you do not want to provide query parameters as part of the URL.
Can be a str, a re.Pattern instance or a httpx.URL instance.
:param method: HTTP method identifying the request to retrieve. Must be an upper-cased string value.
:param proxy_url: Full proxy URL identifying the request to retrieve.
Expand All @@ -295,6 +300,7 @@ def get_request(self, **matchers: Any) -> Optional[httpx.Request]:
:param match_data: Multipart data (excluding files) identifying the request to retrieve. Must be a dictionary.
:param match_files: Multipart files identifying the request to retrieve. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
:param match_extensions: Extensions identifying the request to retrieve. Must be a dictionary.
:param match_params: Query string parameters identifying the request to retrieve (if not provided as part of URL already). Must be a dictionary with str keys (parameter name) and str values (or a list of str values if parameter is provided more than once).
:raises AssertionError: in case more than one request match.
"""
requests = self.get_requests(**matchers)
Expand Down
Loading