From aa9a9b66383ddaa89f67e34014c721b67a434642 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Thu, 6 Aug 2026 20:55:58 +0300 Subject: [PATCH 01/28] Add a compatibility layer that allows using httpx2 --- pyproject.toml | 1 + pytest_httpx/__init__.py | 2 +- pytest_httpx/_compat.py | 31 +++++++++++++++++++++++++++++++ pytest_httpx/_httpx_internals.py | 15 +++++++-------- pytest_httpx/_httpx_mock.py | 3 +-- pytest_httpx/_options.py | 2 +- pytest_httpx/_pretty_print.py | 3 +-- pytest_httpx/_request_matcher.py | 8 +++----- 8 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 pytest_httpx/_compat.py diff --git a/pyproject.toml b/pyproject.toml index 5018df2..e6b4f62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ classifiers = [ ] dependencies = [ "httpx==0.28.*", + "httpx2==2.*", "pytest==9.*", ] dynamic = ["version"] diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py index 8460cb0..119faa2 100644 --- a/pytest_httpx/__init__.py +++ b/pytest_httpx/__init__.py @@ -1,10 +1,10 @@ from collections.abc import Generator from operator import methodcaller -import httpx import pytest from pytest import Config, FixtureRequest, MonkeyPatch +from pytest_httpx._compat import httpx from pytest_httpx._httpx_internals import IteratorStream from pytest_httpx._httpx_mock import HTTPXMock from pytest_httpx._options import _HTTPXMockOptions diff --git a/pytest_httpx/_compat.py b/pytest_httpx/_compat.py new file mode 100644 index 0000000..2ec88d4 --- /dev/null +++ b/pytest_httpx/_compat.py @@ -0,0 +1,31 @@ +import warnings +from typing import TYPE_CHECKING + + +class PytestHTTPXDeprecationWarning(UserWarning): + pass + + +if TYPE_CHECKING: + import httpcore2 as httpcore + import httpx2 as httpx +else: + try: + import httpcore2 as httpcore + import httpx2 as httpx + except ModuleNotFoundError: + try: + import httpcore # noqa: F401 + import httpx # noqa: F401 + except ModuleNotFoundError: + raise RuntimeError( + "pytest-httpx requires the httpx2 package to be installed.\n" + "You can install it with:\n" + " $ pip install httpx2\n" + ) from None + else: + warnings.warn( + "Using `httpx` with pytest-httpx is deprecated; install `httpx2` instead.", + PytestHTTPXDeprecationWarning, + stacklevel=2, + ) diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index 11fe1c0..91702e2 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -2,11 +2,7 @@ from collections.abc import AsyncIterator, Iterable, Iterator, Sequence from typing import TypeAlias -import httpcore -import httpx - -# TODO Get rid of this internal import -from httpx._content import AsyncIteratorByteStream, IteratorByteStream +from pytest_httpx._compat import httpcore, httpx # Those types are internally defined within httpx._types HeaderTypes: TypeAlias = ( @@ -19,7 +15,11 @@ PrimitiveData: TypeAlias = str | int | float | bool | None -class IteratorStream(AsyncIteratorByteStream, IteratorByteStream): +# TODO Get rid of these internal classes +class IteratorStream( + httpx._content.AsyncIteratorByteStream, + httpx._content.IteratorByteStream, +): def __init__(self, stream: Iterable[bytes]): class Stream: def __iter__(self) -> Iterator[bytes]: @@ -29,8 +29,7 @@ async def __aiter__(self) -> AsyncIterator[bytes]: for chunk in stream: yield chunk - AsyncIteratorByteStream.__init__(self, stream=Stream()) - IteratorByteStream.__init__(self, stream=Stream()) + super().__init__(stream=Stream()) def _to_httpx_url(url: httpcore.URL, headers: list[tuple[bytes, bytes]]) -> httpx.URL: diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 5b7468d..40fc3ff 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -3,9 +3,8 @@ from collections.abc import Awaitable, Callable from typing import Any -import httpx - from pytest_httpx import _httpx_internals +from pytest_httpx._compat import httpx from pytest_httpx._options import _HTTPXMockOptions from pytest_httpx._pretty_print import RequestDescription from pytest_httpx._request_matcher import _RequestMatcher diff --git a/pytest_httpx/_options.py b/pytest_httpx/_options.py index 7f13ee5..b1cfeef 100644 --- a/pytest_httpx/_options.py +++ b/pytest_httpx/_options.py @@ -1,6 +1,6 @@ from collections.abc import Callable -import httpx +from pytest_httpx._compat import httpx class _HTTPXMockOptions: diff --git a/pytest_httpx/_pretty_print.py b/pytest_httpx/_pretty_print.py index 60af8cd..bfdd3c6 100644 --- a/pytest_httpx/_pretty_print.py +++ b/pytest_httpx/_pretty_print.py @@ -1,5 +1,4 @@ -import httpx - +from pytest_httpx._compat import httpx from pytest_httpx._httpx_internals import _proxy_url from pytest_httpx._request_matcher import _RequestMatcher diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index ad141b9..86d6830 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -5,9 +5,7 @@ from typing import Any from unittest.mock import ANY -import httpx -from httpx import QueryParams - +from pytest_httpx._compat import httpx from pytest_httpx._httpx_internals import PrimitiveData, _proxy_url from pytest_httpx._options import _HTTPXMockOptions @@ -40,7 +38,7 @@ def _url_match( # Compare query parameters apart as order of parameters should not matter received_params = to_params_dict(received.params) expected_params = to_params_dict( - url_to_match.params if params is None else QueryParams(params) + url_to_match.params if params is None else httpx.QueryParams(params) ) if params: convert_back_mock_any(params, expected_params) @@ -52,7 +50,7 @@ def _url_match( return (received_params == expected_params) and (url == received_url) -def to_params_dict(params: QueryParams) -> dict[str, str | list[str]]: +def to_params_dict(params: httpx.QueryParams) -> dict[str, str | list[str]]: """Convert query parameters to a dict where the value is a string if the parameter has a single value and a list of string otherwise.""" d = {} for key in params: From fc52b8ddbea3784bb25d85456578a1a7d200ece2 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Thu, 6 Aug 2026 21:24:23 +0300 Subject: [PATCH 02/28] Add a changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0140c1..2285cf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sending a non UTF8 body while having a `json` matcher registered will not raise a `UnicodeDecodeError` anymore. - `match_params` is now handling `bool`, `int` and `float` values in addition to `str`. Only str values were previously expected. - A meaningful error will now be returned when a callback that does not return an `httpx.Response` is called. +### Added +- Support for httpx2 is implemented via a httpx/httpx2 compatibility module. ### Removed - python `3.10` is not supported anymore. From dfb44131f725f0154419c93f8c6e125b006ae772 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Tue, 8 Sep 2026 23:51:46 +0300 Subject: [PATCH 03/28] Move httpx and httpx2 dependencies to the extras --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e6b4f62..6b0d31a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,6 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "httpx==0.28.*", - "httpx2==2.*", "pytest==9.*", ] dynamic = ["version"] @@ -52,6 +50,8 @@ changelog = "https://github.com/Colin-b/pytest_httpx/blob/master/CHANGELOG.md" issues = "https://github.com/Colin-b/pytest_httpx/issues" [project.optional-dependencies] +httpx = ["httpx==0.28.*"] +httpx2 = ["httpx2==2.*"] testing = [ # Used to check coverage "pytest-cov==7.*", From 8f80b921d5936b702b736e6cf6f19524575463a7 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Thu, 8 Oct 2026 23:55:00 +0300 Subject: [PATCH 04/28] Make IteratorStream inherit from public classes --- pytest_httpx/_httpx_internals.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index 91702e2..365b52e 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -15,21 +15,19 @@ PrimitiveData: TypeAlias = str | int | float | bool | None -# TODO Get rid of these internal classes class IteratorStream( - httpx._content.AsyncIteratorByteStream, - httpx._content.IteratorByteStream, + httpx.AsyncByteStream, + httpx.SyncByteStream, ): def __init__(self, stream: Iterable[bytes]): - class Stream: - def __iter__(self) -> Iterator[bytes]: - yield from stream + self._stream = stream - async def __aiter__(self) -> AsyncIterator[bytes]: - for chunk in stream: - yield chunk + def __iter__(self) -> Iterator[bytes]: + yield from self._stream - super().__init__(stream=Stream()) + async def __aiter__(self) -> AsyncIterator[bytes]: + for chunk in self._stream: + yield chunk def _to_httpx_url(url: httpcore.URL, headers: list[tuple[bytes, bytes]]) -> httpx.URL: From 86e62355e90800f3f996d152271c8a3c32167bf0 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Fri, 9 Oct 2026 00:07:51 +0300 Subject: [PATCH 05/28] Collect httpx backends for mocking --- pytest_httpx/_httpx_compat.py | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 pytest_httpx/_httpx_compat.py diff --git a/pytest_httpx/_httpx_compat.py b/pytest_httpx/_httpx_compat.py new file mode 100644 index 0000000..2121ec3 --- /dev/null +++ b/pytest_httpx/_httpx_compat.py @@ -0,0 +1,68 @@ +import warnings +from dataclasses import dataclass +from functools import cache +from types import ModuleType +from typing import TYPE_CHECKING, Literal + + +class PytestHTTPXDeprecationWarning(UserWarning): + pass + + +@dataclass(frozen=True) +class HttpxBackend: + name: Literal["httpx", "httpx2"] + httpx: ModuleType + httpcore: ModuleType + + +@cache +def get_backends() -> list[HttpxBackend]: + """Return every httpx backend found in the environment.""" + + backends = [] + + try: + import httpx2 + import httpcore2 + except ModuleNotFoundError: + pass + else: + backends.append(HttpxBackend("httpx2", httpx2, httpcore2)) + + try: + import httpx as httpx_ + import httpcore as httpcore_ + except ModuleNotFoundError: + pass + else: + backends.append(HttpxBackend("httpx", httpx_, httpcore_)) + + if not backends: + raise RuntimeError( + "pytest-httpx requires either httpx or httpx2 to be installed.\n" + "They are now optional dependencies, install the matching extra with:\n" + " $ pip install pytest-httpx[httpx2]\n" + "or:\n" + " $ pip install pytest-httpx[httpx]\n" + ) + + return backends + + +if TYPE_CHECKING: + import httpx2 as httpx + import httpcore2 as httpcore +else: + # A module-level backend for global httpx/httpcore names references. + _backends = get_backends() + [_default_backend, *_] = _backends + httpx = _default_backend.httpx + httpcore = _default_backend.httpcore + + if any(backend.name == "httpx" for backend in _backends): + warnings.warn( + "Using httpx with pytest-httpx is deprecated; consider using httpx2 instead.", + PytestHTTPXDeprecationWarning, + stacklevel=2, + ) From 24a41026bec130e225d758dd2208cd244069984c Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Fri, 9 Oct 2026 01:43:08 +0300 Subject: [PATCH 06/28] Mock every installed backend --- pytest_httpx/__init__.py | 59 +++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py index 119faa2..3f3a4aa 100644 --- a/pytest_httpx/__init__.py +++ b/pytest_httpx/__init__.py @@ -4,7 +4,7 @@ import pytest from pytest import Config, FixtureRequest, MonkeyPatch -from pytest_httpx._compat import httpx +from pytest_httpx._httpx_compat import HttpxBackend, get_backends from pytest_httpx._httpx_internals import IteratorStream from pytest_httpx._httpx_mock import HTTPXMock from pytest_httpx._options import _HTTPXMockOptions @@ -17,51 +17,60 @@ ) -@pytest.fixture -def httpx_mock( +def _mock_backend( monkeypatch: MonkeyPatch, - request: FixtureRequest, -) -> Generator[HTTPXMock, None, None]: - httpx_mock_markers: dict = {} - for marker in request.node.iter_markers("httpx_mock"): - httpx_mock_markers = marker.kwargs | httpx_mock_markers - __tracebackhide__ = methodcaller("errisinstance", TypeError) - options = _HTTPXMockOptions(**httpx_mock_markers) - - mock = HTTPXMock(options) - + options: _HTTPXMockOptions, + mock: HTTPXMock, + backend: HttpxBackend, +) -> None: # Mock synchronous requests - real_handle_request = httpx.HTTPTransport.handle_request + real_handle_request = backend.httpx.HTTPTransport.handle_request - def mocked_handle_request( - transport: httpx.HTTPTransport, request: httpx.Request - ) -> httpx.Response: + def mocked_handle_request(transport, request): # type: ignore[no-untyped-def] if options.should_mock(request): - return mock._handle_request(transport, request) + return mock._handle_request(backend, transport, request) return real_handle_request(transport, request) monkeypatch.setattr( - httpx.HTTPTransport, + backend.httpx.HTTPTransport, "handle_request", mocked_handle_request, ) # Mock asynchronous requests - real_handle_async_request = httpx.AsyncHTTPTransport.handle_async_request + real_handle_async_request = backend.httpx.AsyncHTTPTransport.handle_async_request - async def mocked_handle_async_request( - transport: httpx.AsyncHTTPTransport, request: httpx.Request - ) -> httpx.Response: + async def mocked_handle_async_request(transport, request): # type: ignore[no-untyped-def] if options.should_mock(request): - return await mock._handle_async_request(transport, request) + return await mock._handle_async_request(backend, transport, request) return await real_handle_async_request(transport, request) monkeypatch.setattr( - httpx.AsyncHTTPTransport, + backend.httpx.AsyncHTTPTransport, "handle_async_request", mocked_handle_async_request, ) + +@pytest.fixture +def httpx_mock( + monkeypatch: MonkeyPatch, + request: FixtureRequest, +) -> Generator[HTTPXMock, None, None]: + httpx_mock_markers: dict = {} + + for marker in request.node.iter_markers("httpx_mock"): + httpx_mock_markers = marker.kwargs | httpx_mock_markers + + __tracebackhide__ = methodcaller("errisinstance", TypeError) + options = _HTTPXMockOptions(**httpx_mock_markers) + + mock = HTTPXMock(options) + + # Mock sync and async transports of every installed httpx backend. + for backend in get_backends(): + _mock_backend(monkeypatch, options, mock, backend) + yield mock try: mock._assert_options() From be54b13cce8aa61fc738e231ce473af372e9e9c3 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Fri, 9 Oct 2026 19:35:51 +0300 Subject: [PATCH 07/28] Inject backends throughout the mocking machinery --- pytest_httpx/_compat.py | 31 -------------- pytest_httpx/_httpx_internals.py | 31 +++++++++----- pytest_httpx/_httpx_mock.py | 72 ++++++++++++++++++++++++-------- pytest_httpx/_options.py | 2 +- pytest_httpx/_pretty_print.py | 6 ++- pytest_httpx/_request_matcher.py | 35 ++++++++++------ 6 files changed, 103 insertions(+), 74 deletions(-) delete mode 100644 pytest_httpx/_compat.py diff --git a/pytest_httpx/_compat.py b/pytest_httpx/_compat.py deleted file mode 100644 index 2ec88d4..0000000 --- a/pytest_httpx/_compat.py +++ /dev/null @@ -1,31 +0,0 @@ -import warnings -from typing import TYPE_CHECKING - - -class PytestHTTPXDeprecationWarning(UserWarning): - pass - - -if TYPE_CHECKING: - import httpcore2 as httpcore - import httpx2 as httpx -else: - try: - import httpcore2 as httpcore - import httpx2 as httpx - except ModuleNotFoundError: - try: - import httpcore # noqa: F401 - import httpx # noqa: F401 - except ModuleNotFoundError: - raise RuntimeError( - "pytest-httpx requires the httpx2 package to be installed.\n" - "You can install it with:\n" - " $ pip install httpx2\n" - ) from None - else: - warnings.warn( - "Using `httpx` with pytest-httpx is deprecated; install `httpx2` instead.", - PytestHTTPXDeprecationWarning, - stacklevel=2, - ) diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index 365b52e..15b3a81 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -2,7 +2,7 @@ from collections.abc import AsyncIterator, Iterable, Iterator, Sequence from typing import TypeAlias -from pytest_httpx._compat import httpcore, httpx +from pytest_httpx._httpx_compat import HttpxBackend, get_backends, httpcore, httpx # Those types are internally defined within httpx._types HeaderTypes: TypeAlias = ( @@ -15,10 +15,17 @@ PrimitiveData: TypeAlias = str | int | float | bool | None -class IteratorStream( - httpx.AsyncByteStream, - httpx.SyncByteStream, -): +def _iterator_stream_bases() -> list[type]: + bases: list[type] = [] + + for backend in get_backends(): + bases.append(backend.httpx.SyncByteStream) + bases.append(backend.httpx.AsyncByteStream) + + return bases + + +class IteratorStream(*_iterator_stream_bases()): # type: ignore[misc] def __init__(self, stream: Iterable[bytes]): self._stream = stream @@ -30,10 +37,12 @@ async def __aiter__(self) -> AsyncIterator[bytes]: yield chunk -def _to_httpx_url(url: httpcore.URL, headers: list[tuple[bytes, bytes]]) -> httpx.URL: +def _to_httpx_url( + backend: HttpxBackend, url: httpcore.URL, headers: list[tuple[bytes, bytes]] +) -> httpx.URL: for name, value in headers: if b"Proxy-Authorization" == name: - return httpx.URL( + return backend.httpx.URL( scheme=url.scheme.decode(), host=url.host.decode(), port=url.port, @@ -41,7 +50,7 @@ def _to_httpx_url(url: httpcore.URL, headers: list[tuple[bytes, bytes]]) -> http userinfo=base64.b64decode(value[6:]), ) - return httpx.URL( + return backend.httpx.URL( scheme=url.scheme.decode(), host=url.host.decode(), port=url.port, @@ -50,10 +59,12 @@ def _to_httpx_url(url: httpcore.URL, headers: list[tuple[bytes, bytes]]) -> http def _proxy_url( + backend: HttpxBackend, real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, ) -> httpx.URL | None: if isinstance( - real_pool := real_transport._pool, (httpcore.HTTPProxy, httpcore.AsyncHTTPProxy) + real_pool := real_transport._pool, + (backend.httpcore.HTTPProxy, backend.httpcore.AsyncHTTPProxy), ): - return _to_httpx_url(real_pool._proxy_url, real_pool._proxy_headers) + return _to_httpx_url(backend, real_pool._proxy_url, real_pool._proxy_headers) return None diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 40fc3ff..8eb27da 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -4,7 +4,7 @@ from typing import Any from pytest_httpx import _httpx_internals -from pytest_httpx._compat import httpx +from pytest_httpx._httpx_compat import HttpxBackend, httpx from pytest_httpx._options import _HTTPXMockOptions from pytest_httpx._pretty_print import RequestDescription from pytest_httpx._request_matcher import _RequestMatcher @@ -74,7 +74,10 @@ def add_response( json = copy.deepcopy(json) if json is not None else None def response_callback(request: httpx.Request) -> httpx.Response: - return httpx.Response( + # Build the response with the backend that intercepted the request + # so that its type matches the client. + backend = _get_backend(request) + return backend.httpx.Response( status_code=status_code, extensions={"http_version": http_version.encode("ascii")}, headers=headers, @@ -139,7 +142,9 @@ def add_exception(self, exception: BaseException, **matchers: Any) -> None: """ def exception_callback(request: httpx.Request) -> None: - if isinstance(exception, httpx.RequestError): + backend = _get_backend(request) + + if isinstance(exception, backend.httpx.RequestError): exception.request = request raise exception @@ -147,82 +152,100 @@ def exception_callback(request: httpx.Request) -> None: def _handle_request( self, + backend: HttpxBackend, real_transport: httpx.HTTPTransport, request: httpx.Request, ) -> httpx.Response: + # Save the backend that intercepted this request so that callbacks + # use the matching library. + _set_backend(request, backend) + # Store the content in request for future matching request.read() self._requests.append((real_transport, request)) - callback = self._get_callback(real_transport, request) + callback = self._get_callback(backend, real_transport, request) if callback: response = callback(request) - if isinstance(response, httpx.Response): + if isinstance(response, backend.httpx.Response): return _unread(response) raise self._request_not_matched( + backend, request, self._explain_that_callback_must_return_a_response( - real_transport, request + backend, real_transport, request ), ) raise self._request_not_matched( - request, self._explain_that_no_response_was_found(real_transport, request) + backend, + request, + self._explain_that_no_response_was_found(backend, real_transport, request), ) async def _handle_async_request( self, + backend: HttpxBackend, real_transport: httpx.AsyncHTTPTransport, request: httpx.Request, ) -> httpx.Response: + # Save the backend that intercepted this request so that callbacks + # use the matching library. + _set_backend(request, backend) + # Store the content in request for future matching await request.aread() self._requests.append((real_transport, request)) - callback = self._get_callback(real_transport, request) + callback = self._get_callback(backend, real_transport, request) if callback: response = callback(request) if inspect.isawaitable(response): response = await response - if isinstance(response, httpx.Response): + if isinstance(response, backend.httpx.Response): return _unread(response) raise self._request_not_matched( + backend, request, self._explain_that_callback_must_return_a_response( - real_transport, request + backend, real_transport, request ), ) raise self._request_not_matched( - request, self._explain_that_no_response_was_found(real_transport, request) + backend, + request, + self._explain_that_no_response_was_found(backend, real_transport, request), ) def _request_not_matched( - self, request: httpx.Request, message: str + self, backend: HttpxBackend, request: httpx.Request, message: str ) -> httpx.TimeoutException: self._requests_not_matched.append(request) - return httpx.TimeoutException(message, request=request) + return backend.httpx.TimeoutException(message, request=request) def _explain_that_callback_must_return_a_response( self, + backend: HttpxBackend, real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, request: httpx.Request, ) -> str: - return f"Callback registered for {RequestDescription(real_transport, request, [])} MUST return httpx.Response" + return f"Callback registered for {RequestDescription(backend, real_transport, request, [])} MUST return httpx.Response" def _explain_that_no_response_was_found( self, + backend: HttpxBackend, real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, request: httpx.Request, ) -> str: matchers = [matcher for matcher, _ in self._callbacks] - message = f"No response can be found for {RequestDescription(real_transport, request, matchers)}" + message = f"No response can be found for {RequestDescription(backend, real_transport, request, matchers)}" already_matched = [] unmatched = [] @@ -246,6 +269,7 @@ def _explain_that_no_response_was_found( def _get_callback( self, + backend: HttpxBackend, real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, request: httpx.Request, ) -> ( @@ -257,7 +281,7 @@ def _get_callback( callbacks = [ (matcher, callback) for matcher, callback in self._callbacks - if matcher.match(real_transport, request) + if matcher.match(backend, real_transport, request) ] # No callback match this request @@ -300,7 +324,7 @@ def get_requests(self, **matchers: Any) -> list[httpx.Request]: return [ request for real_transport, request in self._requests - if matcher.match(real_transport, request) + if matcher.match(_get_backend(request), real_transport, request) ] def get_request(self, **matchers: Any) -> httpx.Request | None: @@ -362,6 +386,20 @@ def _assert_options(self) -> None: ) +_BACKEND_ATTRIBUTE = "__pytest_httpx_backend" + + +def _set_backend(request: httpx.Request, backend: HttpxBackend) -> None: + # Store the intercepting backend on the request so that backend-agnostic + # callbacks (e.g. the one built by `add_response`) can build a response + # with the matching library. + setattr(request, _BACKEND_ATTRIBUTE, backend) + + +def _get_backend(request: httpx.Request) -> HttpxBackend: + return getattr(request, _BACKEND_ATTRIBUTE) + + def _unread(response: httpx.Response) -> httpx.Response: # Allow to read the response on client side response.is_stream_consumed = False diff --git a/pytest_httpx/_options.py b/pytest_httpx/_options.py index b1cfeef..bab80db 100644 --- a/pytest_httpx/_options.py +++ b/pytest_httpx/_options.py @@ -1,6 +1,6 @@ from collections.abc import Callable -from pytest_httpx._compat import httpx +from pytest_httpx._httpx_compat import httpx class _HTTPXMockOptions: diff --git a/pytest_httpx/_pretty_print.py b/pytest_httpx/_pretty_print.py index bfdd3c6..c032d2f 100644 --- a/pytest_httpx/_pretty_print.py +++ b/pytest_httpx/_pretty_print.py @@ -1,4 +1,4 @@ -from pytest_httpx._compat import httpx +from pytest_httpx._httpx_compat import HttpxBackend, httpx from pytest_httpx._httpx_internals import _proxy_url from pytest_httpx._request_matcher import _RequestMatcher @@ -6,10 +6,12 @@ class RequestDescription: def __init__( self, + backend: HttpxBackend, real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, request: httpx.Request, matchers: list[_RequestMatcher], ): + self.backend = backend self.real_transport = real_transport self.request = request @@ -57,7 +59,7 @@ def extra_request_description(self) -> str: extra_description.append(f"{self.request.read()} body") if self.expect_proxy: - proxy_url = _proxy_url(self.real_transport) + proxy_url = _proxy_url(self.backend, self.real_transport) extra_description.append(f"{proxy_url if proxy_url else 'no'} proxy URL") if self.expected_extensions: diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index 86d6830..81aa00c 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import ANY -from pytest_httpx._compat import httpx +from pytest_httpx._httpx_compat import HttpxBackend, httpx from pytest_httpx._httpx_internals import PrimitiveData, _proxy_url from pytest_httpx._options import _HTTPXMockOptions @@ -28,17 +28,23 @@ def convert_back_mock_any(original: dict, new: dict) -> None: def _url_match( - url_to_match: Pattern[str] | httpx.URL, + backend: HttpxBackend, + url_to_match: Pattern[str] | httpx.UR, received: httpx.URL, params: dict[str, PrimitiveData | Sequence[PrimitiveData]] | None, ) -> bool: if isinstance(url_to_match, re.Pattern): return url_to_match.match(str(received)) is not None + # The expected URL may have been built with a different backend than the one + # that received the request, so normalize it to the request's backend before + # comparing (URLs from different backends are never equal to each other). + url_to_match = backend.httpx.URL(str(url_to_match)) + # Compare query parameters apart as order of parameters should not matter received_params = to_params_dict(received.params) expected_params = to_params_dict( - url_to_match.params if params is None else httpx.QueryParams(params) + url_to_match.params if params is None else backend.httpx.QueryParams(params) ) if params: convert_back_mock_any(params, expected_params) @@ -155,23 +161,24 @@ def _is_matching_params_more_than_one_way(self) -> bool: def match( self, + backend: HttpxBackend, real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, request: httpx.Request, ) -> bool: return ( - self._url_match(request) + self._url_match(backend, request) and self._method_match(request) and self._headers_match(request) - and self._content_match(request) - and self._proxy_match(real_transport) + and self._content_match(backend, request) + and self._proxy_match(backend, real_transport) and self._extensions_match(request) ) - def _url_match(self, request: httpx.Request) -> bool: + def _url_match(self, backend: HttpxBackend, request: httpx.Request) -> bool: if not self.url: return True - return _url_match(self.url, request.url, self.params) + return _url_match(backend, self.url, request.url, self.params) def _method_match(self, request: httpx.Request) -> bool: if not self.method: @@ -198,7 +205,7 @@ def _headers_match(self, request: httpx.Request) -> bool: for header_name, header_value in self.headers.items() ) - def _content_match(self, request: httpx.Request) -> bool: + def _content_match(self, backend: HttpxBackend, request: httpx.Request) -> bool: if self.content is not None: return request.content == self.content @@ -217,7 +224,7 @@ def _content_match(self, request: httpx.Request) -> bool: # Ensure we re-use the same boundary for comparison boundary = boundary_matched.group(1) # Prevent internal httpx changes from impacting users not matching on files - from httpx._multipart import MultipartStream + MultipartStream = backend.httpx._multipart.MultipartStream multipart_content = b"".join( MultipartStream(self.data or {}, self.files, boundary) @@ -227,13 +234,15 @@ def _content_match(self, request: httpx.Request) -> bool: return True def _proxy_match( - self, real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport + self, + backend: HttpxBackend, + real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, ) -> bool: if not self.proxy_url: return True - if real_proxy_url := _proxy_url(real_transport): - return _url_match(self.proxy_url, real_proxy_url, params=None) + if real_proxy_url := _proxy_url(backend, real_transport): + return _url_match(backend, self.proxy_url, real_proxy_url, params=None) return False From 6100c005c238831369a2f30b5b07d6842826ddf3 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Fri, 9 Oct 2026 21:07:28 +0300 Subject: [PATCH 08/28] Rename HttpxBackend -> HTTPBackend --- pytest_httpx/__init__.py | 6 ++++-- pytest_httpx/_httpx_compat.py | 22 +++++++++++----------- pytest_httpx/_httpx_internals.py | 6 +++--- pytest_httpx/_httpx_mock.py | 18 +++++++++--------- pytest_httpx/_pretty_print.py | 4 ++-- pytest_httpx/_request_matcher.py | 12 ++++++------ 6 files changed, 35 insertions(+), 33 deletions(-) diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py index 3f3a4aa..95ce3cc 100644 --- a/pytest_httpx/__init__.py +++ b/pytest_httpx/__init__.py @@ -4,16 +4,18 @@ import pytest from pytest import Config, FixtureRequest, MonkeyPatch -from pytest_httpx._httpx_compat import HttpxBackend, get_backends +from pytest_httpx._httpx_compat import HTTPXBackend, get_backends from pytest_httpx._httpx_internals import IteratorStream from pytest_httpx._httpx_mock import HTTPXMock from pytest_httpx._options import _HTTPXMockOptions from pytest_httpx.version import __version__ __all__ = ( + "HTTPXBackend", "HTTPXMock", "IteratorStream", "__version__", + "get_backends", ) @@ -21,7 +23,7 @@ def _mock_backend( monkeypatch: MonkeyPatch, options: _HTTPXMockOptions, mock: HTTPXMock, - backend: HttpxBackend, + backend: HTTPXBackend, ) -> None: # Mock synchronous requests real_handle_request = backend.httpx.HTTPTransport.handle_request diff --git a/pytest_httpx/_httpx_compat.py b/pytest_httpx/_httpx_compat.py index 2121ec3..9609679 100644 --- a/pytest_httpx/_httpx_compat.py +++ b/pytest_httpx/_httpx_compat.py @@ -10,35 +10,35 @@ class PytestHTTPXDeprecationWarning(UserWarning): @dataclass(frozen=True) -class HttpxBackend: +class HTTPXBackend: name: Literal["httpx", "httpx2"] httpx: ModuleType httpcore: ModuleType @cache -def get_backends() -> list[HttpxBackend]: +def get_backends() -> list[HTTPXBackend]: """Return every httpx backend found in the environment.""" backends = [] try: - import httpx2 import httpcore2 - except ModuleNotFoundError: + import httpx2 + except ModuleNotFoundError: # pragma: no cover pass else: - backends.append(HttpxBackend("httpx2", httpx2, httpcore2)) + backends.append(HTTPXBackend("httpx2", httpx2, httpcore2)) try: - import httpx as httpx_ import httpcore as httpcore_ - except ModuleNotFoundError: + import httpx as httpx_ + except ModuleNotFoundError: # pragma: no cover pass else: - backends.append(HttpxBackend("httpx", httpx_, httpcore_)) + backends.append(HTTPXBackend("httpx", httpx_, httpcore_)) - if not backends: + if not backends: # pragma: no cover raise RuntimeError( "pytest-httpx requires either httpx or httpx2 to be installed.\n" "They are now optional dependencies, install the matching extra with:\n" @@ -51,8 +51,8 @@ def get_backends() -> list[HttpxBackend]: if TYPE_CHECKING: - import httpx2 as httpx import httpcore2 as httpcore + import httpx2 as httpx else: # A module-level backend for global httpx/httpcore names references. _backends = get_backends() @@ -60,7 +60,7 @@ def get_backends() -> list[HttpxBackend]: httpx = _default_backend.httpx httpcore = _default_backend.httpcore - if any(backend.name == "httpx" for backend in _backends): + if any(backend.name == "httpx" for backend in _backends): # pragma: no cover warnings.warn( "Using httpx with pytest-httpx is deprecated; consider using httpx2 instead.", PytestHTTPXDeprecationWarning, diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index 15b3a81..5b26b8f 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -2,7 +2,7 @@ from collections.abc import AsyncIterator, Iterable, Iterator, Sequence from typing import TypeAlias -from pytest_httpx._httpx_compat import HttpxBackend, get_backends, httpcore, httpx +from pytest_httpx._httpx_compat import HTTPXBackend, get_backends, httpcore, httpx # Those types are internally defined within httpx._types HeaderTypes: TypeAlias = ( @@ -38,7 +38,7 @@ async def __aiter__(self) -> AsyncIterator[bytes]: def _to_httpx_url( - backend: HttpxBackend, url: httpcore.URL, headers: list[tuple[bytes, bytes]] + backend: HTTPXBackend, url: httpcore.URL, headers: list[tuple[bytes, bytes]] ) -> httpx.URL: for name, value in headers: if b"Proxy-Authorization" == name: @@ -59,7 +59,7 @@ def _to_httpx_url( def _proxy_url( - backend: HttpxBackend, + backend: HTTPXBackend, real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, ) -> httpx.URL | None: if isinstance( diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 8eb27da..311dffe 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -4,7 +4,7 @@ from typing import Any from pytest_httpx import _httpx_internals -from pytest_httpx._httpx_compat import HttpxBackend, httpx +from pytest_httpx._httpx_compat import HTTPXBackend, httpx from pytest_httpx._options import _HTTPXMockOptions from pytest_httpx._pretty_print import RequestDescription from pytest_httpx._request_matcher import _RequestMatcher @@ -152,7 +152,7 @@ def exception_callback(request: httpx.Request) -> None: def _handle_request( self, - backend: HttpxBackend, + backend: HTTPXBackend, real_transport: httpx.HTTPTransport, request: httpx.Request, ) -> httpx.Response: @@ -187,7 +187,7 @@ def _handle_request( async def _handle_async_request( self, - backend: HttpxBackend, + backend: HTTPXBackend, real_transport: httpx.AsyncHTTPTransport, request: httpx.Request, ) -> httpx.Response: @@ -224,14 +224,14 @@ async def _handle_async_request( ) def _request_not_matched( - self, backend: HttpxBackend, request: httpx.Request, message: str + self, backend: HTTPXBackend, request: httpx.Request, message: str ) -> httpx.TimeoutException: self._requests_not_matched.append(request) return backend.httpx.TimeoutException(message, request=request) def _explain_that_callback_must_return_a_response( self, - backend: HttpxBackend, + backend: HTTPXBackend, real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, request: httpx.Request, ) -> str: @@ -239,7 +239,7 @@ def _explain_that_callback_must_return_a_response( def _explain_that_no_response_was_found( self, - backend: HttpxBackend, + backend: HTTPXBackend, real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, request: httpx.Request, ) -> str: @@ -269,7 +269,7 @@ def _explain_that_no_response_was_found( def _get_callback( self, - backend: HttpxBackend, + backend: HTTPXBackend, real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, request: httpx.Request, ) -> ( @@ -389,14 +389,14 @@ def _assert_options(self) -> None: _BACKEND_ATTRIBUTE = "__pytest_httpx_backend" -def _set_backend(request: httpx.Request, backend: HttpxBackend) -> None: +def _set_backend(request: httpx.Request, backend: HTTPXBackend) -> None: # Store the intercepting backend on the request so that backend-agnostic # callbacks (e.g. the one built by `add_response`) can build a response # with the matching library. setattr(request, _BACKEND_ATTRIBUTE, backend) -def _get_backend(request: httpx.Request) -> HttpxBackend: +def _get_backend(request: httpx.Request) -> HTTPXBackend: return getattr(request, _BACKEND_ATTRIBUTE) diff --git a/pytest_httpx/_pretty_print.py b/pytest_httpx/_pretty_print.py index c032d2f..5d1acbb 100644 --- a/pytest_httpx/_pretty_print.py +++ b/pytest_httpx/_pretty_print.py @@ -1,4 +1,4 @@ -from pytest_httpx._httpx_compat import HttpxBackend, httpx +from pytest_httpx._httpx_compat import HTTPXBackend, httpx from pytest_httpx._httpx_internals import _proxy_url from pytest_httpx._request_matcher import _RequestMatcher @@ -6,7 +6,7 @@ class RequestDescription: def __init__( self, - backend: HttpxBackend, + backend: HTTPXBackend, real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, request: httpx.Request, matchers: list[_RequestMatcher], diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index 81aa00c..3c4e9a7 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import ANY -from pytest_httpx._httpx_compat import HttpxBackend, httpx +from pytest_httpx._httpx_compat import HTTPXBackend, httpx from pytest_httpx._httpx_internals import PrimitiveData, _proxy_url from pytest_httpx._options import _HTTPXMockOptions @@ -28,7 +28,7 @@ def convert_back_mock_any(original: dict, new: dict) -> None: def _url_match( - backend: HttpxBackend, + backend: HTTPXBackend, url_to_match: Pattern[str] | httpx.UR, received: httpx.URL, params: dict[str, PrimitiveData | Sequence[PrimitiveData]] | None, @@ -161,7 +161,7 @@ def _is_matching_params_more_than_one_way(self) -> bool: def match( self, - backend: HttpxBackend, + backend: HTTPXBackend, real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, request: httpx.Request, ) -> bool: @@ -174,7 +174,7 @@ def match( and self._extensions_match(request) ) - def _url_match(self, backend: HttpxBackend, request: httpx.Request) -> bool: + def _url_match(self, backend: HTTPXBackend, request: httpx.Request) -> bool: if not self.url: return True @@ -205,7 +205,7 @@ def _headers_match(self, request: httpx.Request) -> bool: for header_name, header_value in self.headers.items() ) - def _content_match(self, backend: HttpxBackend, request: httpx.Request) -> bool: + def _content_match(self, backend: HTTPXBackend, request: httpx.Request) -> bool: if self.content is not None: return request.content == self.content @@ -235,7 +235,7 @@ def _content_match(self, backend: HttpxBackend, request: httpx.Request) -> bool: def _proxy_match( self, - backend: HttpxBackend, + backend: HTTPXBackend, real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, ) -> bool: if not self.proxy_url: From 70a1905d622e6287f5649861146bd8f6ba0b3766 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Mon, 12 Oct 2026 18:00:48 +0300 Subject: [PATCH 09/28] Sync and async HTTPX tests pass --- tests/conftest.py | 9 + tests/test_httpx_async.py | 1223 +++++++++++++++++++++++-------------- tests/test_httpx_sync.py | 1043 ++++++++++++++++++------------- 3 files changed, 1385 insertions(+), 890 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4c58045..7ff5fef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,2 +1,11 @@ # see https://docs.pytest.org/en/documentation-restructure/how-to/writing_plugins.html#testing-plugins +import pytest + +from pytest_httpx import HTTPXBackend, get_backends + pytest_plugins = ["pytester"] + + +@pytest.fixture(params=get_backends(), ids=lambda b: b.name) +def backend(request: pytest.FixtureRequest) -> HTTPXBackend: + return request.param diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index 451cf18..6361d77 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -4,22 +4,27 @@ import re import time from collections.abc import AsyncIterable -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import ANY -import httpx import pytest from pytest import Testdir import pytest_httpx -from pytest_httpx import HTTPXMock +from pytest_httpx import HTTPXBackend, HTTPXMock + +if TYPE_CHECKING: + try: + import httpx + except ModuleNotFoundError: + import httpx2 as httpx @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_without_response(httpx_mock: HTTPXMock) -> None: +async def test_without_response(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: with pytest.raises(Exception) as exception_info: - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://test_url") assert ( str(exception_info.value) @@ -28,31 +33,33 @@ async def test_without_response(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_default_response(httpx_mock: HTTPXMock) -> None: +async def test_default_response(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response() - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" assert response.status_code == 200 - assert response.headers == httpx.Headers({}) + assert response.headers == backend.httpx.Headers({}) assert response.http_version == "HTTP/1.1" @pytest.mark.asyncio -async def test_url_matching(httpx_mock: HTTPXMock) -> None: +async def test_url_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(url="https://test_url") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" @pytest.mark.asyncio -async def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None: +async def test_url_matching_reusing_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" @@ -61,10 +68,12 @@ async def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: +async def test_url_query_string_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url?a=1&b=2", is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url?a=1&b=2") assert response.content == b"" @@ -74,14 +83,16 @@ async def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_url_query_params_partial_matching(httpx_mock: HTTPXMock) -> None: +async def test_url_query_params_partial_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( - url=httpx.URL("https://test_url"), + url=backend.httpx.URL("https://test_url"), match_params={"a": ["1", "3"], "b": ANY, "c": "4", "d": ["5", ANY], "e": [ANY]}, is_reusable=True, ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url?a=1&b=2&a=3&c=4&d=5&d=6&e=7") assert response.content == b"" @@ -91,51 +102,59 @@ async def test_url_query_params_partial_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_url_as_pattern_ignoring_query_parameters(httpx_mock: HTTPXMock): +async def test_url_as_pattern_ignoring_query_parameters( + httpx_mock: HTTPXMock, backend: HTTPXBackend +): httpx_mock.add_response(url=re.compile("https://test_url/something.*")) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url/something?a=1&b=2") assert response.content == b"" @pytest.mark.asyncio -async def test_url_query_params_with_single_value_list(httpx_mock: HTTPXMock) -> None: +async def test_url_query_params_with_single_value_list( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", match_params={"a": ["1"]}, is_optional=True, ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url?a=1") assert response.content == b"" @pytest.mark.asyncio -async def test_url_query_params_with_non_str_name(httpx_mock: HTTPXMock) -> None: +async def test_url_query_params_with_non_str_name( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", match_params={1: "1"}, is_optional=True, ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url?1=1") assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_url_query_params_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_url_query_params_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", match_params={"a": "1"}, is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url?a=2") assert ( str(exception_info.value) @@ -201,10 +220,11 @@ async def test_match_params_with_non_str_values_and_params_provided_as_dict( httpx_mock: HTTPXMock, match_params: dict[str, Any], request_params: dict[str, Any], + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url", match_params=match_params) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url", params=request_params) assert response.content == b"" @@ -250,10 +270,11 @@ async def test_match_params_with_non_str_values_and_params_in_requested_url( httpx_mock: HTTPXMock, match_params: dict[str, Any], url: str, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url", match_params=match_params) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get(url) assert response.content == b"" @@ -261,10 +282,11 @@ async def test_match_params_with_non_str_values_and_params_in_requested_url( @pytest.mark.asyncio async def test_url_matching_with_more_than_one_value_on_same_param( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=3", is_optional=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url", params={"a": [1, 3]}) assert response.content == b"" @@ -273,11 +295,12 @@ async def test_url_matching_with_more_than_one_value_on_same_param( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_not_matching_with_more_than_one_value_on_same_param_and_diff_value( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url?a=2&a=3", is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url", params={"a": [1, 3]}) assert ( str(exception_info.value) @@ -290,11 +313,12 @@ async def test_url_not_matching_with_more_than_one_value_on_same_param_and_diff_ @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_not_matching_with_more_than_one_value_on_same_param_and_more_values( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=3", is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url", params={"a": [1, 3, 4]}) assert ( str(exception_info.value) @@ -307,11 +331,12 @@ async def test_url_not_matching_with_more_than_one_value_on_same_param_and_more_ @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_not_matching_with_more_than_one_value_on_same_param_and_less_values( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=3&a=4", is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url", params={"a": [1, 3]}) assert ( str(exception_info.value) @@ -322,11 +347,11 @@ async def test_url_not_matching_with_more_than_one_value_on_same_param_and_less_ @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_url_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_url_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(url="https://test_url", is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url2") assert ( str(exception_info.value) @@ -337,11 +362,13 @@ async def test_url_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_url_query_string_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=2", is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: # Same parameter order matters as it corresponds to a list on server side await client.get("https://test_url?a=2&a=1") assert ( @@ -352,10 +379,10 @@ async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_method_matching(httpx_mock: HTTPXMock) -> None: +async def test_method_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(method="get", is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" @@ -365,11 +392,13 @@ async def test_method_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_method_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_method_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(method="get", is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url") assert ( str(exception_info.value) @@ -379,12 +408,14 @@ async def test_method_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_reusing_one_response(httpx_mock: HTTPXMock) -> None: +async def test_reusing_one_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", content=b"test content", is_reusable=True ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content" @@ -393,39 +424,45 @@ async def test_reusing_one_response(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_response_with_string_body(httpx_mock: HTTPXMock) -> None: +async def test_response_with_string_body( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", text="test content") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content" @pytest.mark.asyncio -async def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None: +async def test_response_with_html_string_body( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", html="test content") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content" @pytest.mark.asyncio -async def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None: +async def test_stream_response_streaming( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", stream=pytest_httpx.IteratorStream([b"part 1", b"part 2"]), is_reusable=True, ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [ b"part 1", b"part 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @@ -435,26 +472,28 @@ async def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None: b"part 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @pytest.mark.asyncio -async def test_content_response_streaming(httpx_mock: HTTPXMock) -> None: +async def test_content_response_streaming( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", content=b"part 1 and 2", is_reusable=True, ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [ b"part 1 and 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @@ -463,26 +502,28 @@ async def test_content_response_streaming(httpx_mock: HTTPXMock) -> None: b"part 1 and 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @pytest.mark.asyncio -async def test_text_response_streaming(httpx_mock: HTTPXMock) -> None: +async def test_text_response_streaming( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", text="part 1 and 2", is_reusable=True, ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [ b"part 1 and 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @@ -491,38 +532,42 @@ async def test_text_response_streaming(httpx_mock: HTTPXMock) -> None: b"part 1 and 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @pytest.mark.asyncio -async def test_default_response_streaming(httpx_mock: HTTPXMock) -> None: +async def test_default_response_streaming( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @pytest.mark.asyncio -async def test_with_many_responses(httpx_mock: HTTPXMock) -> None: +async def test_with_many_responses( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content 1") httpx_mock.add_response(url="https://test_url", content=b"test content 2") httpx_mock.add_response(url="https://test_url", content=b"test content 2") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content 1" @@ -534,13 +579,15 @@ async def test_with_many_responses(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None: +async def test_with_many_reused_responses( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content 1") httpx_mock.add_response( url="https://test_url", content=b"test content 2", is_reusable=True ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content 1" @@ -552,7 +599,9 @@ async def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None: +async def test_with_many_responses_methods( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1" ) @@ -572,7 +621,7 @@ async def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None: url="https://test_url", method="HEAD", content=b"test content 6" ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url") assert response.content == b"test content 2" @@ -593,7 +642,9 @@ async def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None: +async def test_with_many_responses_status_codes( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1", status_code=200 ) @@ -625,7 +676,7 @@ async def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None: status_code=500, ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url") assert response.content == b"test content 2" assert response.status_code == 201 @@ -652,7 +703,9 @@ async def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_with_many_responses_urls_str(httpx_mock: HTTPXMock) -> None: +async def test_with_many_responses_urls_str( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url?param1=test", method="GET", content=b"test content 1" ) @@ -672,44 +725,46 @@ async def test_with_many_responses_urls_str(httpx_mock: HTTPXMock) -> None: url="https://test_url?param6=test", method="HEAD", content=b"test content 6" ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post( - httpx.URL("https://test_url", params={"param2": "test"}) + backend.httpx.URL("https://test_url", params={"param2": "test"}) ) assert response.content == b"test content 2" response = await client.get( - httpx.URL("https://test_url", params={"param1": "test"}) + backend.httpx.URL("https://test_url", params={"param1": "test"}) ) assert response.content == b"test content 1" response = await client.put( - httpx.URL("https://test_url", params={"param3": "test"}) + backend.httpx.URL("https://test_url", params={"param3": "test"}) ) assert response.content == b"test content 3" response = await client.head( - httpx.URL("https://test_url", params={"param6": "test"}) + backend.httpx.URL("https://test_url", params={"param6": "test"}) ) assert response.content == b"test content 6" response = await client.patch( - httpx.URL("https://test_url", params={"param5": "test"}) + backend.httpx.URL("https://test_url", params={"param5": "test"}) ) assert response.content == b"test content 5" response = await client.delete( - httpx.URL("https://test_url", params={"param4": "test"}) + backend.httpx.URL("https://test_url", params={"param4": "test"}) ) assert response.content == b"test content 4" @pytest.mark.asyncio -async def test_response_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: +async def test_response_with_pattern_in_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url=re.compile(".*test.*")) httpx_mock.add_response(url="https://unmatched", content=b"test content") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://unmatched") assert response.content == b"test content" @@ -718,11 +773,13 @@ async def test_response_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_request_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: +async def test_request_with_pattern_in_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url") httpx_mock.add_response(url="https://unmatched") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://unmatched") await client.get("https://test_url", headers={"X-Test": "1"}) @@ -732,12 +789,14 @@ async def test_request_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_requests_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: +async def test_requests_with_pattern_in_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url") httpx_mock.add_response(url="https://tests_url") httpx_mock.add_response(url="https://unmatched") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://tests_url", headers={"X-Test": "1"}) await client.get("https://unmatched", headers={"X-Test": "2"}) await client.get("https://test_url") @@ -749,12 +808,14 @@ async def test_requests_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_callback_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json={"url": str(request.url)}) +async def test_callback_with_pattern_in_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json={"url": str(request.url)}) - def custom_response2(request: httpx.Request) -> httpx.Response: - return httpx.Response( + def custom_response2(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response( status_code=200, extensions={"http_version": b"HTTP/2.0"}, json={"url": str(request.url)}, @@ -763,7 +824,7 @@ def custom_response2(request: httpx.Request) -> httpx.Response: httpx_mock.add_callback(custom_response, url=re.compile(".*test.*")) httpx_mock.add_callback(custom_response2, url="https://unmatched") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://unmatched") assert response.http_version == "HTTP/2.0" @@ -772,16 +833,18 @@ def custom_response2(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_async_callback_with_await_statement(httpx_mock: HTTPXMock) -> None: - async def simulate_network_latency(request: httpx.Request): +async def test_async_callback_with_await_statement( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + async def simulate_network_latency(request: "httpx.Request"): await asyncio.sleep(1) - return httpx.Response( + return backend.httpx.Response( status_code=200, json={"url": str(request.url), "time": time.time()}, ) - def instant_response(request: httpx.Request) -> httpx.Response: - return httpx.Response( + def instant_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response( status_code=200, json={"url": str(request.url), "time": time.time()} ) @@ -789,7 +852,7 @@ def instant_response(request: httpx.Request) -> httpx.Response: httpx_mock.add_callback(instant_response) httpx_mock.add_response(json={"url": "not a callback"}) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: responses = await asyncio.gather( client.get("https://slow"), client.get("https://fast_with_callback"), @@ -809,12 +872,16 @@ def instant_response(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_async_callback_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: - async def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json={"url": str(request.url)}) +async def test_async_callback_with_pattern_in_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + async def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json={"url": str(request.url)}) - async def custom_response2(request: httpx.Request) -> httpx.Response: - return httpx.Response( + async def custom_response2( + request: "httpx.Request", + ) -> "httpx.Response": + return backend.httpx.Response( status_code=200, extensions={"http_version": b"HTTP/2.0"}, json={"url": str(request.url)}, @@ -823,7 +890,7 @@ async def custom_response2(request: httpx.Request) -> httpx.Response: httpx_mock.add_callback(custom_response, url=re.compile(".*test.*")) httpx_mock.add_callback(custom_response2, url="https://unmatched") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://unmatched") assert response.http_version == "HTTP/2.0" @@ -832,39 +899,42 @@ async def custom_response2(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_with_many_responses_urls_instances(httpx_mock: HTTPXMock) -> None: +async def test_with_many_responses_urls_instances( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param1": "test"}), + url=backend.httpx.URL("https://test_url", params={"param1": "test"}), method="GET", content=b"test content 1", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param2": "test"}), + url=backend.httpx.URL("https://test_url", params={"param2": "test"}), method="POST", content=b"test content 2", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param3": "test"}), + url=backend.httpx.URL("https://test_url", params={"param3": "test"}), method="PUT", content=b"test content 3", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param4": "test"}), + url=backend.httpx.URL("https://test_url", params={"param4": "test"}), method="DELETE", content=b"test content 4", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param5": "test"}), + url=backend.httpx.URL("https://test_url", params={"param5": "test"}), method="PATCH", content=b"test content 5", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param6": "test"}), + url=backend.httpx.URL("https://test_url", params={"param6": "test"}), method="HEAD", content=b"test content 6", ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url?param2=test") assert response.content == b"test content 2" @@ -885,35 +955,38 @@ async def test_with_many_responses_urls_instances(httpx_mock: HTTPXMock) -> None @pytest.mark.asyncio -async def test_with_http_version_2(httpx_mock: HTTPXMock) -> None: +async def test_with_http_version_2( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", http_version="HTTP/2", content=b"test content 1" ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content 1" assert response.http_version == "HTTP/2" @pytest.mark.asyncio -async def test_with_headers(httpx_mock: HTTPXMock) -> None: +async def test_with_headers(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( url="https://test_url", content=b"test content 1", headers={"X-Test": "Test value"}, ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content 1" - assert response.headers == httpx.Headers( + assert response.headers == backend.httpx.Headers( {"x-test": "Test value", "content-length": "14"} ) @pytest.mark.asyncio -async def test_requests_retrieval(httpx_mock: HTTPXMock) -> None: +async def test_requests_retrieval(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: + httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1" ) @@ -933,7 +1006,7 @@ async def test_requests_retrieval(httpx_mock: HTTPXMock) -> None: url="https://test_url", method="HEAD", content=b"test content 6" ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.post("https://test_url", content=b"sent content 2") await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.put("https://test_url", content=b"sent content 3") @@ -942,74 +1015,80 @@ async def test_requests_retrieval(httpx_mock: HTTPXMock) -> None: await client.delete("https://test_url", headers={"X-Test": "test header 4"}) patch_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="PATCH" + url=backend.httpx.URL("https://test_url"), method="PATCH" ) assert patch_request is not None assert patch_request.read() == b"sent content 5" head_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="HEAD" + url=backend.httpx.URL("https://test_url"), method="HEAD" ) assert head_request is not None assert head_request.read() == b"" put_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="PUT" + url=backend.httpx.URL("https://test_url"), method="PUT" ) assert put_request is not None assert put_request.read() == b"sent content 3" get_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="GET" + url=backend.httpx.URL("https://test_url"), method="GET" ) assert get_request is not None assert get_request.headers["x-test"] == "test header 1" post_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="POST" + url=backend.httpx.URL("https://test_url"), method="POST" ) assert post_request is not None assert post_request.read() == b"sent content 2" delete_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="DELETE" + url=backend.httpx.URL("https://test_url"), method="DELETE" ) assert delete_request is not None assert delete_request.headers["x-test"] == "test header 4" @pytest.mark.asyncio -async def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None: +async def test_requests_retrieval_on_same_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url", headers={"X-TEST": "test header 2"}) - requests = httpx_mock.get_requests(url=httpx.URL("https://test_url")) + requests = httpx_mock.get_requests(url=backend.httpx.URL("https://test_url")) assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" @pytest.mark.asyncio -async def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None: +async def test_request_retrieval_on_same_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url2", headers={"X-TEST": "test header 2"}) - request = httpx_mock.get_request(url=httpx.URL("https://test_url")) + request = httpx_mock.get_request(url=backend.httpx.URL("https://test_url")) assert request is not None assert request.headers["x-test"] == "test header 1" @pytest.mark.asyncio -async def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: +async def test_requests_retrieval_on_same_method( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url2", headers={"X-TEST": "test header 2"}) @@ -1020,10 +1099,12 @@ async def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: +async def test_request_retrieval_on_same_method( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.post("https://test_url", headers={"X-TEST": "test header 2"}) @@ -1033,26 +1114,32 @@ async def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock) -> None: +async def test_requests_retrieval_on_same_url_and_method( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url", headers={"X-TEST": "test header 2"}) await client.post("https://test_url", headers={"X-TEST": "test header 3"}) await client.get("https://test_url2", headers={"X-TEST": "test header 4"}) - requests = httpx_mock.get_requests(url=httpx.URL("https://test_url"), method="GET") + requests = httpx_mock.get_requests( + url=backend.httpx.URL("https://test_url"), method="GET" + ) assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" @pytest.mark.asyncio -async def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None: +async def test_default_requests_retrieval( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.post("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url2", headers={"X-TEST": "test header 2"}) @@ -1063,10 +1150,12 @@ async def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_default_request_retrieval(httpx_mock: HTTPXMock) -> None: +async def test_default_request_retrieval( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response() - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.post("https://test_url", headers={"X-TEST": "test header 1"}) request = httpx_mock.get_request() @@ -1075,7 +1164,7 @@ async def test_default_request_retrieval(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_requests_json_body(httpx_mock: HTTPXMock) -> None: +async def test_requests_json_body(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( url="https://test_url", method="GET", json=["list content 1", "list content 2"] ) @@ -1086,7 +1175,7 @@ async def test_requests_json_body(httpx_mock: HTTPXMock) -> None: ) httpx_mock.add_response(url="https://test_url", method="PUT", json="string value") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url") assert response.json() == {"key 1": "value 1", "key 2": "value 2"} assert response.headers["content-type"] == "application/json" @@ -1101,85 +1190,107 @@ async def test_requests_json_body(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_callback_raising_exception(httpx_mock: HTTPXMock) -> None: - def raise_timeout(request: httpx.Request) -> httpx.Response: - raise httpx.ReadTimeout( +async def test_callback_raising_exception( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + def raise_timeout(request: "httpx.Request") -> "httpx.Response": + raise backend.httpx.ReadTimeout( f"Unable to read within {request.extensions['timeout']['read']}", request=request, ) httpx_mock.add_callback(raise_timeout, url="https://test_url") - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.ReadTimeout) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.ReadTimeout) as exception_info: await client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" @pytest.mark.asyncio -async def test_async_callback_raising_exception(httpx_mock: HTTPXMock) -> None: - async def raise_timeout(request: httpx.Request) -> httpx.Response: - raise httpx.ReadTimeout( +async def test_async_callback_raising_exception( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + async def raise_timeout(request: "httpx.Request") -> "httpx.Response": + raise backend.httpx.ReadTimeout( f"Unable to read within {request.extensions['timeout']['read']}", request=request, ) httpx_mock.add_callback(raise_timeout, url="https://test_url") - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.ReadTimeout) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.ReadTimeout) as exception_info: await client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" @pytest.mark.asyncio -async def test_request_exception_raising(httpx_mock: HTTPXMock) -> None: +async def test_request_exception_raising( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_exception( - httpx.ReadTimeout("Unable to read within 5.0"), url="https://test_url" + backend.httpx.ReadTimeout("Unable to read within 5.0"), url="https://test_url" ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.ReadTimeout) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.ReadTimeout) as exception_info: await client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" assert exception_info.value.request is not None +@pytest.fixture +def _non_request_exception_raising_errors( + request: pytest.FixtureRequest, backend: HTTPXBackend +) -> tuple[type[BaseException], str]: + if request.param == "http": + return backend.httpx.HTTPError, "Unable to read within 5.0" + + if request.param == "cancelled": + return asyncio.CancelledError, "Request was cancelled" + + raise ValueError( + f'Invalid param {request.param}, must be either "http" or "cancelled".' + ) + + @pytest.mark.asyncio @pytest.mark.parametrize( - ("exception_type", "message"), + "_non_request_exception_raising_errors", [ # httpx exception without request context - pytest.param( - httpx.HTTPError, "Unable to read within 5.0", id="non_request_exception" - ), + pytest.param("http", id="non_request_exception"), # BaseException derived exception - pytest.param( - asyncio.CancelledError, - "Request was cancelled", - id="cancelled_exception", - ), + pytest.param("cancelled", id="cancelled_exception"), ], + indirect=True, ) async def test_non_request_exception_raising( - httpx_mock: HTTPXMock, exception_type: type, message: str + httpx_mock: HTTPXMock, _non_request_exception_raising_errors, backend: HTTPXBackend ) -> None: - httpx_mock.add_exception(exception_type(message), url="https://test_url") + [exc_type, message] = _non_request_exception_raising_errors + httpx_mock.add_exception(exc_type(message), url="https://test_url") - async with httpx.AsyncClient() as client: - with pytest.raises(exception_type) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(exc_type) as exception_info: await client.get("https://test_url") assert str(exception_info.value) == message @pytest.mark.asyncio -async def test_callback_returning_response(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json={"url": str(request.url)}) +async def test_callback_returning_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json={"url": str(request.url)}) httpx_mock.add_callback(custom_response, url="https://test_url") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == {"url": "https://test_url"} assert response.headers["content-type"] == "application/json" @@ -1189,12 +1300,12 @@ def custom_response(request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("return_value", [None, "not a response"]) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_callback_not_returning_a_response( - httpx_mock: HTTPXMock, return_value: str | None + httpx_mock: HTTPXMock, return_value: str | None, backend: HTTPXBackend ) -> None: httpx_mock.add_callback(lambda request: return_value, url="https://test_url") - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url") assert ( str(exception_info.value) @@ -1206,15 +1317,18 @@ async def test_callback_not_returning_a_response( @pytest.mark.parametrize("return_value", [None, "not a response"]) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_async_callback_not_returning_a_response( - httpx_mock: HTTPXMock, return_value: str | None + httpx_mock: HTTPXMock, return_value: str | None, backend: HTTPXBackend ) -> None: - async def invalid_response(request: httpx.Request) -> httpx.Response: + + async def invalid_response( + request: "httpx.Request", + ) -> "httpx.Response": return return_value httpx_mock.add_callback(invalid_response, url="https://test_url") - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url") assert ( str(exception_info.value) @@ -1223,26 +1337,32 @@ async def invalid_response(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_async_callback_returning_response(httpx_mock: HTTPXMock) -> None: - async def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json={"url": str(request.url)}) +async def test_async_callback_returning_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + async def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json={"url": str(request.url)}) httpx_mock.add_callback(custom_response, url="https://test_url") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == {"url": "https://test_url"} assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio -async def test_callback_executed_twice(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content"]) +async def test_callback_executed_twice( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" @@ -1253,13 +1373,16 @@ def custom_response(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_async_callback_executed_twice(httpx_mock: HTTPXMock) -> None: - async def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content"]) +async def test_async_callback_executed_twice( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + async def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" @@ -1270,14 +1393,17 @@ async def custom_response(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_callback_registered_after_response(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content2"]) +async def test_callback_registered_after_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content2"]) httpx_mock.add_response(json=["content1"]) httpx_mock.add_callback(custom_response, is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" @@ -1293,14 +1419,17 @@ def custom_response(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_async_callback_registered_after_response(httpx_mock: HTTPXMock) -> None: - async def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content2"]) +async def test_async_callback_registered_after_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + async def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content2"]) httpx_mock.add_response(json=["content1"]) httpx_mock.add_callback(custom_response, is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" @@ -1316,14 +1445,17 @@ async def custom_response(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_response_registered_after_callback(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content1"]) +async def test_response_registered_after_callback( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content1"]) httpx_mock.add_callback(custom_response) httpx_mock.add_response(json=["content2"], is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" @@ -1339,14 +1471,17 @@ def custom_response(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_response_registered_after_async_callback(httpx_mock: HTTPXMock) -> None: - async def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content1"]) +async def test_response_registered_after_async_callback( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + async def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content1"]) httpx_mock.add_callback(custom_response) httpx_mock.add_response(json=["content2"], is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" @@ -1362,13 +1497,16 @@ async def custom_response(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_callback_matching_method(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content"]) +async def test_callback_matching_method( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, method="GET", is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" @@ -1379,13 +1517,16 @@ def custom_response(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_async_callback_matching_method(httpx_mock: HTTPXMock) -> None: - async def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content"]) +async def test_async_callback_matching_method( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + + async def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, method="GET", is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" @@ -1399,20 +1540,26 @@ def test_request_retrieval_with_more_than_one(testdir: Testdir) -> None: """ Single request cannot be returned if there is more than one matching. """ - testdir.makepyfile(""" - import httpx + testdir.makeconftest(""" import pytest + from pytest_httpx import HTTPXBackend, get_backends + @pytest.fixture(params=get_backends(), ids=lambda b: b.name) + def backend(request): + return request.param + """) + testdir.makepyfile(""" + import pytest @pytest.mark.asyncio - async def test_request_retrieval_with_more_than_one(httpx_mock): + async def test_request_retrieval_with_more_than_one(httpx_mock, backend): httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url", headers={"X-TEST": "test header 2"}) - httpx_mock.get_request(url=httpx.URL("https://test_url")) + httpx_mock.get_request(url=backend.httpx.URL("https://test_url")) """) result = testdir.runpytest() result.assert_outcomes(failed=1) @@ -1424,21 +1571,26 @@ async def test_request_retrieval_with_more_than_one(httpx_mock): @pytest.mark.asyncio -async def test_headers_matching(httpx_mock: HTTPXMock) -> None: +async def test_headers_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: + httpx_mock.add_response( - match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"} + match_headers={ + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + } ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" @pytest.mark.asyncio -async def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: +async def test_multi_value_headers_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"}) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get( "https://test_url", headers=[("my-custom-header", "value1"), ("my-custom-header", "value2")], @@ -1450,13 +1602,14 @@ async def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_multi_value_headers_not_matching_single_value_issued( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( match_headers={"my-custom-header": "value1"}, is_optional=True ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get( "https://test_url", headers=[ @@ -1475,13 +1628,14 @@ async def test_multi_value_headers_not_matching_single_value_issued( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_multi_value_headers_not_matching_multi_value_issued( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( match_headers={"my-custom-header": "value1, value2"}, is_optional=True ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get( "https://test_url", headers=[ @@ -1498,41 +1652,49 @@ async def test_multi_value_headers_not_matching_multi_value_issued( @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: +async def test_headers_matching_respect_case( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + httpx_mock.add_response( - match_headers={"user-agent": f"python-httpx/{httpx.__version__}"}, + match_headers={ + "user-agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + }, is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url") assert ( str(exception_info.value) - == f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst: -- Match any request with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers""" + == f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers amongst: +- Match any request with {{'user-agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_headers_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + httpx_mock.add_response( match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", "Host2": "test_url", }, is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url") assert ( str(exception_info.value) - == f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst: -- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" + == f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers amongst: +- Match any request with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" ) @@ -1540,6 +1702,7 @@ async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_not_matching_upper_case_headers_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( method="GET", @@ -1547,8 +1710,8 @@ async def test_url_not_matching_upper_case_headers_matching( match_headers={"MyHeader": "Something"}, is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url", headers={"MyHeader": "Something"}) assert ( str(exception_info.value) @@ -1558,29 +1721,37 @@ async def test_url_not_matching_upper_case_headers_matching( @pytest.mark.asyncio -async def test_content_matching(httpx_mock: HTTPXMock) -> None: +async def test_content_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_content=b"This is the body") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url", content=b"This is the body") assert response.read() == b"" @pytest.mark.asyncio -async def test_proxy_matching_with_authentication(httpx_mock: HTTPXMock) -> None: +async def test_proxy_matching_with_authentication( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(proxy_url="http://user:pwd@my_other_proxy/") - async with httpx.AsyncClient(proxy="http://user:pwd@my_other_proxy") as client: + async with backend.httpx.AsyncClient( + proxy="http://user:pwd@my_other_proxy" + ) as client: response = await client.get("https://test_url") assert response.read() == b"" @pytest.mark.asyncio -async def test_proxy_matching_with_custom_proxy_headers(httpx_mock: HTTPXMock) -> None: +async def test_proxy_matching_with_custom_proxy_headers( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy/") - async with httpx.AsyncClient( - proxy=httpx.Proxy("http://my_test_proxy", headers={"X-Something": "value"}) + async with backend.httpx.AsyncClient( + proxy=backend.httpx.Proxy( + "http://my_test_proxy", headers={"X-Something": "value"} + ) ) as client: response = await client.get("https://test_url") assert response.read() == b"" @@ -1589,11 +1760,12 @@ async def test_proxy_matching_with_custom_proxy_headers(httpx_mock: HTTPXMock) - @pytest.mark.asyncio async def test_proxy_matching_with_authentication_and_custom_proxy_headers( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(proxy_url="http://user:pwd@my_other_proxy/") - async with httpx.AsyncClient( - proxy=httpx.Proxy( + async with backend.httpx.AsyncClient( + proxy=backend.httpx.Proxy( "http://user:pwd@my_other_proxy", headers={"X-Something": "value"} ) ) as client: @@ -1603,11 +1775,11 @@ async def test_proxy_matching_with_authentication_and_custom_proxy_headers( @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_proxy_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True) - async with httpx.AsyncClient(proxy="http://my_test_proxy") as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient(proxy="http://my_test_proxy") as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("http://test_url") assert ( str(exception_info.value) @@ -1618,11 +1790,11 @@ async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: +async def test_proxy_not_existing(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("http://test_url") assert ( str(exception_info.value) @@ -1632,10 +1804,12 @@ async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None: +async def test_requests_retrieval_content_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.post("https://test_url", content=b"This is the body") await client.post("https://test_url2", content=b"This is the body") await client.post("https://test_url2", content=b"This is the body2") @@ -1644,10 +1818,12 @@ async def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> Non @pytest.mark.asyncio -async def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None: +async def test_requests_retrieval_json_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.post("https://test_url", json=["my_str"]) await client.post("https://test_url2", json=["my_str"]) await client.post("https://test_url2", json=["my_str2"]) @@ -1656,13 +1832,15 @@ async def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: +async def test_requests_retrieval_proxy_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient( + async with backend.httpx.AsyncClient( mounts={ - "http://": httpx.AsyncHTTPTransport(proxy="http://my_test_proxy"), - "https://": httpx.AsyncHTTPTransport( + "http://": backend.httpx.AsyncHTTPTransport(proxy="http://my_test_proxy"), + "https://": backend.httpx.AsyncHTTPTransport( proxy="http://user:pwd@my_other_proxy" ), } @@ -1677,13 +1855,15 @@ async def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: +async def test_request_retrieval_proxy_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient( + async with backend.httpx.AsyncClient( mounts={ - "http://": httpx.AsyncHTTPTransport(proxy="http://my_test_proxy"), - "https://": httpx.AsyncHTTPTransport( + "http://": backend.httpx.AsyncHTTPTransport(proxy="http://my_test_proxy"), + "https://": backend.httpx.AsyncHTTPTransport( proxy="http://user:pwd@my_other_proxy" ), } @@ -1698,10 +1878,11 @@ async def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio async def test_requests_retrieval_files_and_data_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.put( "https://test_url", files={"name": ("file_name", b"File content")}, @@ -1730,10 +1911,12 @@ async def test_requests_retrieval_files_and_data_matching( @pytest.mark.asyncio -async def test_request_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> None: +async def test_request_retrieval_files_and_data_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.put( "https://test_url", files={"name": ("file_name", b"File content")}, @@ -1749,10 +1932,12 @@ async def test_request_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) @pytest.mark.asyncio -async def test_requests_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: +async def test_requests_retrieval_extensions_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://test_url") await client.get("https://test_url2", timeout=10) await client.get("https://test_url2", timeout=10) @@ -1769,11 +1954,13 @@ async def test_requests_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> @pytest.mark.asyncio -async def test_request_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: +async def test_request_retrieval_extensions_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: - await client.get("https://test_url", timeout=httpx.Timeout(5, read=10)) + async with backend.httpx.AsyncClient() as client: + await client.get("https://test_url", timeout=backend.httpx.Timeout(5, read=10)) await client.get("https://test_url2", timeout=10) await client.get("http://test_url2", timeout=10) @@ -1784,11 +1971,13 @@ async def test_request_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> N @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_content_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_content_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(match_content=b"This is the body", is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body2") assert ( str(exception_info.value) @@ -1798,30 +1987,32 @@ async def test_content_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_json_matching(httpx_mock: HTTPXMock) -> None: +async def test_json_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url", json={"b": 2, "a": 1}) assert response.read() == b"" @pytest.mark.asyncio -async def test_json_partial_matching(httpx_mock: HTTPXMock) -> None: +async def test_json_partial_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(match_json={"a": 1, "b": ANY}) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url", json={"b": 2, "a": 1}) assert response.read() == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_json_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_json_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1}) assert ( str(exception_info.value) @@ -1832,15 +2023,17 @@ async def test_json_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_headers_and_json_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_json={"a": 1, "b": 2}, match_headers={"foo": "bar"}, is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1}) assert ( str(exception_info.value) @@ -1851,11 +2044,13 @@ async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: +async def test_match_json_invalid_json( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"foobar") assert ( str(exception_info.value) @@ -1867,11 +2062,13 @@ async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio @pytest.mark.parametrize("content", [b'{"a":"\xff"}', b'{"a":"\xc3"}']) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_match_json_invalid_utf8(httpx_mock: HTTPXMock, content: bytes) -> None: +async def test_match_json_invalid_utf8( + httpx_mock: HTTPXMock, backend: HTTPXBackend, content: bytes +) -> None: httpx_mock.add_response(match_json={"a": "value"}) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=content) assert exception_info.value.request.content == content assert "No response can be found" in str(exception_info.value) @@ -1881,11 +2078,13 @@ async def test_match_json_invalid_utf8(httpx_mock: HTTPXMock, content: bytes) -> @pytest.mark.asyncio -async def test_match_json_binary_content_fallback(httpx_mock: HTTPXMock) -> None: +async def test_match_json_binary_content_fallback( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(match_json={"a": "value"}, status_code=201) httpx_mock.add_response(match_content=b"\xff", status_code=202) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url", content=b"\xff") assert response.status_code == 202 response = await client.post("https://test_url", json={"a": "value"}) @@ -1894,11 +2093,11 @@ async def test_match_json_binary_content_fallback(httpx_mock: HTTPXMock) -> None @pytest.mark.asyncio async def test_requests_retrieval_json_matching_with_binary_content( - httpx_mock: HTTPXMock, + httpx_mock: HTTPXMock, backend: HTTPXBackend ) -> None: httpx_mock.add_response(is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: binary_request = ( await client.post("https://test_url", content=b"\xff") ).request @@ -1912,92 +2111,106 @@ async def test_requests_retrieval_json_matching_with_binary_content( @pytest.mark.asyncio -async def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: +async def test_headers_and_content_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( - match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, + match_headers={ + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + }, match_content=b"This is the body", ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url", content=b"This is the body") assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None: +async def test_headers_not_matching_and_content_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_headers_matching_and_content_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_headers_and_content_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio -async def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: +async def test_url_and_headers_and_content_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", - match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, + match_headers={ + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + }, match_content=b"This is the body", ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url", content=b"This is the body") assert response.content == b"" @@ -2006,24 +2219,25 @@ async def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> No @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_not_matching_and_url_and_content_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @@ -2031,24 +2245,25 @@ async def test_headers_not_matching_and_url_and_content_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_and_headers_not_matching_and_content_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @@ -2056,24 +2271,25 @@ async def test_url_and_headers_not_matching_and_content_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @@ -2081,24 +2297,25 @@ async def test_url_and_headers_matching_and_content_not_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @@ -2106,62 +2323,68 @@ async def test_headers_matching_and_url_and_content_not_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_url_and_headers_and_content_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio async def test_method_and_url_and_headers_and_content_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", - match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, + match_headers={ + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + }, match_content=b"This is the body", ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.post("https://test_url", content=b"This is the body") assert response.content == b"" @@ -2170,25 +2393,26 @@ async def test_method_and_url_and_headers_and_content_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_not_matching_and_method_and_url_and_content_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @@ -2196,25 +2420,26 @@ async def test_headers_not_matching_and_method_and_url_and_content_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_and_headers_not_matching_and_method_and_content_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @@ -2222,25 +2447,26 @@ async def test_url_and_headers_not_matching_and_method_and_content_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_and_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @@ -2248,25 +2474,26 @@ async def test_method_and_url_and_headers_matching_and_content_not_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_and_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @@ -2274,25 +2501,26 @@ async def test_method_and_headers_matching_and_url_and_content_not_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_and_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @@ -2300,25 +2528,26 @@ async def test_method_and_url_matching_and_headers_and_content_not_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_matching_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @@ -2326,126 +2555,149 @@ async def test_method_matching_and_url_and_headers_and_content_not_matching( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", method="PUT", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match PUT request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match PUT request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio -async def test_header_as_str_tuple_list(httpx_mock: HTTPXMock) -> None: +async def test_header_as_str_tuple_list( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")] ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert dict(response.cookies) == {"key": "value", "key2": "value2"} @pytest.mark.asyncio -async def test_header_as_bytes_tuple_list(httpx_mock: HTTPXMock) -> None: +async def test_header_as_bytes_tuple_list( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( headers=[(b"set-cookie", b"key=value"), (b"set-cookie", b"key2=value2")] ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert dict(response.cookies) == {"key": "value", "key2": "value2"} @pytest.mark.asyncio -async def test_header_as_bytes_dict(httpx_mock: HTTPXMock) -> None: +async def test_header_as_bytes_dict( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(headers={b"set-cookie": b"key=value"}) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert dict(response.cookies) == {"key": "value"} @pytest.mark.asyncio -async def test_header_as_httpx_headers(httpx_mock: HTTPXMock) -> None: - httpx_mock.add_response(headers=httpx.Headers({"set-cookie": "key=value"})) +async def test_header_as_httpx_headers( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + httpx_mock.add_response(headers=backend.httpx.Headers({"set-cookie": "key=value"})) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert dict(response.cookies) == {"key": "value"} @pytest.mark.asyncio -async def test_elapsed_when_add_response(httpx_mock: HTTPXMock) -> None: +async def test_elapsed_when_add_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response() - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.elapsed is not None @pytest.mark.asyncio -async def test_elapsed_when_add_callback(httpx_mock: HTTPXMock) -> None: +async def test_elapsed_when_add_callback( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_callback( - callback=lambda req: httpx.Response(status_code=200, json={"foo": "bar"}) + callback=lambda req: backend.httpx.Response( + status_code=200, json={"foo": "bar"} + ) ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.elapsed is not None @pytest.mark.asyncio -async def test_elapsed_when_add_async_callback(httpx_mock: HTTPXMock) -> None: - async def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json={"foo": "bar"}) +async def test_elapsed_when_add_async_callback( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + async def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json={"foo": "bar"}) httpx_mock.add_callback(custom_response) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.elapsed is not None @pytest.mark.asyncio -async def test_non_ascii_url_response(httpx_mock: HTTPXMock) -> None: +async def test_non_ascii_url_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url?query_type=数据") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url?query_type=数据") assert response.content == b"" @pytest.mark.asyncio -async def test_url_encoded_matching_response(httpx_mock: HTTPXMock) -> None: +async def test_url_encoded_matching_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url?a=%E6%95%B0%E6%8D%AE") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url?a=数据") assert response.content == b"" @pytest.mark.asyncio -async def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None: +async def test_reset_is_removing_requests( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response() - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: await client.get("https://test_url") assert len(httpx_mock.get_requests()) == 1 @@ -2455,14 +2707,14 @@ async def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_mutating_json(httpx_mock: HTTPXMock) -> None: +async def test_mutating_json(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: mutating_json = {"content": "request 1"} httpx_mock.add_response(json=mutating_json) mutating_json["content"] = "request 2" httpx_mock.add_response(json=mutating_json) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == {"content": "request 1"} @@ -2473,32 +2725,35 @@ async def test_mutating_json(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio async def test_streams_are_not_cascading_resulting_in_maximum_recursion( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(json={"abc": "def"}, is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: 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 @pytest.mark.asyncio -async def test_custom_transport(httpx_mock: HTTPXMock) -> None: - class CustomTransport(httpx.AsyncHTTPTransport): +async def test_custom_transport(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: + class CustomTransport(backend.httpx.AsyncHTTPTransport): # type: ignore[name-defined] def __init__(self, prefix: str, *args, **kwargs): super().__init__(*args, **kwargs) self.prefix = prefix async def handle_async_request( self, - request: httpx.Request, - ) -> httpx.Response: + request: "httpx.Request", + ) -> "httpx.Response": httpx_response = await super().handle_async_request(request) httpx_response.headers["x-prefix"] = self.prefix return httpx_response httpx_mock.add_response() - async with httpx.AsyncClient(transport=CustomTransport(prefix="test")) as client: + async with backend.httpx.AsyncClient( + transport=CustomTransport(prefix="test") + ) as client: response = await client.post("https://test_url", content=b"This is the body") assert response.read() == b"" assert response.headers["x-prefix"] == "test" @@ -2507,6 +2762,7 @@ async def handle_async_request( @pytest.mark.asyncio async def test_response_selection_content_matching_with_async_iterable( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> 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") @@ -2523,7 +2779,7 @@ async def stream_content_2() -> AsyncIterable[bytes]: yield b"content" yield b" 2" - async with httpx.AsyncClient() as client: + async with backend.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" @@ -2533,6 +2789,7 @@ async def stream_content_2() -> AsyncIterable[bytes]: @pytest.mark.asyncio async def test_request_selection_content_matching_with_async_iterable( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(match_content=b"full content 1") httpx_mock.add_response(match_content=b"full content 2") @@ -2549,7 +2806,7 @@ async def stream_content_2() -> AsyncIterable[bytes]: yield b"content" yield b" 2" - async with httpx.AsyncClient() as client: + async with backend.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()) @@ -2563,10 +2820,10 @@ async def stream_content_2() -> AsyncIterable[bytes]: @pytest.mark.asyncio -async def test_files_matching(httpx_mock: HTTPXMock) -> None: +async def test_files_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_files={"name": ("file_name", b"File content")}) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.put( "https://test_url", files={"name": ("file_name", b"File content")} ) @@ -2574,13 +2831,15 @@ async def test_files_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_files_and_data_matching(httpx_mock: HTTPXMock) -> None: +async def test_files_and_data_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.put( "https://test_url", files={"name": ("file_name", b"File content")}, @@ -2591,7 +2850,9 @@ async def test_files_and_data_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> None: +async def test_files_not_matching_name( + httpx_mock: HTTPXMock, monkeypatch, backend: HTTPXBackend +) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, @@ -2603,8 +2864,8 @@ async def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> No match_files={"name2": ("file_name", b"File content")}, is_optional=True ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.put( "https://test_url", files={"name1": ("file_name", b"File content")} ) @@ -2617,7 +2878,9 @@ async def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> No @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) -> None: +async def test_files_not_matching_file_name( + httpx_mock: HTTPXMock, monkeypatch, backend: HTTPXBackend +) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, @@ -2629,8 +2892,8 @@ async def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) match_files={"name": ("file_name2", b"File content")}, is_optional=True ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.put( "https://test_url", files={"name": ("file_name1", b"File content")} ) @@ -2643,7 +2906,9 @@ async def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> None: +async def test_files_not_matching_content( + httpx_mock: HTTPXMock, monkeypatch, backend: HTTPXBackend +) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, @@ -2655,8 +2920,8 @@ async def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> match_files={"name": ("file_name", b"File content2")}, is_optional=True ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.put( "https://test_url", files={"name": ("file_name", b"File content1")} ) @@ -2670,7 +2935,7 @@ async def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_files_matching_but_data_not_matching( - httpx_mock: HTTPXMock, monkeypatch + httpx_mock: HTTPXMock, monkeypatch, backend: HTTPXBackend ) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( @@ -2685,8 +2950,8 @@ async def test_files_matching_but_data_not_matching( is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.put( "https://test_url", files={"name": ("file_name", b"File content")} ) @@ -2698,28 +2963,30 @@ async def test_files_matching_but_data_not_matching( @pytest.mark.asyncio -async def test_timeout_matching(httpx_mock: HTTPXMock) -> None: +async def test_timeout_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}} ) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.put( - "https://test_url", timeout=httpx.Timeout(5, write=10) + "https://test_url", timeout=backend.httpx.Timeout(5, write=10) ) assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_timeout_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_timeout_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}}, is_optional=True, ) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url", extensions={"test": "value"}) assert ( str(exception_info.value) @@ -2729,10 +2996,12 @@ async def test_timeout_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_extensions_matching(httpx_mock: HTTPXMock) -> None: +async def test_extensions_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(match_extensions={"test": "value"}) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.put( "https://test_url", extensions={"test": "value", "test2": "value2"} ) @@ -2741,11 +3010,13 @@ async def test_extensions_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -async def test_extensions_not_matching(httpx_mock: HTTPXMock) -> None: +async def test_extensions_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(match_extensions={"test": "value"}, is_optional=True) - async with httpx.AsyncClient() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + async with backend.httpx.AsyncClient() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: await client.get("https://test_url", extensions={"test": "value2"}) assert ( str(exception_info.value) @@ -2755,23 +3026,27 @@ async def test_extensions_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio -async def test_optional_response_not_matched(httpx_mock: HTTPXMock) -> None: +async def test_optional_response_not_matched( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url", is_optional=True) httpx_mock.add_response(url="https://test_url2") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url2") assert response.content == b"" @pytest.mark.asyncio -async def test_optional_response_matched(httpx_mock: HTTPXMock) -> None: +async def test_optional_response_matched( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url", is_optional=True) httpx_mock.add_response(url="https://test_url2") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response1 = await client.get("https://test_url") response2 = await client.get("https://test_url2") assert response1.content == b"" @@ -2780,31 +3055,37 @@ async def test_optional_response_matched(httpx_mock: HTTPXMock) -> None: @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) -async def test_mandatory_response_matched(httpx_mock: HTTPXMock) -> None: +async def test_mandatory_response_matched( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url") # This response MUST be requested (overrides global settings via marker) httpx_mock.add_response(url="https://test_url2", is_optional=False) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url2") assert response.content == b"" @pytest.mark.asyncio -async def test_multi_response_matched_once(httpx_mock: HTTPXMock) -> None: +async def test_multi_response_matched_once( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" @pytest.mark.asyncio -async def test_multi_response_matched_twice(httpx_mock: HTTPXMock) -> None: +async def test_multi_response_matched_twice( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: response1 = await client.get("https://test_url") response2 = await client.get("https://test_url") assert response1.content == b"" diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py index 4b4bdfe..130068a 100644 --- a/tests/test_httpx_sync.py +++ b/tests/test_httpx_sync.py @@ -1,20 +1,25 @@ import os import re from collections.abc import Iterable -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import ANY -import httpx import pytest from pytest import Testdir import pytest_httpx -from pytest_httpx import HTTPXMock +from pytest_httpx import HTTPXBackend, HTTPXMock + +if TYPE_CHECKING: + try: + import httpx + except ModuleNotFoundError: + import httpx2 as httpx @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_without_response(httpx_mock: HTTPXMock) -> None: - with pytest.raises(Exception) as exception_info, httpx.Client() as client: +def test_without_response(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: + with pytest.raises(Exception) as exception_info, backend.httpx.Client() as client: client.get("https://test_url") assert ( str(exception_info.value) @@ -22,29 +27,31 @@ def test_without_response(httpx_mock: HTTPXMock) -> None: ) -def test_default_response(httpx_mock: HTTPXMock) -> None: +def test_default_response(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response() - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" assert response.status_code == 200 - assert response.headers == httpx.Headers({}) + assert response.headers == backend.httpx.Headers({}) assert response.http_version == "HTTP/1.1" -def test_url_matching(httpx_mock: HTTPXMock) -> None: +def test_url_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(url="https://test_url") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" -def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None: +def test_url_matching_reusing_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" @@ -52,10 +59,12 @@ def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None: assert response.content == b"" -def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: +def test_url_query_string_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url?a=1&b=2", is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url?a=1&b=2") assert response.content == b"" @@ -64,14 +73,16 @@ def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" -def test_url_query_params_partial_matching(httpx_mock: HTTPXMock) -> None: +def test_url_query_params_partial_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", match_params={"a": ["1", "3"], "b": ANY, "c": "4", "d": ["5", ANY], "e": [ANY]}, is_reusable=True, ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url?a=1&b=2&a=3&c=4&d=5&d=6&e=7") assert response.content == b"" @@ -80,29 +91,35 @@ def test_url_query_params_partial_matching(httpx_mock: HTTPXMock) -> None: assert response.content == b"" -def test_url_as_pattern_ignoring_query_parameters(httpx_mock: HTTPXMock): +def test_url_as_pattern_ignoring_query_parameters( + httpx_mock: HTTPXMock, backend: HTTPXBackend +): httpx_mock.add_response(url=re.compile("https://test_url/something.*")) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url/something?a=1&b=2") assert response.content == b"" -def test_url_query_params_with_single_value_list(httpx_mock: HTTPXMock) -> None: +def test_url_query_params_with_single_value_list( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", match_params={"a": ["1"]}) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url?a=1") assert response.content == b"" -def test_url_query_params_with_non_str_name(httpx_mock: HTTPXMock) -> None: +def test_url_query_params_with_non_str_name( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", match_params={1: "1"}, ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url?1=1") assert response.content == b"" @@ -137,15 +154,17 @@ def test_regex_url_and_match_params(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_url_query_params_not_matching(httpx_mock: HTTPXMock) -> None: +def test_url_query_params_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", match_params={"a": "1"}, is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url?a=2") assert ( str(exception_info.value) @@ -210,10 +229,11 @@ def test_match_params_with_non_str_values_and_params_provided_as_dict( httpx_mock: HTTPXMock, match_params: dict[str, Any], request_params: dict[str, Any], + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url", match_params=match_params) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url", params=request_params) assert response.content == b"" @@ -258,20 +278,22 @@ def test_match_params_with_non_str_values_and_params_in_requested_url( httpx_mock: HTTPXMock, match_params: dict[str, Any], url: str, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url", match_params=match_params) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get(url) assert response.content == b"" def test_url_matching_with_more_than_one_value_on_same_param( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=3", is_optional=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url", params={"a": [1, 3]}) assert response.content == b"" @@ -279,11 +301,12 @@ def test_url_matching_with_more_than_one_value_on_same_param( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_not_matching_with_more_than_one_value_on_same_param_and_diff_value( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url?a=2&a=3", is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url", params={"a": [1, 3]}) assert ( str(exception_info.value) @@ -295,11 +318,12 @@ def test_url_not_matching_with_more_than_one_value_on_same_param_and_diff_value( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_not_matching_with_more_than_one_value_on_same_param_and_more_values( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=3", is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url", params={"a": [1, 3, 4]}) assert ( str(exception_info.value) @@ -311,11 +335,12 @@ def test_url_not_matching_with_more_than_one_value_on_same_param_and_more_values @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_not_matching_with_more_than_one_value_on_same_param_and_less_values( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=3&a=4", is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url", params={"a": [1, 3]}) assert ( str(exception_info.value) @@ -325,11 +350,11 @@ def test_url_not_matching_with_more_than_one_value_on_same_param_and_less_values @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_url_not_matching(httpx_mock: HTTPXMock) -> None: +def test_url_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(url="https://test_url", is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url2") assert ( str(exception_info.value) @@ -339,11 +364,13 @@ def test_url_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: +def test_url_query_string_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=2", is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: # Same parameter order matters as it corresponds to a list on server side client.get("https://test_url?a=2&a=1") assert ( @@ -353,10 +380,10 @@ def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: ) -def test_method_matching(httpx_mock: HTTPXMock) -> None: +def test_method_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(method="get", is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" @@ -365,11 +392,11 @@ def test_method_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_method_not_matching(httpx_mock: HTTPXMock) -> None: +def test_method_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(method="get", is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url") assert ( str(exception_info.value) @@ -378,12 +405,12 @@ def test_method_not_matching(httpx_mock: HTTPXMock) -> None: ) -def test_reusing_one_response(httpx_mock: HTTPXMock) -> None: +def test_reusing_one_response(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( url="https://test_url", content=b"test content", is_reusable=True ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content" @@ -391,32 +418,38 @@ def test_reusing_one_response(httpx_mock: HTTPXMock) -> None: assert response.content == b"test content" -def test_response_with_string_body(httpx_mock: HTTPXMock) -> None: +def test_response_with_string_body( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", text="test content") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content" -def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None: +def test_response_with_html_string_body( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", html="test content") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.text == "test content" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> None: +def test_url_not_matching_upper_case_headers_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( method="GET", url="https://test_url?q=b", match_headers={"MyHeader": "Something"}, is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url", headers={"MyHeader": "Something"}) assert ( str(exception_info.value) @@ -425,96 +458,102 @@ def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> ) -def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None: +def test_stream_response_streaming( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", stream=pytest_httpx.IteratorStream([b"part 1", b"part 2"]), is_reusable=True, ) - with httpx.Client() as client: + with backend.httpx.Client() as client: with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1", b"part 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): list(response.iter_raw()) # Assert a response can be streamed more than once with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1", b"part 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): list(response.iter_raw()) -def test_content_response_streaming(httpx_mock: HTTPXMock) -> None: +def test_content_response_streaming( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", content=b"part 1 and 2", is_reusable=True, ) - with httpx.Client() as client: + with backend.httpx.Client() as client: with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1 and 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): list(response.iter_raw()) # Assert a response can be streamed more than once with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1 and 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): list(response.iter_raw()) -def test_text_response_streaming(httpx_mock: HTTPXMock) -> None: +def test_text_response_streaming(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( url="https://test_url", text="part 1 and 2", is_reusable=True, ) - with httpx.Client() as client: + with backend.httpx.Client() as client: with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1 and 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): list(response.iter_raw()) # Assert a response can be streamed more than once with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1 and 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): list(response.iter_raw()) -def test_default_response_streaming(httpx_mock: HTTPXMock) -> None: +def test_default_response_streaming( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): list(response.iter_raw()) # Assert a response can be streamed more than once with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [] # Assert that stream still behaves the proper way (can only be consumed once per request) - with pytest.raises(httpx.StreamConsumed): + with pytest.raises(backend.httpx.StreamConsumed): list(response.iter_raw()) -def test_with_many_responses(httpx_mock: HTTPXMock) -> None: +def test_with_many_responses(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content 1") httpx_mock.add_response(url="https://test_url", content=b"test content 2") httpx_mock.add_response(url="https://test_url", content=b"test content 2") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content 1" @@ -525,13 +564,15 @@ def test_with_many_responses(httpx_mock: HTTPXMock) -> None: assert response.content == b"test content 2" -def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None: +def test_with_many_reused_responses( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content 1") httpx_mock.add_response( url="https://test_url", content=b"test content 2", is_reusable=True ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content 1" @@ -542,7 +583,9 @@ def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None: assert response.content == b"test content 2" -def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None: +def test_with_many_responses_methods( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1" ) @@ -562,7 +605,7 @@ def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None: url="https://test_url", method="HEAD", content=b"test content 6" ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url") assert response.content == b"test content 2" @@ -582,7 +625,9 @@ def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None: assert response.content == b"test content 4" -def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None: +def test_with_many_responses_status_codes( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1", status_code=200 ) @@ -614,7 +659,7 @@ def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None: status_code=500, ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url") assert response.content == b"test content 2" assert response.status_code == 201 @@ -640,7 +685,9 @@ def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None: assert response.status_code == 303 -def test_with_many_responses_urls_str(httpx_mock: HTTPXMock) -> None: +def test_with_many_responses_urls_str( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url?param1=test", method="GET", content=b"test content 1" ) @@ -660,35 +707,45 @@ def test_with_many_responses_urls_str(httpx_mock: HTTPXMock) -> None: url="https://test_url?param6=test", method="HEAD", content=b"test content 6" ) - with httpx.Client() as client: - response = client.post(httpx.URL("https://test_url", params={"param2": "test"})) + with backend.httpx.Client() as client: + response = client.post( + backend.httpx.URL("https://test_url", params={"param2": "test"}) + ) assert response.content == b"test content 2" - response = client.get(httpx.URL("https://test_url", params={"param1": "test"})) + response = client.get( + backend.httpx.URL("https://test_url", params={"param1": "test"}) + ) assert response.content == b"test content 1" - response = client.put(httpx.URL("https://test_url", params={"param3": "test"})) + response = client.put( + backend.httpx.URL("https://test_url", params={"param3": "test"}) + ) assert response.content == b"test content 3" - response = client.head(httpx.URL("https://test_url", params={"param6": "test"})) + response = client.head( + backend.httpx.URL("https://test_url", params={"param6": "test"}) + ) assert response.content == b"test content 6" response = client.patch( - httpx.URL("https://test_url", params={"param5": "test"}) + backend.httpx.URL("https://test_url", params={"param5": "test"}) ) assert response.content == b"test content 5" response = client.delete( - httpx.URL("https://test_url", params={"param4": "test"}) + backend.httpx.URL("https://test_url", params={"param4": "test"}) ) assert response.content == b"test content 4" -def test_response_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: +def test_response_with_pattern_in_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url=re.compile(".*test.*")) httpx_mock.add_response(url="https://unmatched", content=b"test content") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://unmatched") assert response.content == b"test content" @@ -696,11 +753,13 @@ def test_response_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: assert response.content == b"" -def test_request_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: +def test_request_with_pattern_in_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url") httpx_mock.add_response(url="https://unmatched") - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://unmatched") client.get("https://test_url", headers={"X-Test": "1"}) @@ -709,12 +768,14 @@ def test_request_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: assert request.headers["x-test"] == "1" -def test_requests_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: +def test_requests_with_pattern_in_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url") httpx_mock.add_response(url="https://tests_url") httpx_mock.add_response(url="https://unmatched") - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://tests_url", headers={"X-Test": "1"}) client.get("https://unmatched", headers={"X-Test": "2"}) client.get("https://test_url") @@ -725,12 +786,14 @@ def test_requests_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: assert "x-test" not in requests[1].headers -def test_callback_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json={"url": str(request.url)}) +def test_callback_with_pattern_in_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json={"url": str(request.url)}) - def custom_response2(request: httpx.Request) -> httpx.Response: - return httpx.Response( + def custom_response2(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response( status_code=200, extensions={"http_version": b"HTTP/2.0"}, json={"url": str(request.url)}, @@ -739,7 +802,7 @@ def custom_response2(request: httpx.Request) -> httpx.Response: httpx_mock.add_callback(custom_response, url=re.compile(".*test.*")) httpx_mock.add_callback(custom_response2, url="https://unmatched") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://unmatched") assert response.http_version == "HTTP/2.0" @@ -747,39 +810,41 @@ def custom_response2(request: httpx.Request) -> httpx.Response: assert response.http_version == "HTTP/1.1" -def test_with_many_responses_urls_instances(httpx_mock: HTTPXMock) -> None: +def test_with_many_responses_urls_instances( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param1": "test"}), + url=backend.httpx.URL("https://test_url", params={"param1": "test"}), method="GET", content=b"test content 1", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param2": "test"}), + url=backend.httpx.URL("https://test_url", params={"param2": "test"}), method="POST", content=b"test content 2", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param3": "test"}), + url=backend.httpx.URL("https://test_url", params={"param3": "test"}), method="PUT", content=b"test content 3", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param4": "test"}), + url=backend.httpx.URL("https://test_url", params={"param4": "test"}), method="DELETE", content=b"test content 4", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param5": "test"}), + url=backend.httpx.URL("https://test_url", params={"param5": "test"}), method="PATCH", content=b"test content 5", ) httpx_mock.add_response( - url=httpx.URL("https://test_url", params={"param6": "test"}), + url=backend.httpx.URL("https://test_url", params={"param6": "test"}), method="HEAD", content=b"test content 6", ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url?param2=test") assert response.content == b"test content 2" @@ -799,33 +864,33 @@ def test_with_many_responses_urls_instances(httpx_mock: HTTPXMock) -> None: assert response.content == b"test content 4" -def test_with_http_version_2(httpx_mock: HTTPXMock) -> None: +def test_with_http_version_2(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( url="https://test_url", http_version="HTTP/2", content=b"test content 1" ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content 1" assert response.http_version == "HTTP/2" -def test_with_headers(httpx_mock: HTTPXMock) -> None: +def test_with_headers(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( url="https://test_url", content=b"test content 1", headers={"X-Test": "Test value"}, ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content 1" - assert response.headers == httpx.Headers( + assert response.headers == backend.httpx.Headers( {"x-test": "Test value", "content-length": "14"} ) -def test_requests_retrieval(httpx_mock: HTTPXMock) -> None: +def test_requests_retrieval(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1" ) @@ -845,7 +910,7 @@ def test_requests_retrieval(httpx_mock: HTTPXMock) -> None: url="https://test_url", method="HEAD", content=b"test content 6" ) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.post("https://test_url", content=b"sent content 2") client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.put("https://test_url", content=b"sent content 3") @@ -854,71 +919,77 @@ def test_requests_retrieval(httpx_mock: HTTPXMock) -> None: client.delete("https://test_url", headers={"X-Test": "test header 4"}) patch_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="PATCH" + url=backend.httpx.URL("https://test_url"), method="PATCH" ) assert patch_request is not None assert patch_request.read() == b"sent content 5" head_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="HEAD" + url=backend.httpx.URL("https://test_url"), method="HEAD" ) assert head_request is not None assert head_request.read() == b"" put_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="PUT" + url=backend.httpx.URL("https://test_url"), method="PUT" ) assert put_request is not None assert put_request.read() == b"sent content 3" get_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="GET" + url=backend.httpx.URL("https://test_url"), method="GET" ) assert get_request is not None assert get_request.headers["x-test"] == "test header 1" post_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="POST" + url=backend.httpx.URL("https://test_url"), method="POST" ) assert post_request is not None assert post_request.read() == b"sent content 2" delete_request = httpx_mock.get_request( - url=httpx.URL("https://test_url"), method="DELETE" + url=backend.httpx.URL("https://test_url"), method="DELETE" ) assert delete_request is not None assert delete_request.headers["x-test"] == "test header 4" -def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None: +def test_requests_retrieval_on_same_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url", headers={"X-TEST": "test header 2"}) - requests = httpx_mock.get_requests(url=httpx.URL("https://test_url")) + requests = httpx_mock.get_requests(url=backend.httpx.URL("https://test_url")) assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" -def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None: +def test_request_retrieval_on_same_url( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url2", headers={"X-TEST": "test header 2"}) - request = httpx_mock.get_request(url=httpx.URL("https://test_url")) + request = httpx_mock.get_request(url=backend.httpx.URL("https://test_url")) assert request is not None assert request.headers["x-test"] == "test header 1" -def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: +def test_requests_retrieval_on_same_method( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url2", headers={"X-TEST": "test header 2"}) @@ -928,10 +999,12 @@ def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: assert requests[1].headers["x-test"] == "test header 2" -def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: +def test_request_retrieval_on_same_method( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.post("https://test_url", headers={"X-TEST": "test header 2"}) @@ -940,25 +1013,31 @@ def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: assert request.headers["x-test"] == "test header 1" -def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock) -> None: +def test_requests_retrieval_on_same_url_and_method( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url", headers={"X-TEST": "test header 2"}) client.post("https://test_url", headers={"X-TEST": "test header 3"}) client.get("https://test_url2", headers={"X-TEST": "test header 4"}) - requests = httpx_mock.get_requests(url=httpx.URL("https://test_url"), method="GET") + requests = httpx_mock.get_requests( + url=backend.httpx.URL("https://test_url"), method="GET" + ) assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" -def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None: +def test_default_requests_retrieval( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.post("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url2", headers={"X-TEST": "test header 2"}) @@ -968,10 +1047,12 @@ def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None: assert requests[1].headers["x-test"] == "test header 2" -def test_default_request_retrieval(httpx_mock: HTTPXMock) -> None: +def test_default_request_retrieval( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response() - with httpx.Client() as client: + with backend.httpx.Client() as client: client.post("https://test_url", headers={"X-TEST": "test header 1"}) request = httpx_mock.get_request() @@ -979,7 +1060,7 @@ def test_default_request_retrieval(httpx_mock: HTTPXMock) -> None: assert request.headers["x-test"] == "test header 1" -def test_requests_json_body(httpx_mock: HTTPXMock) -> None: +def test_requests_json_body(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( url="https://test_url", method="GET", json=["list content 1", "list content 2"] ) @@ -990,7 +1071,7 @@ def test_requests_json_body(httpx_mock: HTTPXMock) -> None: ) httpx_mock.add_response(url="https://test_url", method="PUT", json="string value") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url") assert response.json() == {"key 1": "value 1", "key 2": "value 2"} assert response.headers["content-type"] == "application/json" @@ -1004,51 +1085,59 @@ def test_requests_json_body(httpx_mock: HTTPXMock) -> None: assert response.headers["content-type"] == "application/json" -def test_callback_raising_exception(httpx_mock: HTTPXMock) -> None: - def raise_timeout(request: httpx.Request) -> httpx.Response: - raise httpx.ReadTimeout( +def test_callback_raising_exception( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + def raise_timeout(request: "httpx.Request") -> "httpx.Response": + raise backend.httpx.ReadTimeout( f"Unable to read within {request.extensions['timeout']['read']}", request=request, ) httpx_mock.add_callback(raise_timeout, url="https://test_url") - with httpx.Client() as client: - with pytest.raises(httpx.ReadTimeout) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.ReadTimeout) as exception_info: client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" -def test_request_exception_raising(httpx_mock: HTTPXMock) -> None: +def test_request_exception_raising( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_exception( - httpx.ReadTimeout("Unable to read within 5.0"), url="https://test_url" + backend.httpx.ReadTimeout("Unable to read within 5.0"), url="https://test_url" ) - with httpx.Client() as client: - with pytest.raises(httpx.ReadTimeout) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.ReadTimeout) as exception_info: client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" assert exception_info.value.request is not None -def test_non_request_exception_raising(httpx_mock: HTTPXMock) -> None: +def test_non_request_exception_raising( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_exception( - httpx.HTTPError("Unable to read within 5.0"), url="https://test_url" + backend.httpx.HTTPError("Unable to read within 5.0"), url="https://test_url" ) - with httpx.Client() as client: - with pytest.raises(httpx.HTTPError) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.HTTPError) as exception_info: client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" -def test_callback_returning_response(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json={"url": str(request.url)}) +def test_callback_returning_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json={"url": str(request.url)}) httpx_mock.add_callback(custom_response, url="https://test_url") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.json() == {"url": "https://test_url"} assert response.headers["content-type"] == "application/json" @@ -1057,12 +1146,12 @@ def custom_response(request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("return_value", [None, "not a response"]) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_callback_not_returning_a_response( - httpx_mock: HTTPXMock, return_value: str | None + httpx_mock: HTTPXMock, return_value: str | None, backend: HTTPXBackend ) -> None: httpx_mock.add_callback(lambda request: return_value, url="https://test_url") - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url") assert ( str(exception_info.value) @@ -1070,13 +1159,13 @@ def test_callback_not_returning_a_response( ) -def test_callback_executed_twice(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content"]) +def test_callback_executed_twice(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" @@ -1086,14 +1175,16 @@ def custom_response(request: httpx.Request) -> httpx.Response: assert response.headers["content-type"] == "application/json" -def test_callback_registered_after_response(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content2"]) +def test_callback_registered_after_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content2"]) httpx_mock.add_response(json=["content1"]) httpx_mock.add_callback(custom_response, is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" @@ -1108,14 +1199,16 @@ def custom_response(request: httpx.Request) -> httpx.Response: assert response.headers["content-type"] == "application/json" -def test_response_registered_after_callback(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content1"]) +def test_response_registered_after_callback( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content1"]) httpx_mock.add_callback(custom_response) httpx_mock.add_response(json=["content2"], is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" @@ -1130,13 +1223,13 @@ def custom_response(request: httpx.Request) -> httpx.Response: assert response.headers["content-type"] == "application/json" -def test_callback_matching_method(httpx_mock: HTTPXMock) -> None: - def custom_response(request: httpx.Request) -> httpx.Response: - return httpx.Response(status_code=200, json=["content"]) +def test_callback_matching_method(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: + def custom_response(request: "httpx.Request") -> "httpx.Response": + return backend.httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, method="GET", is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" @@ -1150,18 +1243,23 @@ def test_request_retrieval_with_more_than_one(testdir: Testdir) -> None: """ Single request cannot be returned if there is more than one matching. """ + testdir.makeconftest(""" + import pytest + from pytest_httpx import HTTPXBackend, get_backends + + @pytest.fixture(params=get_backends(), ids=lambda b: b.name) + def backend(request): + return request.param + """) testdir.makepyfile(""" - import httpx - - - def test_request_retrieval_with_more_than_one(httpx_mock): + def test_request_retrieval_with_more_than_one(httpx_mock, backend): httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url", headers={"X-TEST": "test header 2"}) - httpx_mock.get_request(url=httpx.URL("https://test_url")) + httpx_mock.get_request(url=backend.httpx.URL("https://test_url")) """) result = testdir.runpytest() result.assert_outcomes(failed=1) @@ -1172,20 +1270,24 @@ def test_request_retrieval_with_more_than_one(httpx_mock): ) -def test_headers_matching(httpx_mock: HTTPXMock) -> None: +def test_headers_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( - match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"} + match_headers={ + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + } ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" -def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: +def test_multi_value_headers_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"}) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get( "https://test_url", headers=[("my-custom-header", "value1"), ("my-custom-header", "value2")], @@ -1196,13 +1298,14 @@ def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_multi_value_headers_not_matching_single_value_issued( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( match_headers={"my-custom-header": "value1"}, is_optional=True ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get( "https://test_url", headers=[ @@ -1220,13 +1323,14 @@ def test_multi_value_headers_not_matching_single_value_issued( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_multi_value_headers_not_matching_multi_value_issued( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( match_headers={"my-custom-header": "value1, value2"}, is_optional=True ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get( "https://test_url", headers=[ @@ -1242,64 +1346,74 @@ def test_multi_value_headers_not_matching_multi_value_issued( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: +def test_headers_matching_respect_case( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( - match_headers={"user-agent": f"python-httpx/{httpx.__version__}"}, + match_headers={ + "user-agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + }, is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url") assert ( str(exception_info.value) - == f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst: -- Match any request with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers""" + == f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers amongst: +- Match any request with {{'user-agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: +def test_headers_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", "Host2": "test_url", }, is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url") assert ( str(exception_info.value) - == f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst: -- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" + == f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers amongst: +- Match any request with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" ) -def test_content_matching(httpx_mock: HTTPXMock) -> None: +def test_content_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_content=b"This is the body") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url", content=b"This is the body") assert response.read() == b"" -def test_proxy_matching_with_authentication(httpx_mock: HTTPXMock) -> None: +def test_proxy_matching_with_authentication( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(proxy_url="http://user:pwd@my_other_proxy/") - with httpx.Client(proxy="http://user:pwd@my_other_proxy") as client: + with backend.httpx.Client(proxy="http://user:pwd@my_other_proxy") as client: response = client.get("https://test_url") assert response.read() == b"" -def test_proxy_matching_with_custom_proxy_headers(httpx_mock: HTTPXMock) -> None: +def test_proxy_matching_with_custom_proxy_headers( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy/") - with httpx.Client( - proxy=httpx.Proxy("http://my_test_proxy", headers={"X-Something": "value"}) + with backend.httpx.Client( + proxy=backend.httpx.Proxy( + "http://my_test_proxy", headers={"X-Something": "value"} + ) ) as client: response = client.get("https://test_url") assert response.read() == b"" @@ -1307,11 +1421,12 @@ def test_proxy_matching_with_custom_proxy_headers(httpx_mock: HTTPXMock) -> None def test_proxy_matching_with_authentication_and_custom_proxy_headers( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(proxy_url="http://user:pwd@my_other_proxy/") - with httpx.Client( - proxy=httpx.Proxy( + with backend.httpx.Client( + proxy=backend.httpx.Proxy( "http://user:pwd@my_other_proxy", headers={"X-Something": "value"} ) ) as client: @@ -1320,11 +1435,11 @@ def test_proxy_matching_with_authentication_and_custom_proxy_headers( @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: +def test_proxy_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True) - with httpx.Client(proxy="http://my_test_proxy") as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client(proxy="http://my_test_proxy") as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("http://test_url") assert ( str(exception_info.value) @@ -1334,11 +1449,11 @@ def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: +def test_proxy_not_existing(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("http://test_url") assert ( str(exception_info.value) @@ -1347,10 +1462,12 @@ def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: ) -def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None: +def test_requests_retrieval_content_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.post("https://test_url", content=b"This is the body") client.post("https://test_url2", content=b"This is the body") client.post("https://test_url2", content=b"This is the body2") @@ -1358,10 +1475,12 @@ def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None: assert len(httpx_mock.get_requests(match_content=b"This is the body")) == 2 -def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None: +def test_requests_retrieval_json_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.post("https://test_url", json=["my_str"]) client.post("https://test_url2", json=["my_str"]) client.post("https://test_url2", json=["my_str2"]) @@ -1369,13 +1488,17 @@ def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None: assert len(httpx_mock.get_requests(match_json=["my_str"])) == 2 -def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: +def test_requests_retrieval_proxy_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client( + with backend.httpx.Client( mounts={ - "http://": httpx.HTTPTransport(proxy="http://my_test_proxy"), - "https://": httpx.HTTPTransport(proxy="http://user:pwd@my_other_proxy"), + "http://": backend.httpx.HTTPTransport(proxy="http://my_test_proxy"), + "https://": backend.httpx.HTTPTransport( + proxy="http://user:pwd@my_other_proxy" + ), } ) as client: client.get("https://test_url") @@ -1387,13 +1510,17 @@ def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: ) -def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: +def test_request_retrieval_proxy_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client( + with backend.httpx.Client( mounts={ - "http://": httpx.HTTPTransport(proxy="http://my_test_proxy"), - "https://": httpx.HTTPTransport(proxy="http://user:pwd@my_other_proxy"), + "http://": backend.httpx.HTTPTransport(proxy="http://my_test_proxy"), + "https://": backend.httpx.HTTPTransport( + proxy="http://user:pwd@my_other_proxy" + ), } ) as client: client.get("https://test_url") @@ -1403,10 +1530,12 @@ def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: assert httpx_mock.get_request(proxy_url="http://my_test_proxy/") -def test_requests_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> None: +def test_requests_retrieval_files_and_data_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.put( "https://test_url", files={"name": ("file_name", b"File content")}, @@ -1434,10 +1563,12 @@ def test_requests_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> No ) -def test_request_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> None: +def test_request_retrieval_files_and_data_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.put( "https://test_url", files={"name": ("file_name", b"File content")}, @@ -1452,10 +1583,12 @@ def test_request_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> Non ) -def test_requests_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: +def test_requests_retrieval_extensions_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://test_url") client.get("https://test_url2", timeout=10) client.get("https://test_url2", timeout=10) @@ -1471,11 +1604,13 @@ def test_requests_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: ) -def test_request_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: +def test_request_retrieval_extensions_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: - client.get("https://test_url", timeout=httpx.Timeout(5, read=10)) + with backend.httpx.Client() as client: + client.get("https://test_url", timeout=backend.httpx.Timeout(5, read=10)) client.get("https://test_url2", timeout=10) client.get("http://test_url2", timeout=10) @@ -1485,11 +1620,11 @@ def test_request_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_content_not_matching(httpx_mock: HTTPXMock) -> None: +def test_content_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_content=b"This is the body", is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body2") assert ( str(exception_info.value) @@ -1533,28 +1668,28 @@ def test_match_content_and_match_files_error(httpx_mock: HTTPXMock) -> None: ) -def test_json_matching(httpx_mock: HTTPXMock) -> None: +def test_json_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url", json={"b": 2, "a": 1}) assert response.read() == b"" -def test_json_partial_matching(httpx_mock: HTTPXMock) -> None: +def test_json_partial_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_json={"a": 1, "b": ANY}) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url", json={"b": 2, "a": 1}) assert response.read() == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_json_not_matching(httpx_mock: HTTPXMock) -> None: +def test_json_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", json={"c": 3, "b": 2, "a": 1}) assert ( str(exception_info.value) @@ -1564,15 +1699,17 @@ def test_json_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: +def test_headers_and_json_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_json={"a": 1, "b": 2}, match_headers={"foo": "bar"}, is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", json={"c": 3, "b": 2, "a": 1}) assert ( str(exception_info.value) @@ -1582,11 +1719,11 @@ def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: +def test_match_json_invalid_json(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"foobar") assert ( str(exception_info.value) @@ -1597,11 +1734,13 @@ def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: @pytest.mark.parametrize("content", [b'{"a":"\xff"}', b'{"a":"\xc3"}']) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_match_json_invalid_utf8(httpx_mock: HTTPXMock, content: bytes) -> None: +def test_match_json_invalid_utf8( + httpx_mock: HTTPXMock, backend: HTTPXBackend, content: bytes +) -> None: httpx_mock.add_response(match_json={"a": "value"}) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=content) assert exception_info.value.request.content == content assert "No response can be found" in str(exception_info.value) @@ -1610,11 +1749,13 @@ def test_match_json_invalid_utf8(httpx_mock: HTTPXMock, content: bytes) -> None: assert response.status_code == 200 -def test_match_json_binary_content_fallback(httpx_mock: HTTPXMock) -> None: +def test_match_json_binary_content_fallback( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(match_json={"a": "value"}, status_code=201) httpx_mock.add_response(match_content=b"\xff", status_code=202) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url", content=b"\xff") assert response.status_code == 202 response = client.post("https://test_url", json={"a": "value"}) @@ -1622,11 +1763,11 @@ def test_match_json_binary_content_fallback(httpx_mock: HTTPXMock) -> None: def test_requests_retrieval_json_matching_with_binary_content( - httpx_mock: HTTPXMock, + httpx_mock: HTTPXMock, backend: HTTPXBackend ) -> None: httpx_mock.add_response(is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: binary_request = client.post("https://test_url", content=b"\xff").request json_request = client.post("https://test_url", json={"a": "value"}).request @@ -1635,88 +1776,102 @@ def test_requests_retrieval_json_matching_with_binary_content( assert httpx_mock.get_request(match_content=b"\xff") is binary_request -def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: +def test_headers_and_content_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( - match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, + match_headers={ + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + }, match_content=b"This is the body", ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url", content=b"This is the body") assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None: +def test_headers_not_matching_and_content_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None: +def test_headers_matching_and_content_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: +def test_headers_and_content_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) -def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: +def test_url_and_headers_and_content_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", - match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, + match_headers={ + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + }, match_content=b"This is the body", ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url", content=b"This is the body") assert response.content == b"" @@ -1724,154 +1879,165 @@ def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_not_matching_and_url_and_content_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_and_headers_not_matching_and_content_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: +def test_url_and_headers_and_content_not_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match any request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) -def test_method_and_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: +def test_method_and_url_and_headers_and_content_matching( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( url="https://test_url", method="POST", - match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, + match_headers={ + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}" + }, match_content=b"This is the body", ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.post("https://test_url", content=b"This is the body") assert response.content == b"" @@ -1879,255 +2045,274 @@ def test_method_and_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_not_matching_and_method_and_url_and_content_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_and_headers_not_matching_and_method_and_content_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_and_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_and_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_and_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_matching_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match POST request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response( url="https://test_url2", method="PUT", match_headers={ - "User-Agent": f"python-httpx/{httpx.__version__}", + "User-Agent": f"python-{backend.httpx.__name__}/{backend.httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) - == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: -- Match PUT request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" + == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}'}} headers and b'This is the body' body amongst: +- Match PUT request on https://test_url2 with {{'User-Agent': 'python-{backend.httpx.__name__}/{backend.httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) -def test_header_as_str_tuple_list(httpx_mock: HTTPXMock) -> None: +def test_header_as_str_tuple_list(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")] ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value", "key2": "value2"} -def test_header_as_bytes_tuple_list(httpx_mock: HTTPXMock) -> None: +def test_header_as_bytes_tuple_list( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response( headers=[(b"set-cookie", b"key=value"), (b"set-cookie", b"key2=value2")] ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value", "key2": "value2"} -def test_header_as_bytes_dict(httpx_mock: HTTPXMock) -> None: +def test_header_as_bytes_dict(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(headers={b"set-cookie": b"key=value"}) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value"} -def test_header_as_httpx_headers(httpx_mock: HTTPXMock) -> None: - httpx_mock.add_response(headers=httpx.Headers({"set-cookie": "key=value"})) +def test_header_as_httpx_headers(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: + httpx_mock.add_response(headers=backend.httpx.Headers({"set-cookie": "key=value"})) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value"} -def test_elapsed_when_add_response(httpx_mock: HTTPXMock) -> None: +def test_elapsed_when_add_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response() - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.elapsed is not None -def test_elapsed_when_add_callback(httpx_mock: HTTPXMock) -> None: +def test_elapsed_when_add_callback( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_callback( - callback=lambda req: httpx.Response(status_code=200, json={"foo": "bar"}) + callback=lambda req: backend.httpx.Response( + status_code=200, json={"foo": "bar"} + ) ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.elapsed is not None -def test_non_ascii_url_response(httpx_mock: HTTPXMock) -> None: +def test_non_ascii_url_response(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(url="https://test_url?query_type=数据") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url?query_type=数据") assert response.content == b"" -def test_url_encoded_matching_response(httpx_mock: HTTPXMock) -> None: +def test_url_encoded_matching_response( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url?a=%E6%95%B0%E6%8D%AE") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url?a=数据") assert response.content == b"" -def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None: +def test_reset_is_removing_requests( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response() - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://test_url") assert len(httpx_mock.get_requests()) == 1 @@ -2136,14 +2321,14 @@ def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None: assert len(httpx_mock.get_requests()) == 0 -def test_mutating_json(httpx_mock: HTTPXMock) -> None: +def test_mutating_json(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: mutating_json = {"content": "request 1"} httpx_mock.add_response(json=mutating_json) mutating_json["content"] = "request 2" httpx_mock.add_response(json=mutating_json) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.json() == {"content": "request 1"} @@ -2151,23 +2336,23 @@ def test_mutating_json(httpx_mock: HTTPXMock) -> None: assert response.json() == {"content": "request 2"} -def test_custom_transport(httpx_mock: HTTPXMock) -> None: - class CustomTransport(httpx.HTTPTransport): +def test_custom_transport(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: + class CustomTransport(backend.httpx.HTTPTransport): # type: ignore[name-defined] def __init__(self, prefix: str, *args, **kwargs): super().__init__(*args, **kwargs) self.prefix = prefix def handle_request( self, - request: httpx.Request, - ) -> httpx.Response: + request: "httpx.Request", + ) -> "httpx.Response": httpx_response = super().handle_request(request) httpx_response.headers["x-prefix"] = self.prefix return httpx_response httpx_mock.add_response() - with httpx.Client(transport=CustomTransport(prefix="test")) as client: + with backend.httpx.Client(transport=CustomTransport(prefix="test")) as client: response = client.post("https://test_url", content=b"This is the body") assert response.read() == b"" assert response.headers["x-prefix"] == "test" @@ -2175,6 +2360,7 @@ def handle_request( def test_response_selection_content_matching_with_iterable( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> 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") @@ -2191,7 +2377,7 @@ def stream_content_2() -> Iterable[bytes]: yield b"content" yield b" 2" - with httpx.Client() as client: + with backend.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" @@ -2200,6 +2386,7 @@ def stream_content_2() -> Iterable[bytes]: def test_request_selection_content_matching_with_iterable( httpx_mock: HTTPXMock, + backend: HTTPXBackend, ) -> None: httpx_mock.add_response(match_content=b"full content 1") httpx_mock.add_response(match_content=b"full content 2") @@ -2216,7 +2403,7 @@ def stream_content_2() -> Iterable[bytes]: yield b"content" yield b" 2" - with httpx.Client() as client: + with backend.httpx.Client() as client: client.put("https://test_url_2", content=stream_content_2()) client.put("https://test_url_1", content=stream_content_1()) @@ -2229,23 +2416,23 @@ def stream_content_2() -> Iterable[bytes]: assert request2.url == "https://test_url_2" -def test_files_matching(httpx_mock: HTTPXMock) -> None: +def test_files_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_files={"name": ("file_name", b"File content")}) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.put( "https://test_url", files={"name": ("file_name", b"File content")} ) assert response.content == b"" -def test_files_and_data_matching(httpx_mock: HTTPXMock) -> None: +def test_files_and_data_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, ) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.put( "https://test_url", files={"name": ("file_name", b"File content")}, @@ -2255,7 +2442,9 @@ def test_files_and_data_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> None: +def test_files_not_matching_name( + httpx_mock: HTTPXMock, monkeypatch, backend: HTTPXBackend +) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, @@ -2267,8 +2456,8 @@ def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> None: match_files={"name2": ("file_name", b"File content")}, is_optional=True ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.put( "https://test_url", files={"name1": ("file_name", b"File content")} ) @@ -2280,7 +2469,9 @@ def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) -> None: +def test_files_not_matching_file_name( + httpx_mock: HTTPXMock, monkeypatch, backend: HTTPXBackend +) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, @@ -2292,8 +2483,8 @@ def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) -> Non match_files={"name": ("file_name2", b"File content")}, is_optional=True ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.put( "https://test_url", files={"name": ("file_name1", b"File content")} ) @@ -2305,7 +2496,9 @@ def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) -> Non @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> None: +def test_files_not_matching_content( + httpx_mock: HTTPXMock, monkeypatch, backend: HTTPXBackend +) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, @@ -2317,8 +2510,8 @@ def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> None: match_files={"name": ("file_name", b"File content2")}, is_optional=True ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.put( "https://test_url", files={"name": ("file_name", b"File content1")} ) @@ -2331,7 +2524,7 @@ def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_files_matching_but_data_not_matching( - httpx_mock: HTTPXMock, monkeypatch + httpx_mock: HTTPXMock, monkeypatch, backend: HTTPXBackend ) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( @@ -2346,8 +2539,8 @@ def test_files_matching_but_data_not_matching( is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.put( "https://test_url", files={"name": ("file_name", b"File content")} ) @@ -2368,25 +2561,27 @@ def test_data_without_files(httpx_mock: HTTPXMock) -> None: ) -def test_timeout_matching(httpx_mock: HTTPXMock) -> None: +def test_timeout_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}} ) - with httpx.Client() as client: - response = client.put("https://test_url", timeout=httpx.Timeout(5, write=10)) + with backend.httpx.Client() as client: + response = client.put( + "https://test_url", timeout=backend.httpx.Timeout(5, write=10) + ) assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_timeout_not_matching(httpx_mock: HTTPXMock) -> None: +def test_timeout_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response( match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}}, is_optional=True, ) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url", extensions={"test": "value"}) assert ( str(exception_info.value) @@ -2395,10 +2590,10 @@ def test_timeout_not_matching(httpx_mock: HTTPXMock) -> None: ) -def test_extensions_matching(httpx_mock: HTTPXMock) -> None: +def test_extensions_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_extensions={"test": "value"}) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.put( "https://test_url", extensions={"test": "value", "test2": "value2"} ) @@ -2406,11 +2601,11 @@ def test_extensions_matching(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) -def test_extensions_not_matching(httpx_mock: HTTPXMock) -> None: +def test_extensions_not_matching(httpx_mock: HTTPXMock, backend: HTTPXBackend) -> None: httpx_mock.add_response(match_extensions={"test": "value"}, is_optional=True) - with httpx.Client() as client: - with pytest.raises(httpx.TimeoutException) as exception_info: + with backend.httpx.Client() as client: + with pytest.raises(backend.httpx.TimeoutException) as exception_info: client.get("https://test_url", extensions={"test": "value2"}) assert ( str(exception_info.value) @@ -2419,22 +2614,26 @@ def test_extensions_not_matching(httpx_mock: HTTPXMock) -> None: ) -def test_optional_response_not_matched(httpx_mock: HTTPXMock) -> None: +def test_optional_response_not_matched( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url", is_optional=True) httpx_mock.add_response(url="https://test_url2") - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url2") assert response.content == b"" -def test_optional_response_matched(httpx_mock: HTTPXMock) -> None: +def test_optional_response_matched( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url", is_optional=True) httpx_mock.add_response(url="https://test_url2") - with httpx.Client() as client: + with backend.httpx.Client() as client: response1 = client.get("https://test_url") response2 = client.get("https://test_url2") assert response1.content == b"" @@ -2442,29 +2641,35 @@ def test_optional_response_matched(httpx_mock: HTTPXMock) -> None: @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) -def test_mandatory_response_matched(httpx_mock: HTTPXMock) -> None: +def test_mandatory_response_matched( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url") # This response MUST be requested (overrides global settings via marker) httpx_mock.add_response(url="https://test_url2", is_optional=False) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url2") assert response.content == b"" -def test_multi_response_matched_once(httpx_mock: HTTPXMock) -> None: +def test_multi_response_matched_once( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" -def test_multi_response_matched_twice(httpx_mock: HTTPXMock) -> None: +def test_multi_response_matched_twice( + httpx_mock: HTTPXMock, backend: HTTPXBackend +) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) - with httpx.Client() as client: + with backend.httpx.Client() as client: response1 = client.get("https://test_url") response2 = client.get("https://test_url") assert response1.content == b"" From 7c4c7349295222b1c1e83af500d98e537d70c931 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Mon, 12 Oct 2026 18:39:04 +0300 Subject: [PATCH 10/28] Plugin tests pass --- tests/conftest.py | 12 +++ tests/test_plugin.py | 195 ++++++++++++++++++++----------------------- 2 files changed, 102 insertions(+), 105 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7ff5fef..b188679 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,3 +9,15 @@ @pytest.fixture(params=get_backends(), ids=lambda b: b.name) def backend(request: pytest.FixtureRequest) -> HTTPXBackend: return request.param + + +@pytest.fixture +def plugin_tests_conftest(testdir: pytest.Testdir) -> None: + testdir.makeconftest(""" + import pytest + from pytest_httpx import get_backends + + @pytest.fixture(params=get_backends(), ids=lambda b: b.name) + def backend(request): + return request.param + """) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 13ed9ea..70c4c1f 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,20 +1,18 @@ +import pytest from pytest import Testdir +pytestmark = pytest.mark.usefixtures("plugin_tests_conftest") + def test_fixture_is_available(testdir: Testdir) -> None: - # create a temporary pytest test file testdir.makepyfile(""" - import httpx - - - def test_http(httpx_mock): + def test_http(httpx_mock, backend): mock = httpx_mock.add_response(url="https://foo.tld") - r = httpx.get("https://foo.tld") + r = backend.httpx.get("https://foo.tld") assert httpx_mock.get_request() is not None """) - # run all tests with pytest result = testdir.runpytest() result.assert_outcomes(passed=1) @@ -24,7 +22,7 @@ def test_httpx_mock_unused_response(testdir: Testdir) -> None: Unused responses should fail test case. """ testdir.makepyfile(""" - def test_httpx_mock_unused_response(httpx_mock): + def test_httpx_mock_unused_response(httpx_mock, backend): httpx_mock.add_response() """) result = testdir.runpytest() @@ -49,7 +47,7 @@ def test_httpx_mock_unused_response_without_assertion(testdir: Testdir) -> None: import pytest @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) - def test_httpx_mock_unused_response_without_assertion(httpx_mock): + def test_httpx_mock_unused_response_without_assertion(httpx_mock, backend): httpx_mock.add_response() """) result = testdir.runpytest() @@ -61,7 +59,7 @@ def test_httpx_mock_unused_callback(testdir: Testdir) -> None: Unused callbacks should fail test case. """ testdir.makepyfile(""" - def test_httpx_mock_unused_callback(httpx_mock): + def test_httpx_mock_unused_callback(httpx_mock, backend): def unused(*args, **kwargs): pass @@ -90,7 +88,7 @@ def test_httpx_mock_unused_callback_without_assertion(testdir: Testdir) -> None: import pytest @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) - def test_httpx_mock_unused_callback_without_assertion(httpx_mock): + def test_httpx_mock_unused_callback_without_assertion(httpx_mock, backend): def unused(*args, **kwargs): pass @@ -107,13 +105,12 @@ def test_httpx_mock_unexpected_request(testdir: Testdir) -> None: assert_all_requests_were_expected option is set to True (default). """ testdir.makepyfile(""" - import httpx import pytest - def test_httpx_mock_unexpected_request(httpx_mock): - with httpx.Client() as client: + def test_httpx_mock_unexpected_request(httpx_mock, backend): + with backend.httpx.Client() as client: # Non mocked request - with pytest.raises(httpx.TimeoutException): + with pytest.raises(backend.httpx.TimeoutException): client.get("https://foo.tld") """) result = testdir.runpytest() @@ -135,14 +132,13 @@ def test_httpx_mock_unexpected_request_without_assertion(testdir: Testdir) -> No assert_all_requests_were_expected option is set to False. """ testdir.makepyfile(""" - import httpx import pytest @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) - def test_httpx_mock_unexpected_request(httpx_mock): - with httpx.Client() as client: + def test_httpx_mock_unexpected_request(httpx_mock, backend): + with backend.httpx.Client() as client: # Non mocked request - with pytest.raises(httpx.TimeoutException): + with pytest.raises(backend.httpx.TimeoutException): client.get("https://foo.tld") """) result = testdir.runpytest() @@ -155,15 +151,14 @@ def test_httpx_mock_already_matched_response(testdir: Testdir) -> None: can_send_already_matched_responses option is set to False (default). """ testdir.makepyfile(""" - import httpx import pytest - def test_httpx_mock_already_matched_response(httpx_mock): + def test_httpx_mock_already_matched_response(httpx_mock, backend): httpx_mock.add_response() - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://foo.tld") # Non mocked (already matched) request - with pytest.raises(httpx.TimeoutException): + with pytest.raises(backend.httpx.TimeoutException): client.get("https://foo.tld") """) result = testdir.runpytest() @@ -185,13 +180,12 @@ def test_httpx_mock_reusing_matched_response(testdir: Testdir) -> None: can_send_already_matched_responses option is set to True. """ testdir.makepyfile(""" - import httpx import pytest @pytest.mark.httpx_mock(can_send_already_matched_responses=True) - def test_httpx_mock_reusing_matched_response(httpx_mock): + def test_httpx_mock_reusing_matched_response(httpx_mock, backend): httpx_mock.add_response() - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://foo.tld") # Reusing response client.get("https://foo.tld") @@ -204,11 +198,10 @@ def test_httpx_mock_unmatched_request_without_responses( testdir: Testdir, ) -> None: testdir.makepyfile(""" - import httpx import pytest - def test_httpx_mock_unmatched_request_without_responses(httpx_mock): - with httpx.Client() as client: + def test_httpx_mock_unmatched_request_without_responses(httpx_mock, backend): + with backend.httpx.Client() as client: # This request will not be matched client.get("https://foo22.tld") # This code will not be reached @@ -217,9 +210,9 @@ def test_httpx_mock_unmatched_request_without_responses(httpx_mock): result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred - result.stdout.fnmatch_lines( + result.stdout.re_match_lines( [ - "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld", + r".*httpx2?\.TimeoutException: No response can be found for GET request on https://foo22\.tld", ], consecutive=True, ) @@ -239,16 +232,15 @@ def test_httpx_mock_unmatched_request_with_only_unmatched_responses( testdir: Testdir, ) -> None: testdir.makepyfile(""" - import httpx import pytest - def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock): + def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock, backend): # This response will not be sent (because of a typo in the URL) httpx_mock.add_response(url="https://foo2.tld") # This response will not be sent (because test execution failed earlier) httpx_mock.add_response(url="https://foo3.tld") - with httpx.Client() as client: + with backend.httpx.Client() as client: # This request will not be matched client.get("https://foo22.tld") # This code will not be reached @@ -257,22 +249,22 @@ def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock): result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred - result.stdout.fnmatch_lines( + result.stdout.re_match_lines( [ - "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", - "*- Match any request on https://foo2.tld", - "*- Match any request on https://foo3.tld", + r".*httpx2?\.TimeoutException: No response can be found for GET request on https://foo22\.tld amongst:", + r".*- Match any request on https://foo2\.tld", + r".*- Match any request on https://foo3\.tld", ], consecutive=True, ) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ - "*AssertionError: The following responses are mocked but not requested:", - "* - Match any request on https://foo2.tld", - "* - Match any request on https://foo3.tld", - "* ", - "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", + r"*AssertionError: The following responses are mocked but not requested:", + r"* - Match any request on https://foo2.tld", + r"* - Match any request on https://foo3.tld", + r"* ", + r"* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", ], consecutive=True, ) @@ -282,17 +274,16 @@ def test_httpx_mock_unmatched_request_with_only_unmatched_reusable_responses( testdir: Testdir, ) -> None: testdir.makepyfile(""" - import httpx import pytest @pytest.mark.httpx_mock(can_send_already_matched_responses=True) - def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock): + def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock, backend): # This response will not be sent (because of a typo in the URL) httpx_mock.add_response(url="https://foo2.tld", method="GET") # This response will not be sent (because test execution failed earlier) httpx_mock.add_response(url="https://foo3.tld") - with httpx.Client() as client: + with backend.httpx.Client() as client: # This request will not be matched client.get("https://foo22.tld") # This code will not be reached @@ -301,11 +292,11 @@ def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock): result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred - result.stdout.fnmatch_lines( + result.stdout.re_match_lines( [ - "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", - "*- Match GET request on https://foo2.tld", - "*- Match every request on https://foo3.tld", + r".*httpx2?\.TimeoutException: No response can be found for GET request on https://foo22\.tld amongst:", + r".*- Match GET request on https://foo2\.tld", + r".*- Match every request on https://foo3\.tld", ], consecutive=True, ) @@ -326,16 +317,15 @@ def test_httpx_mock_unmatched_request_with_only_matched_responses( testdir: Testdir, ) -> None: testdir.makepyfile(""" - import httpx import pytest - def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock): + def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock, backend): # Sent response httpx_mock.add_response(url="https://foo.tld") # Sent response httpx_mock.add_response(url="https://foo.tld") - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://foo.tld") client.get("https://foo.tld") # This request will not be matched @@ -346,13 +336,13 @@ def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock): result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred - result.stdout.fnmatch_lines( + result.stdout.re_match_lines( [ - "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", - "*- Already matched any request on https://foo.tld", - "*- Already matched any request on https://foo.tld", - "*", - "*If you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request", + r".*httpx2?\.TimeoutException: No response can be found for GET request on https://foo22\.tld amongst:", + r".*- Already matched any request on https://foo\.tld", + r".*- Already matched any request on https://foo\.tld", + ".*", + r".*If you wanted to reuse an already matched response instead of registering it again, refer to https://github\.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request", ], consecutive=True, ) @@ -372,17 +362,16 @@ def test_httpx_mock_unmatched_request_with_only_matched_reusable_responses( testdir: Testdir, ) -> None: testdir.makepyfile(""" - import httpx import pytest @pytest.mark.httpx_mock(can_send_already_matched_responses=True) - def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock): + def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock, backend): # Sent response httpx_mock.add_response(url="https://foo.tld") # Sent response httpx_mock.add_response(url="https://foo3.tld") - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://foo.tld") client.get("https://foo.tld") client.get("https://foo3.tld") @@ -394,11 +383,11 @@ def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock): result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred - result.stdout.fnmatch_lines( + result.stdout.re_match_lines( [ - "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", - "*- Match every request on https://foo.tld", - "*- Match every request on https://foo3.tld", + r".*httpx2?\.TimeoutException: No response can be found for GET request on https://foo22\.tld amongst:", + r".*- Match every request on https://foo\.tld", + r".*- Match every request on https://foo3\.tld", ], consecutive=True, ) @@ -418,10 +407,9 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses( testdir: Testdir, ) -> None: testdir.makepyfile(""" - import httpx import pytest - def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock): + def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock, backend): # Sent response httpx_mock.add_response(url="https://foo.tld") # This response will not be sent (because of a typo in the URL) @@ -431,7 +419,7 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx # This response will not be sent (because test execution failed earlier) httpx_mock.add_response(url="https://foo3.tld") - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://foo.tld") client.get("https://foo.tld") # This request will not be matched @@ -442,15 +430,15 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred - result.stdout.fnmatch_lines( + result.stdout.re_match_lines( [ - "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", - "*- Match any request on https://foo2.tld", - "*- Match any request on https://foo3.tld", - "*- Already matched any request on https://foo.tld", - "*- Already matched any request on https://foo.tld", - "*", - "*If you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request", + r".*httpx2?\.TimeoutException: No response can be found for GET request on https://foo22\.tld amongst:", + r".*- Match any request on https://foo2\.tld", + r".*- Match any request on https://foo3\.tld", + r".*- Already matched any request on https://foo\.tld", + r".*- Already matched any request on https://foo\.tld", + ".*", + r".*If you wanted to reuse an already matched response instead of registering it again, refer to https://github\.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request", ], consecutive=True, ) @@ -471,11 +459,10 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_reusable_respon testdir: Testdir, ) -> None: testdir.makepyfile(""" - import httpx import pytest @pytest.mark.httpx_mock(can_send_already_matched_responses=True) - def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock): + def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock, backend): # Sent response httpx_mock.add_response(url="https://foo.tld") # This response will not be sent (because of a typo in the URL) @@ -485,7 +472,7 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx # This response will not be sent (because test execution failed earlier) httpx_mock.add_response(url="https://foo4.tld") - with httpx.Client() as client: + with backend.httpx.Client() as client: client.get("https://foo.tld") client.get("https://foo2.tld") client.get("https://foo.tld") @@ -497,13 +484,13 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred - result.stdout.fnmatch_lines( + result.stdout.re_match_lines( [ - "*httpx.TimeoutException: No response can be found for GET request on https://foo3.tld amongst:", - "*- Match every request on https://foo33.tld", - "*- Match every request on https://foo4.tld", - "*- Match every request on https://foo.tld", - "*- Match every request on https://foo2.tld", + r".*httpx2?.TimeoutException: No response can be found for GET request on https://foo3\.tld amongst:", + r".*- Match every request on https://foo33\.tld", + r".*- Match every request on https://foo4\.tld", + r".*- Match every request on https://foo\.tld", + r".*- Match every request on https://foo2\.tld", ], consecutive=True, ) @@ -525,19 +512,18 @@ def test_httpx_mock_should_mock_sync(testdir: Testdir) -> None: Non mocked requests should go through while other requests should be mocked. """ testdir.makepyfile(""" - import httpx import pytest @pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "localhost") - def test_httpx_mock_should_mock_sync(httpx_mock): + def test_httpx_mock_should_mock_sync(httpx_mock, backend): httpx_mock.add_response() - with httpx.Client() as client: + with backend.httpx.Client() as client: # Mocked request client.get("https://foo.tld") # Non mocked request - with pytest.raises(httpx.ConnectError): + with pytest.raises(backend.httpx.ConnectError): client.get("https://localhost:5005") # Assert that a single request was mocked @@ -553,20 +539,19 @@ def test_httpx_mock_should_mock_async(testdir: Testdir) -> None: Non mocked requests should go through while other requests should be mocked. """ testdir.makepyfile(""" - import httpx import pytest @pytest.mark.asyncio @pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "localhost") - async def test_httpx_mock_should_mock_async(httpx_mock): + async def test_httpx_mock_should_mock_async(httpx_mock, backend): httpx_mock.add_response() - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: # Mocked request await client.get("https://foo.tld") # Non mocked request - with pytest.raises(httpx.ConnectError): + with pytest.raises(backend.httpx.ConnectError): await client.get("https://localhost:5005") # Assert that a single request was mocked @@ -587,37 +572,40 @@ def test_httpx_mock_options_on_multi_levels_are_aggregated(testdir: Testdir) -> """ testdir.makeconftest(""" import pytest + from pytest_httpx import get_backends + @pytest.fixture(params=get_backends(), ids=lambda b: b.name) + def backend(request): + return request.param def pytest_collection_modifyitems(session, config, items): for item in items: item.add_marker(pytest.mark.httpx_mock(assert_all_responses_were_requested=False)) """) testdir.makepyfile(""" - import httpx import pytest pytestmark = pytest.mark.httpx_mock(assert_all_requests_were_expected=False, should_mock=lambda request: request.url.host != "https://foo.tld") @pytest.mark.asyncio @pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "localhost") - async def test_httpx_mock_options_on_multi_levels_are_aggregated(httpx_mock): + async def test_httpx_mock_options_on_multi_levels_are_aggregated(httpx_mock, backend): httpx_mock.add_response(url="https://foo.tld", headers={"x-pytest-httpx": "this was mocked"}) # This response will never be used, testing that assert_all_responses_were_requested is handled httpx_mock.add_response(url="https://never_called.url") - async with httpx.AsyncClient() as client: + async with backend.httpx.AsyncClient() as client: # Assert that previously set should_mock was overridden response = await client.get("https://foo.tld") assert response.headers["x-pytest-httpx"] == "this was mocked" # Assert that latest should_mock is handled - with pytest.raises(httpx.ConnectError): + with pytest.raises(backend.httpx.ConnectError): await client.get("https://localhost:5005") # Assert that assert_all_requests_were_expected is the one at module level - with pytest.raises(httpx.TimeoutException): + with pytest.raises(backend.httpx.TimeoutException): await client.get("https://unexpected.url") # Assert that 2 requests out of 3 were mocked @@ -636,7 +624,7 @@ def test_invalid_marker(testdir: Testdir) -> None: import pytest @pytest.mark.httpx_mock(foo=123) - def test_invalid_marker(httpx_mock): + def test_invalid_marker(httpx_mock, backend): pass """) @@ -650,11 +638,10 @@ def test_mandatory_response_not_matched(testdir: Testdir) -> None: is_optional MUST take precedence over assert_all_responses_were_requested. """ testdir.makepyfile(""" - import httpx import pytest @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) - def test_mandatory_response_not_matched(httpx_mock): + def test_mandatory_response_not_matched(httpx_mock, backend): # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url") # This response MUST be requested @@ -677,9 +664,7 @@ def test_mandatory_response_not_matched(httpx_mock): def test_reusable_response_not_matched(testdir: Testdir) -> None: testdir.makepyfile(""" - import httpx - - def test_reusable_response_not_matched(httpx_mock): + def test_reusable_response_not_matched(httpx_mock, backend): httpx_mock.add_response(url="https://test_url2", is_reusable=True) """) From 7e6d9b4ed089da939b233c77fd36beba55a02266 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Mon, 12 Oct 2026 20:01:18 +0300 Subject: [PATCH 11/28] Update number of outcomes with the backends count --- tests/test_httpx_async.py | 4 ++-- tests/test_httpx_sync.py | 4 ++-- tests/test_plugin.py | 50 ++++++++++++++++++++++----------------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index 6361d77..a28fb10 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -11,7 +11,7 @@ from pytest import Testdir import pytest_httpx -from pytest_httpx import HTTPXBackend, HTTPXMock +from pytest_httpx import HTTPXBackend, HTTPXMock, get_backends if TYPE_CHECKING: try: @@ -1562,7 +1562,7 @@ async def test_request_retrieval_with_more_than_one(httpx_mock, backend): httpx_mock.get_request(url=backend.httpx.URL("https://test_url")) """) result = testdir.runpytest() - result.assert_outcomes(failed=1) + result.assert_outcomes(failed=len(get_backends())) result.stdout.fnmatch_lines( [ "*AssertionError: More than one request (2) matched, use get_requests instead or refine your filters." diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py index 130068a..3b537b5 100644 --- a/tests/test_httpx_sync.py +++ b/tests/test_httpx_sync.py @@ -8,7 +8,7 @@ from pytest import Testdir import pytest_httpx -from pytest_httpx import HTTPXBackend, HTTPXMock +from pytest_httpx import HTTPXBackend, HTTPXMock, get_backends if TYPE_CHECKING: try: @@ -1262,7 +1262,7 @@ def test_request_retrieval_with_more_than_one(httpx_mock, backend): httpx_mock.get_request(url=backend.httpx.URL("https://test_url")) """) result = testdir.runpytest() - result.assert_outcomes(failed=1) + result.assert_outcomes(failed=len(get_backends())) result.stdout.fnmatch_lines( [ "*AssertionError: More than one request (2) matched, use get_requests instead or refine your filters." diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 70c4c1f..977536a 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,9 +1,15 @@ import pytest from pytest import Testdir +from pytest_httpx import get_backends + pytestmark = pytest.mark.usefixtures("plugin_tests_conftest") +def backends_count() -> int: + return len(get_backends()) + + def test_fixture_is_available(testdir: Testdir) -> None: testdir.makepyfile(""" def test_http(httpx_mock, backend): @@ -14,7 +20,7 @@ def test_http(httpx_mock, backend): """) result = testdir.runpytest() - result.assert_outcomes(passed=1) + result.assert_outcomes(passed=backends_count()) def test_httpx_mock_unused_response(testdir: Testdir) -> None: @@ -26,7 +32,7 @@ def test_httpx_mock_unused_response(httpx_mock, backend): httpx_mock.add_response() """) result = testdir.runpytest() - result.assert_outcomes(errors=1, passed=1) + result.assert_outcomes(errors=backends_count(), passed=backends_count()) result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", @@ -51,7 +57,7 @@ def test_httpx_mock_unused_response_without_assertion(httpx_mock, backend): httpx_mock.add_response() """) result = testdir.runpytest() - result.assert_outcomes(passed=1) + result.assert_outcomes(passed=backends_count()) def test_httpx_mock_unused_callback(testdir: Testdir) -> None: @@ -67,7 +73,7 @@ def unused(*args, **kwargs): """) result = testdir.runpytest() - result.assert_outcomes(errors=1, passed=1) + result.assert_outcomes(errors=backends_count(), passed=backends_count()) result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", @@ -96,7 +102,7 @@ def unused(*args, **kwargs): """) result = testdir.runpytest() - result.assert_outcomes(passed=1) + result.assert_outcomes(passed=backends_count()) def test_httpx_mock_unexpected_request(testdir: Testdir) -> None: @@ -114,7 +120,7 @@ def test_httpx_mock_unexpected_request(httpx_mock, backend): client.get("https://foo.tld") """) result = testdir.runpytest() - result.assert_outcomes(errors=1, passed=1) + result.assert_outcomes(errors=backends_count(), passed=backends_count()) result.stdout.fnmatch_lines( [ "*AssertionError: The following requests were not expected:", @@ -142,7 +148,7 @@ def test_httpx_mock_unexpected_request(httpx_mock, backend): client.get("https://foo.tld") """) result = testdir.runpytest() - result.assert_outcomes(passed=1) + result.assert_outcomes(passed=backends_count()) def test_httpx_mock_already_matched_response(testdir: Testdir) -> None: @@ -162,7 +168,7 @@ def test_httpx_mock_already_matched_response(httpx_mock, backend): client.get("https://foo.tld") """) result = testdir.runpytest() - result.assert_outcomes(errors=1, passed=1) + result.assert_outcomes(errors=backends_count(), passed=backends_count()) result.stdout.fnmatch_lines( [ "*AssertionError: The following requests were not expected:", @@ -191,7 +197,7 @@ def test_httpx_mock_reusing_matched_response(httpx_mock, backend): client.get("https://foo.tld") """) result = testdir.runpytest() - result.assert_outcomes(passed=1) + result.assert_outcomes(passed=backends_count()) def test_httpx_mock_unmatched_request_without_responses( @@ -208,7 +214,7 @@ def test_httpx_mock_unmatched_request_without_responses(httpx_mock, backend): client.get("https://foo3.tld") """) result = testdir.runpytest() - result.assert_outcomes(errors=1, failed=1) + result.assert_outcomes(errors=backends_count(), failed=backends_count()) # Assert the error that occurred result.stdout.re_match_lines( [ @@ -247,7 +253,7 @@ def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock, client.get("https://foo3.tld") """) result = testdir.runpytest() - result.assert_outcomes(errors=1, failed=1) + result.assert_outcomes(errors=backends_count(), failed=backends_count()) # Assert the error that occurred result.stdout.re_match_lines( [ @@ -290,7 +296,7 @@ def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock, client.get("https://foo3.tld") """) result = testdir.runpytest() - result.assert_outcomes(errors=1, failed=1) + result.assert_outcomes(errors=backends_count(), failed=backends_count()) # Assert the error that occurred result.stdout.re_match_lines( [ @@ -334,7 +340,7 @@ def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock, ba client.get("https://foo3.tld") """) result = testdir.runpytest() - result.assert_outcomes(errors=1, failed=1) + result.assert_outcomes(errors=backends_count(), failed=backends_count()) # Assert the error that occurred result.stdout.re_match_lines( [ @@ -381,7 +387,7 @@ def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock, ba client.get("https://foo3.tld") """) result = testdir.runpytest() - result.assert_outcomes(errors=1, failed=1) + result.assert_outcomes(errors=backends_count(), failed=backends_count()) # Assert the error that occurred result.stdout.re_match_lines( [ @@ -428,7 +434,7 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx client.get("https://foo3.tld") """) result = testdir.runpytest() - result.assert_outcomes(errors=1, failed=1) + result.assert_outcomes(errors=backends_count(), failed=backends_count()) # Assert the error that occurred result.stdout.re_match_lines( [ @@ -482,7 +488,7 @@ def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx client.get("https://foo2.tld") """) result = testdir.runpytest() - result.assert_outcomes(errors=1, failed=1) + result.assert_outcomes(errors=backends_count(), failed=backends_count()) # Assert the error that occurred result.stdout.re_match_lines( [ @@ -531,7 +537,7 @@ def test_httpx_mock_should_mock_sync(httpx_mock, backend): """) result = testdir.runpytest() - result.assert_outcomes(passed=1) + result.assert_outcomes(passed=backends_count()) def test_httpx_mock_should_mock_async(testdir: Testdir) -> None: @@ -559,7 +565,7 @@ async def test_httpx_mock_should_mock_async(httpx_mock, backend): """) result = testdir.runpytest() - result.assert_outcomes(passed=1) + result.assert_outcomes(passed=backends_count()) def test_httpx_mock_options_on_multi_levels_are_aggregated(testdir: Testdir) -> None: @@ -613,7 +619,7 @@ async def test_httpx_mock_options_on_multi_levels_are_aggregated(httpx_mock, bac """) result = testdir.runpytest() - result.assert_outcomes(passed=1) + result.assert_outcomes(passed=backends_count()) def test_invalid_marker(testdir: Testdir) -> None: @@ -629,7 +635,7 @@ def test_invalid_marker(httpx_mock, backend): """) result = testdir.runpytest() - result.assert_outcomes(errors=1) + result.assert_outcomes(errors=backends_count()) result.stdout.re_match_lines([r".*got an unexpected keyword argument 'foo'"]) @@ -649,7 +655,7 @@ def test_mandatory_response_not_matched(httpx_mock, backend): """) result = testdir.runpytest() - result.assert_outcomes(errors=1, passed=1) + result.assert_outcomes(errors=backends_count(), passed=backends_count()) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ @@ -669,7 +675,7 @@ def test_reusable_response_not_matched(httpx_mock, backend): """) result = testdir.runpytest() - result.assert_outcomes(errors=1, passed=1) + result.assert_outcomes(errors=backends_count(), passed=backends_count()) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ From 2b7c537cdad35a8153f6ddc18de74aa2f9d3ecbd Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Mon, 12 Oct 2026 20:02:35 +0300 Subject: [PATCH 12/28] Make error messages use the correct backend name --- pytest_httpx/_httpx_mock.py | 2 +- tests/test_httpx_async.py | 4 ++-- tests/test_httpx_sync.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 311dffe..d6bbd08 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -235,7 +235,7 @@ def _explain_that_callback_must_return_a_response( real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, request: httpx.Request, ) -> str: - return f"Callback registered for {RequestDescription(backend, real_transport, request, [])} MUST return httpx.Response" + return f"Callback registered for {RequestDescription(backend, real_transport, request, [])} MUST return {backend.name}.Response" def _explain_that_no_response_was_found( self, diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index a28fb10..fdb0c91 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -1309,7 +1309,7 @@ async def test_callback_not_returning_a_response( await client.get("https://test_url") assert ( str(exception_info.value) - == "Callback registered for GET request on https://test_url MUST return httpx.Response" + == f"Callback registered for GET request on https://test_url MUST return {backend.name}.Response" ) @@ -1332,7 +1332,7 @@ async def invalid_response( await client.get("https://test_url") assert ( str(exception_info.value) - == "Callback registered for GET request on https://test_url MUST return httpx.Response" + == f"Callback registered for GET request on https://test_url MUST return {backend.name}.Response" ) diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py index 3b537b5..33c949b 100644 --- a/tests/test_httpx_sync.py +++ b/tests/test_httpx_sync.py @@ -1155,7 +1155,7 @@ def test_callback_not_returning_a_response( client.get("https://test_url") assert ( str(exception_info.value) - == "Callback registered for GET request on https://test_url MUST return httpx.Response" + == f"Callback registered for GET request on https://test_url MUST return {backend.name}.Response" ) From ec9730cfa5fdc5b322dfa8aa12e44a0aaad42f26 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Mon, 12 Oct 2026 20:24:21 +0300 Subject: [PATCH 13/28] Add tests covering the case of both backends installed --- pytest_httpx/_httpx_compat.py | 8 +- tests/test_backends.py | 163 ++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 tests/test_backends.py diff --git a/pytest_httpx/_httpx_compat.py b/pytest_httpx/_httpx_compat.py index 9609679..39df84f 100644 --- a/pytest_httpx/_httpx_compat.py +++ b/pytest_httpx/_httpx_compat.py @@ -23,20 +23,20 @@ def get_backends() -> list[HTTPXBackend]: backends = [] try: - import httpcore2 + import httpcore2 # pragma: no cover import httpx2 except ModuleNotFoundError: # pragma: no cover pass else: - backends.append(HTTPXBackend("httpx2", httpx2, httpcore2)) + backends.append(HTTPXBackend("httpx2", httpx2, httpcore2)) # pragma: no cover try: - import httpcore as httpcore_ + import httpcore as httpcore_ # pragma: no cover import httpx as httpx_ except ModuleNotFoundError: # pragma: no cover pass else: - backends.append(HTTPXBackend("httpx", httpx_, httpcore_)) + backends.append(HTTPXBackend("httpx", httpx_, httpcore_)) # pragma: no cover if not backends: # pragma: no cover raise RuntimeError( diff --git a/tests/test_backends.py b/tests/test_backends.py new file mode 100644 index 0000000..a0174f6 --- /dev/null +++ b/tests/test_backends.py @@ -0,0 +1,163 @@ +import pytest + +try: + import httpx + import httpx2 +except ModuleNotFoundError: + # If one of the modules isn't installed, there's no sense running these tests. + pytest.skip( + allow_module_level=True, reason="One of http/httpx2 modules is missing." + ) + +from pytest_httpx import HTTPXMock, get_backends + + +def test_two_backends_two_names() -> None: + backends = get_backends() + names = {backend.name for backend in backends} + + assert names == {"httpx", "httpx2"} + + +def test_two_backends_are_different() -> None: + backends = get_backends() + modules = {id(backend.httpx) for backend in backends} + + assert len(modules) == 2 + + +def test_single_add_response_mocks_both_backends(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(text="hello", is_reusable=True) + + with httpx.Client() as client: + httpx_response = client.get("https://test_url") + + with httpx2.Client() as client: + httpx2_response = client.get("https://test_url") + + assert httpx_response.text == "hello" + assert httpx2_response.text == "hello" + + +@pytest.mark.asyncio +async def test_single_add_response_mocks_both_async_backends( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response(text="async hello", is_reusable=True) + + async with httpx.AsyncClient() as client: + httpx_response = await client.get("https://test_url") + + async with httpx2.AsyncClient() as client: + httpx2_response = await client.get("https://test_url") + + assert httpx_response.text == "async hello" + assert httpx2_response.text == "async hello" + + +def test_response_type_matches_backend(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response(is_reusable=True) + + with httpx.Client() as client: + httpx_response = client.get("https://test_url") + + with httpx2.Client() as client: + httpx2_response = client.get("https://test_url") + + # Each response must be an instance of its own backend's Response type. + assert isinstance(httpx_response, httpx.Response) + assert not isinstance(httpx_response, httpx2.Response) + + assert isinstance(httpx2_response, httpx2.Response) + assert not isinstance(httpx2_response, httpx.Response) + + +def test_requests_from_both_backends_are_retrieved( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response(is_reusable=True) + + with httpx.Client() as client: + client.get("https://httpx_url") + + with httpx2.Client() as client: + client.options("https://httpx2_url") + + all_requests = httpx_mock.get_requests() + + assert len(all_requests) == 2 + + httpx_request = httpx_mock.get_request(url="https://httpx_url") + + assert httpx_request + assert httpx_request.method == "GET" + + httpx2_request = httpx_mock.get_request(url="https://httpx2_url") + + assert httpx2_request + assert httpx2_request.method == "OPTIONS" + + +def test_responses_matched_in_registration_order( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response(text="first") + httpx_mock.add_response(text="second") + + with httpx.Client() as client: + first_response = client.get("https://test_url") + + with httpx2.Client() as client: + second_response = client.get("https://test_url") + + assert first_response.text == "first" + assert second_response.text == "second" + + +def test_add_exception_raises_matching_backend_error(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_exception( + httpx.ReadTimeout("HTTPX timeout"), url="https://httpx_url" + ) + httpx_mock.add_exception( + httpx2.ReadTimeout("HTTPX2 timeout"), url="https://httpx2_url" + ) + + with httpx.Client() as client, pytest.raises(httpx.ReadTimeout) as httpx_exc: + client.get("https://httpx_url") + + assert str(httpx_exc.value) == "HTTPX timeout" + assert httpx_exc.value.request.url == httpx.URL("https://httpx_url") + + with httpx2.Client() as client, pytest.raises(httpx2.ReadTimeout) as httpx2_exc: + client.get("https://httpx2_url") + + assert str(httpx2_exc.value) == "HTTPX2 timeout" + assert httpx2_exc.value.request.url == httpx2.URL("https://httpx2_url") + + +seen_request_modules: set[str] = set() + + +def _should_mock(request: httpx.Request | httpx2.Request) -> bool: # type: ignore[no-untyped-def] + # Record the module of the request object so the test below could check them. + match type(request): + case httpx.Request: + seen_request_modules.add("httpx") + case httpx2.Request: + seen_request_modules.add("httpx2") + + return True + + +@pytest.mark.httpx_mock(should_mock=_should_mock) +def test_should_mock_used_by_both_backends(httpx_mock: HTTPXMock) -> None: + seen_request_modules.clear() + httpx_mock.add_response(is_reusable=True) + + with httpx.Client() as client: + client.get("https://test_url") + + with httpx2.Client() as client: + client.get("https://test_url") + + assert set(seen_request_modules) == {"httpx", "httpx2"} From 9c0de8627423e490c7f33e800555b10599d6ff92 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Tue, 13 Oct 2026 02:44:41 +0300 Subject: [PATCH 14/28] Add tox config --- tox.toml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tox.toml diff --git a/tox.toml b/tox.toml new file mode 100644 index 0000000..a36073d --- /dev/null +++ b/tox.toml @@ -0,0 +1,19 @@ +env_list = ["clean", "httpx", "httpx2", "both"] +isolated_build = true + +[env_run_base] +# Editable mode so that coverage measures the repo source. +package = "editable" +extras = ["testing", "{env_name}"] +commands = [ + ["pytest", "--cov-append", { replace = "posargs", extend = true }], +] + +[env.clean] +skip_install = true +deps = ["coverage[toml]"] +commands = [["coverage", "erase"]] + +[env.both] +# The "both" environment needs both extras, so override the {env_name}-derived one. +extras = ["testing", "httpx", "httpx2"] From 3c4c9c7b2504cde0d6de8d6eabd2756b287b79e2 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Tue, 13 Oct 2026 19:46:42 +0300 Subject: [PATCH 15/28] Refactor _RequestMatcher to drop the runtime httpx.URL dependency --- pytest_httpx/_httpx_compat.py | 26 ++++-------- pytest_httpx/_httpx_internals.py | 33 ++++++++------- pytest_httpx/_httpx_mock.py | 72 ++++++++++++++++---------------- pytest_httpx/_options.py | 6 ++- pytest_httpx/_pretty_print.py | 11 +++-- pytest_httpx/_request_matcher.py | 60 +++++++++++++------------- tests/test_backends.py | 20 ++++++++- 7 files changed, 125 insertions(+), 103 deletions(-) diff --git a/pytest_httpx/_httpx_compat.py b/pytest_httpx/_httpx_compat.py index 39df84f..1d43011 100644 --- a/pytest_httpx/_httpx_compat.py +++ b/pytest_httpx/_httpx_compat.py @@ -1,4 +1,3 @@ -import warnings from dataclasses import dataclass from functools import cache from types import ModuleType @@ -23,12 +22,12 @@ def get_backends() -> list[HTTPXBackend]: backends = [] try: - import httpcore2 # pragma: no cover - import httpx2 + import httpcore2 as httpcore2_ # pragma: no cover + import httpx2 as httpx2_ except ModuleNotFoundError: # pragma: no cover pass else: - backends.append(HTTPXBackend("httpx2", httpx2, httpcore2)) # pragma: no cover + backends.append(HTTPXBackend("httpx2", httpx2_, httpcore2_)) # pragma: no cover try: import httpcore as httpcore_ # pragma: no cover @@ -51,18 +50,9 @@ def get_backends() -> list[HTTPXBackend]: if TYPE_CHECKING: - import httpcore2 as httpcore - import httpx2 as httpx -else: - # A module-level backend for global httpx/httpcore names references. - _backends = get_backends() - [_default_backend, *_] = _backends - httpx = _default_backend.httpx - httpcore = _default_backend.httpcore + import httpcore # noqa: F401 + import httpcore2 # noqa: F401 + import httpx + import httpx2 - if any(backend.name == "httpx" for backend in _backends): # pragma: no cover - warnings.warn( - "Using httpx with pytest-httpx is deprecated; consider using httpx2 instead.", - PytestHTTPXDeprecationWarning, - stacklevel=2, - ) + HttpxURL = httpx.URL | httpx2.URL diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index 5b26b8f..8b80ff8 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -1,18 +1,21 @@ import base64 from collections.abc import AsyncIterator, Iterable, Iterator, Sequence -from typing import TypeAlias +from typing import TYPE_CHECKING, TypeAlias, Union -from pytest_httpx._httpx_compat import HTTPXBackend, get_backends, httpcore, httpx +from pytest_httpx._httpx_compat import HTTPXBackend, get_backends + +if TYPE_CHECKING: + from pytest_httpx._httpx_compat import HttpxURL, httpcore, httpcore2, httpx # Those types are internally defined within httpx._types -HeaderTypes: TypeAlias = ( - httpx.Headers - | dict[str, str] - | dict[bytes, bytes] - | Sequence[tuple[str, str]] - | Sequence[tuple[bytes, bytes]] -) -PrimitiveData: TypeAlias = str | int | float | bool | None +HeaderTypes: TypeAlias = Union[ + "httpx.Headers", + dict[str, str], + dict[bytes, bytes], + Sequence[tuple[str, str]], + Sequence[tuple[bytes, bytes]], +] +PrimitiveData: TypeAlias = str | int | float | bool def _iterator_stream_bases() -> list[type]: @@ -38,8 +41,10 @@ async def __aiter__(self) -> AsyncIterator[bytes]: def _to_httpx_url( - backend: HTTPXBackend, url: httpcore.URL, headers: list[tuple[bytes, bytes]] -) -> httpx.URL: + backend: HTTPXBackend, + url: "httpcore.URL | httpcore2.URL", + headers: list[tuple[bytes, bytes]], +) -> "HttpxURL": for name, value in headers: if b"Proxy-Authorization" == name: return backend.httpx.URL( @@ -60,8 +65,8 @@ def _to_httpx_url( def _proxy_url( backend: HTTPXBackend, - real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, -) -> httpx.URL | None: + real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", +) -> "httpx.URL | None": if isinstance( real_pool := real_transport._pool, (backend.httpcore.HTTPProxy, backend.httpcore.AsyncHTTPProxy), diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index d6bbd08..1dd121d 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -1,14 +1,23 @@ import copy import inspect from collections.abc import Awaitable, Callable -from typing import Any +from typing import TYPE_CHECKING, Any, TypeAlias from pytest_httpx import _httpx_internals -from pytest_httpx._httpx_compat import HTTPXBackend, httpx +from pytest_httpx._httpx_compat import HTTPXBackend from pytest_httpx._options import _HTTPXMockOptions from pytest_httpx._pretty_print import RequestDescription from pytest_httpx._request_matcher import _RequestMatcher +if TYPE_CHECKING: + from pytest_httpx._httpx_compat import httpx + + +Callback: TypeAlias = Callable[ + ["httpx.Request"], + "httpx.Response | None | Awaitable[httpx.Response | None]", +] + class HTTPXMock: """ @@ -24,10 +33,7 @@ def __init__(self, options: _HTTPXMockOptions) -> None: self._callbacks: list[ tuple[ _RequestMatcher, - Callable[ - [httpx.Request], - httpx.Response | None | Awaitable[httpx.Response | None], - ], + Callback, ] ] = [] self._requests_not_matched: list[httpx.Request] = [] @@ -73,7 +79,7 @@ def add_response( json = copy.deepcopy(json) if json is not None else None - def response_callback(request: httpx.Request) -> httpx.Response: + def response_callback(request: "httpx.Request") -> "httpx.Response": # Build the response with the backend that intercepted the request # so that its type matches the client. backend = _get_backend(request) @@ -92,10 +98,7 @@ def response_callback(request: httpx.Request) -> httpx.Response: def add_callback( self, - callback: Callable[ - [httpx.Request], - httpx.Response | None | Awaitable[httpx.Response | None], - ], + callback: Callback, **matchers: Any, ) -> None: """ @@ -141,7 +144,7 @@ def add_exception(self, exception: BaseException, **matchers: Any) -> None: :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). """ - def exception_callback(request: httpx.Request) -> None: + def exception_callback(request: "httpx.Request") -> None: backend = _get_backend(request) if isinstance(exception, backend.httpx.RequestError): @@ -153,9 +156,9 @@ def exception_callback(request: httpx.Request) -> None: def _handle_request( self, backend: HTTPXBackend, - real_transport: httpx.HTTPTransport, - request: httpx.Request, - ) -> httpx.Response: + real_transport: "httpx.HTTPTransport", + request: "httpx.Request", + ) -> "httpx.Response": # Save the backend that intercepted this request so that callbacks # use the matching library. _set_backend(request, backend) @@ -188,9 +191,9 @@ def _handle_request( async def _handle_async_request( self, backend: HTTPXBackend, - real_transport: httpx.AsyncHTTPTransport, - request: httpx.Request, - ) -> httpx.Response: + real_transport: "httpx.AsyncHTTPTransport", + request: "httpx.Request", + ) -> "httpx.Response": # Save the backend that intercepted this request so that callbacks # use the matching library. _set_backend(request, backend) @@ -224,24 +227,24 @@ async def _handle_async_request( ) def _request_not_matched( - self, backend: HTTPXBackend, request: httpx.Request, message: str - ) -> httpx.TimeoutException: + self, backend: HTTPXBackend, request: "httpx.Request", message: str + ) -> "httpx.TimeoutException": self._requests_not_matched.append(request) return backend.httpx.TimeoutException(message, request=request) def _explain_that_callback_must_return_a_response( self, backend: HTTPXBackend, - real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, - request: httpx.Request, + real_transport: "httpx.BaseTransport | httpx.AsyncBaseTransport", + request: "httpx.Request", ) -> str: return f"Callback registered for {RequestDescription(backend, real_transport, request, [])} MUST return {backend.name}.Response" def _explain_that_no_response_was_found( self, backend: HTTPXBackend, - real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, - request: httpx.Request, + real_transport: "httpx.BaseTransport | httpx.AsyncBaseTransport", + request: "httpx.Request", ) -> str: matchers = [matcher for matcher, _ in self._callbacks] @@ -270,14 +273,9 @@ def _explain_that_no_response_was_found( def _get_callback( self, backend: HTTPXBackend, - real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, - request: httpx.Request, - ) -> ( - Callable[ - [httpx.Request], httpx.Response | None | Awaitable[httpx.Response | None] - ] - | None - ): + real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", + request: "httpx.Request", + ) -> Callback | None: callbacks = [ (matcher, callback) for matcher, callback in self._callbacks @@ -303,7 +301,7 @@ def _get_callback( # All callbacks have already been matched and last registered cannot be reused return None - def get_requests(self, **matchers: Any) -> list[httpx.Request]: + def get_requests(self, **matchers: Any) -> list["httpx.Request"]: """ Return all requests sent that match (empty list if no requests were matched). @@ -327,7 +325,7 @@ def get_requests(self, **matchers: Any) -> list[httpx.Request]: if matcher.match(_get_backend(request), real_transport, request) ] - def get_request(self, **matchers: Any) -> httpx.Request | None: + def get_request(self, **matchers: Any) -> "httpx.Request | None": """ Return the single request that match (or None). @@ -389,18 +387,18 @@ def _assert_options(self) -> None: _BACKEND_ATTRIBUTE = "__pytest_httpx_backend" -def _set_backend(request: httpx.Request, backend: HTTPXBackend) -> None: +def _set_backend(request: "httpx.Request", backend: HTTPXBackend) -> None: # Store the intercepting backend on the request so that backend-agnostic # callbacks (e.g. the one built by `add_response`) can build a response # with the matching library. setattr(request, _BACKEND_ATTRIBUTE, backend) -def _get_backend(request: httpx.Request) -> HTTPXBackend: +def _get_backend(request: "httpx.Request") -> HTTPXBackend: return getattr(request, _BACKEND_ATTRIBUTE) -def _unread(response: httpx.Response) -> httpx.Response: +def _unread(response: "httpx.Response") -> "httpx.Response": # Allow to read the response on client side response.is_stream_consumed = False response.is_closed = False diff --git a/pytest_httpx/_options.py b/pytest_httpx/_options.py index bab80db..5c9131b 100644 --- a/pytest_httpx/_options.py +++ b/pytest_httpx/_options.py @@ -1,6 +1,8 @@ from collections.abc import Callable +from typing import TYPE_CHECKING -from pytest_httpx._httpx_compat import httpx +if TYPE_CHECKING: + from pytest_httpx._httpx_compat import httpx class _HTTPXMockOptions: @@ -10,7 +12,7 @@ def __init__( assert_all_responses_were_requested: bool = True, assert_all_requests_were_expected: bool = True, can_send_already_matched_responses: bool = False, - should_mock: Callable[[httpx.Request], bool] = lambda request: True, + should_mock: Callable[["httpx.Request"], bool] = lambda request: True, ) -> None: self.assert_all_responses_were_requested = assert_all_responses_were_requested self.assert_all_requests_were_expected = assert_all_requests_were_expected diff --git a/pytest_httpx/_pretty_print.py b/pytest_httpx/_pretty_print.py index 5d1acbb..98a6e46 100644 --- a/pytest_httpx/_pretty_print.py +++ b/pytest_httpx/_pretty_print.py @@ -1,14 +1,19 @@ -from pytest_httpx._httpx_compat import HTTPXBackend, httpx +from typing import TYPE_CHECKING + +from pytest_httpx._httpx_compat import HTTPXBackend from pytest_httpx._httpx_internals import _proxy_url from pytest_httpx._request_matcher import _RequestMatcher +if TYPE_CHECKING: + from pytest_httpx._httpx_compat import httpx + class RequestDescription: def __init__( self, backend: HTTPXBackend, - real_transport: httpx.BaseTransport | httpx.AsyncBaseTransport, - request: httpx.Request, + real_transport: "httpx.BaseTransport | httpx.AsyncBaseTransport", + request: "httpx.Request", matchers: list[_RequestMatcher], ): self.backend = backend diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index 3c4e9a7..5e49613 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -1,14 +1,17 @@ import json import re from collections.abc import Sequence -from re import Pattern -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import ANY -from pytest_httpx._httpx_compat import HTTPXBackend, httpx +from pytest_httpx import get_backends +from pytest_httpx._httpx_compat import HTTPXBackend from pytest_httpx._httpx_internals import PrimitiveData, _proxy_url from pytest_httpx._options import _HTTPXMockOptions +if TYPE_CHECKING: + from pytest_httpx._httpx_compat import httpx, httpx2 + def convert_back_mock_any(original: dict, new: dict) -> None: """ @@ -29,8 +32,8 @@ def convert_back_mock_any(original: dict, new: dict) -> None: def _url_match( backend: HTTPXBackend, - url_to_match: Pattern[str] | httpx.UR, - received: httpx.URL, + url_to_match: "re.Pattern[str] | httpx.URL", + received: "httpx.URL", params: dict[str, PrimitiveData | Sequence[PrimitiveData]] | None, ) -> bool: if isinstance(url_to_match, re.Pattern): @@ -38,7 +41,7 @@ def _url_match( # The expected URL may have been built with a different backend than the one # that received the request, so normalize it to the request's backend before - # comparing (URLs from different backends are never equal to each other). + # comparing. url_to_match = backend.httpx.URL(str(url_to_match)) # Compare query parameters apart as order of parameters should not matter @@ -56,7 +59,9 @@ def _url_match( return (received_params == expected_params) and (url == received_url) -def to_params_dict(params: httpx.QueryParams) -> dict[str, str | list[str]]: +def to_params_dict( + params: "httpx.QueryParams | httpx2.QueryParams", +) -> dict[str, str | list[str]]: """Convert query parameters to a dict where the value is a string if the parameter has a single value and a list of string otherwise.""" d = {} for key in params: @@ -69,9 +74,9 @@ class _RequestMatcher: def __init__( self, options: _HTTPXMockOptions, - url: str | Pattern[str] | httpx.URL | None = None, + url: "str | re.Pattern[str] | httpx.URL | None" = None, method: str | None = None, - proxy_url: str | Pattern[str] | httpx.URL | None = None, + proxy_url: "str | re.Pattern[str] | httpx.URL | None" = None, match_headers: dict[str, Any] | None = None, match_content: bytes | None = None, match_json: Any | None = None, @@ -84,7 +89,7 @@ def __init__( ): self._options = options self.nb_calls = 0 - self.url = httpx.URL(url) if url and isinstance(url, str) else url + self.url = url self.method = method.upper() if method else method self.headers = match_headers self.content = match_content @@ -92,11 +97,7 @@ def __init__( self.data = match_data self.files = match_files self.params = match_params - self.proxy_url = ( - httpx.URL(proxy_url) - if proxy_url and isinstance(proxy_url, str) - else proxy_url - ) + self.proxy_url = proxy_url self.extensions = match_extensions self.is_optional = ( not options.assert_all_responses_were_requested @@ -148,11 +149,14 @@ def _is_matching_body_more_than_one_way(self) -> bool: return sum(matching_ways) > 1 def _is_matching_params_more_than_one_way(self) -> bool: - url_has_params = ( - bool(self.url.params) - if (self.url and isinstance(self.url, httpx.URL)) - else False - ) + if not self.url or isinstance(self.url, re.Pattern): + return False + + [any_backend, *_] = get_backends() + # For now it doesn't matter which backend to use, both + # httpx and httpx2 has the same URL params parsing logic. + url_has_params = bool(any_backend.httpx.URL(str(self.url)).params) + matching_ways = [ self.params is not None, url_has_params, @@ -162,8 +166,8 @@ def _is_matching_params_more_than_one_way(self) -> bool: def match( self, backend: HTTPXBackend, - real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, - request: httpx.Request, + real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", + request: "httpx.Request", ) -> bool: return ( self._url_match(backend, request) @@ -174,19 +178,19 @@ def match( and self._extensions_match(request) ) - def _url_match(self, backend: HTTPXBackend, request: httpx.Request) -> bool: + def _url_match(self, backend: HTTPXBackend, request: "httpx.Request") -> bool: if not self.url: return True return _url_match(backend, self.url, request.url, self.params) - def _method_match(self, request: httpx.Request) -> bool: + def _method_match(self, request: "httpx.Request") -> bool: if not self.method: return True return request.method == self.method - def _headers_match(self, request: httpx.Request) -> bool: + def _headers_match(self, request: "httpx.Request") -> bool: if not self.headers: return True @@ -205,7 +209,7 @@ def _headers_match(self, request: httpx.Request) -> bool: for header_name, header_value in self.headers.items() ) - def _content_match(self, backend: HTTPXBackend, request: httpx.Request) -> bool: + def _content_match(self, backend: HTTPXBackend, request: "httpx.Request") -> bool: if self.content is not None: return request.content == self.content @@ -236,7 +240,7 @@ def _content_match(self, backend: HTTPXBackend, request: httpx.Request) -> bool: def _proxy_match( self, backend: HTTPXBackend, - real_transport: httpx.HTTPTransport | httpx.AsyncHTTPTransport, + real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", ) -> bool: if not self.proxy_url: return True @@ -246,7 +250,7 @@ def _proxy_match( return False - def _extensions_match(self, request: httpx.Request) -> bool: + def _extensions_match(self, request: "httpx.Request") -> bool: if not self.extensions: return True diff --git a/tests/test_backends.py b/tests/test_backends.py index a0174f6..9c4037c 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -138,7 +138,7 @@ def test_add_exception_raises_matching_backend_error(httpx_mock: HTTPXMock) -> N seen_request_modules: set[str] = set() -def _should_mock(request: httpx.Request | httpx2.Request) -> bool: # type: ignore[no-untyped-def] +def _should_mock(request: httpx.Request | httpx2.Request) -> bool: # Record the module of the request object so the test below could check them. match type(request): case httpx.Request: @@ -161,3 +161,21 @@ def test_should_mock_used_by_both_backends(httpx_mock: HTTPXMock) -> None: client.get("https://test_url") assert set(seen_request_modules) == {"httpx", "httpx2"} + + +def test_conflicting_params_backend_agnostic(httpx_mock: HTTPXMock) -> None: + with pytest.raises(ValueError) as exc_httpx: + httpx_mock.add_response( + url=httpx.URL("https://test_url?a=1"), match_params={"a": "1"} + ) + + with pytest.raises(ValueError) as exc_httpx2: + httpx_mock.add_response( + url=httpx2.URL("https://test_url?a=1"), match_params={"a": "1"} + ) + + assert ( + str(exc_httpx.value) + == str(exc_httpx2.value) + == "Provided URL must not contain any query parameter when match_params is used." + ) From 71f9887e91b3ede58b3729e850ebc216ccae77b3 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Tue, 13 Oct 2026 20:05:15 +0300 Subject: [PATCH 16/28] Update pyproject to include dev extra, update the contriburing guide --- CONTRIBUTING.md | 7 ++++--- pyproject.toml | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1192ab8..953c5d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,12 +28,13 @@ Title should be a small sentence describing the request. 1) Create a new branch based on `develop` branch. 2) Fetch all dependencies using [`pip`](https://pip.pypa.io/en/stable/). ```shell - python -m pip install .[testing] + python -m pip install .[dev] ``` -3) Ensure tests are ok by running them using [`pytest`](https://doc.pytest.org/en/latest/index.html). +3) Ensure tests are ok by running them using [`tox`](https://tox.wiki/en/4.65.4/index.html). ```shell - pytest + tox ``` + You can also run tests directly using [`pytest`](https://doc.pytest.org/en/latest/index.html). 4) Install [pre-commit](https://pre-commit.com) hook to ensure new code will follow guidelines. ```shell python -m pip install pre-commit diff --git a/pyproject.toml b/pyproject.toml index 6b0d31a..6369409 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,10 @@ testing = [ # Used to run async tests "pytest-asyncio==1.*", ] +dev = [ + "pytest-httpx[testing,httpx,httpx2]", + "tox==4.*" +] [project.entry-points.pytest11] pytest_httpx = "pytest_httpx" From 7368f00ac3ada85a07cb1575a6f111f22d0dbf9d Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Tue, 13 Oct 2026 20:07:24 +0300 Subject: [PATCH 17/28] Add a changlelog entry and update docs --- CHANGELOG.md | 8 +++++++- README.md | 20 ++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2285cf0..395874d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sending a non UTF8 body while having a `json` matcher registered will not raise a `UnicodeDecodeError` anymore. - `match_params` is now handling `bool`, `int` and `float` values in addition to `str`. Only str values were previously expected. - A meaningful error will now be returned when a callback that does not return an `httpx.Response` is called. + ### Added -- Support for httpx2 is implemented via a httpx/httpx2 compatibility module. +- Support for httpx2 is implemented. When both `httpx` and `httpx2` are installed, the `httpx_mock` + fixture now mocks both of them within the same test session. + +### Changed +- `httpx` is not installed automatically anymore. You now have to require it explicitly through the `httpx` or `httpx2` extra. + Both extras can be installed at the same time to mock both libraries simultaneously. ### Removed - python `3.10` is not supported anymore. diff --git a/README.md b/README.md index a9c429f..d485388 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,23 @@ > > However, current state can be considered as stable. -Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture will make sure every [`httpx`](https://www.python-httpx.org) request will be replied to with user provided responses ([unless some hosts are explicitly skipped](#do-not-mock-some-requests)). +Since `pytest-httpx` supports both `httpx` and `httpx2`, it doesn't install any of them by itself, +you have to pick which one (or both) you are using: + +```bash +# For httpx. +pip install pytest-httpx[httpx] +# For httpx2. +pip install pytest-httpx[httpx2] +# For both. +pip install pytest-httpx[httpx,httpx2] +``` + +Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture will make sure every [`httpx`](https://www.python-httpx.org) or [`httpx2`](https://pydantic.dev/docs/httpx2/get-started/) +request will be replied to with user provided responses ([unless some hosts are explicitly skipped](#do-not-mock-some-requests)). + +For brevity, both httpx and httpx2 will be further referred to as httpx. All the examples using httpx +should stay intact when using httpx2. - [Add responses](#add-responses) - [JSON body](#add-json-response) @@ -36,7 +52,7 @@ Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixt ## Add responses -You can register responses for both sync and async [`HTTPX`](https://www.python-httpx.org) requests. +You can register responses for both sync and async HTTPX requests. ```python import pytest From 11aabfd7c9195d6bc5627f7664fe20196bffd19f Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Tue, 13 Oct 2026 20:08:22 +0300 Subject: [PATCH 18/28] Use tox in CI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf73ffd..a80afbb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: pre-commit run --all-files - name: Test run: | - pytest --cov-report=xml --junitxml=junit.xml -o junit_family=legacy + tox -- --cov-report=xml --junitxml=junit.xml -o junit_family=legacy - name: Upload test results to Codecov uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: From e81ece1dd4d46b6da4395c0365bb3d96c1164771 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Tue, 13 Oct 2026 21:42:51 +0300 Subject: [PATCH 19/28] Revert using httpx2 in type hints --- pytest_httpx/_httpx_internals.py | 6 +++--- pytest_httpx/_httpx_mock.py | 25 +++++++++++-------------- pytest_httpx/_request_matcher.py | 4 ++-- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index 8b80ff8..d72b58b 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -5,7 +5,7 @@ from pytest_httpx._httpx_compat import HTTPXBackend, get_backends if TYPE_CHECKING: - from pytest_httpx._httpx_compat import HttpxURL, httpcore, httpcore2, httpx + from pytest_httpx._httpx_compat import httpcore, httpx # Those types are internally defined within httpx._types HeaderTypes: TypeAlias = Union[ @@ -42,9 +42,9 @@ async def __aiter__(self) -> AsyncIterator[bytes]: def _to_httpx_url( backend: HTTPXBackend, - url: "httpcore.URL | httpcore2.URL", + url: "httpcore.URL", headers: list[tuple[bytes, bytes]], -) -> "HttpxURL": +) -> "httpx.URL": for name, value in headers: if b"Proxy-Authorization" == name: return backend.httpx.URL( diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 1dd121d..dfbacc1 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -167,7 +167,7 @@ def _handle_request( request.read() self._requests.append((real_transport, request)) - callback = self._get_callback(backend, real_transport, request) + callback = self._get_callback(real_transport, request) if callback: response = callback(request) @@ -175,17 +175,15 @@ def _handle_request( return _unread(response) raise self._request_not_matched( - backend, request, self._explain_that_callback_must_return_a_response( - backend, real_transport, request + real_transport, request ), ) raise self._request_not_matched( - backend, request, - self._explain_that_no_response_was_found(backend, real_transport, request), + self._explain_that_no_response_was_found(real_transport, request), ) async def _handle_async_request( @@ -202,7 +200,7 @@ async def _handle_async_request( await request.aread() self._requests.append((real_transport, request)) - callback = self._get_callback(backend, real_transport, request) + callback = self._get_callback(real_transport, request) if callback: response = callback(request) @@ -213,40 +211,39 @@ async def _handle_async_request( return _unread(response) raise self._request_not_matched( - backend, request, self._explain_that_callback_must_return_a_response( - backend, real_transport, request + real_transport, request ), ) raise self._request_not_matched( - backend, request, - self._explain_that_no_response_was_found(backend, real_transport, request), + self._explain_that_no_response_was_found(real_transport, request), ) def _request_not_matched( - self, backend: HTTPXBackend, request: "httpx.Request", message: str + self, request: "httpx.Request", message: str ) -> "httpx.TimeoutException": + backend = _get_backend(request) self._requests_not_matched.append(request) return backend.httpx.TimeoutException(message, request=request) def _explain_that_callback_must_return_a_response( self, - backend: HTTPXBackend, real_transport: "httpx.BaseTransport | httpx.AsyncBaseTransport", request: "httpx.Request", ) -> str: + backend = _get_backend(request) return f"Callback registered for {RequestDescription(backend, real_transport, request, [])} MUST return {backend.name}.Response" def _explain_that_no_response_was_found( self, - backend: HTTPXBackend, real_transport: "httpx.BaseTransport | httpx.AsyncBaseTransport", request: "httpx.Request", ) -> str: matchers = [matcher for matcher, _ in self._callbacks] + backend = _get_backend(request) message = f"No response can be found for {RequestDescription(backend, real_transport, request, matchers)}" @@ -272,10 +269,10 @@ def _explain_that_no_response_was_found( def _get_callback( self, - backend: HTTPXBackend, real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", request: "httpx.Request", ) -> Callback | None: + backend = _get_backend(request) callbacks = [ (matcher, callback) for matcher, callback in self._callbacks diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index 5e49613..ccac9fb 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -10,7 +10,7 @@ from pytest_httpx._options import _HTTPXMockOptions if TYPE_CHECKING: - from pytest_httpx._httpx_compat import httpx, httpx2 + from pytest_httpx._httpx_compat import httpx def convert_back_mock_any(original: dict, new: dict) -> None: @@ -60,7 +60,7 @@ def _url_match( def to_params_dict( - params: "httpx.QueryParams | httpx2.QueryParams", + params: "httpx.QueryParams", ) -> dict[str, str | list[str]]: """Convert query parameters to a dict where the value is a string if the parameter has a single value and a list of string otherwise.""" d = {} From 3069009568051debe6f69f2f0a5e601d20cb0c08 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Wed, 14 Oct 2026 20:03:11 +0300 Subject: [PATCH 20/28] Update tox config --- tox.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.toml b/tox.toml index a36073d..48640cd 100644 --- a/tox.toml +++ b/tox.toml @@ -1,5 +1,4 @@ env_list = ["clean", "httpx", "httpx2", "both"] -isolated_build = true [env_run_base] # Editable mode so that coverage measures the repo source. From 7e11b1fa9db3fe7a2a48807439ef7d00e8e625b2 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Thu, 15 Oct 2026 00:01:17 +0300 Subject: [PATCH 21/28] Remove stale code --- pytest_httpx/_httpx_compat.py | 11 ++--------- pytest_httpx/_httpx_mock.py | 6 ++---- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/pytest_httpx/_httpx_compat.py b/pytest_httpx/_httpx_compat.py index 1d43011..9ddb0c5 100644 --- a/pytest_httpx/_httpx_compat.py +++ b/pytest_httpx/_httpx_compat.py @@ -4,10 +4,6 @@ from typing import TYPE_CHECKING, Literal -class PytestHTTPXDeprecationWarning(UserWarning): - pass - - @dataclass(frozen=True) class HTTPXBackend: name: Literal["httpx", "httpx2"] @@ -51,8 +47,5 @@ def get_backends() -> list[HTTPXBackend]: if TYPE_CHECKING: import httpcore # noqa: F401 - import httpcore2 # noqa: F401 - import httpx - import httpx2 - - HttpxURL = httpx.URL | httpx2.URL + import httpx # noqa: F401 + import httpx2 # noqa: F401 diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index dfbacc1..8dcdd61 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -182,8 +182,7 @@ def _handle_request( ) raise self._request_not_matched( - request, - self._explain_that_no_response_was_found(real_transport, request), + request, self._explain_that_no_response_was_found(real_transport, request) ) async def _handle_async_request( @@ -218,8 +217,7 @@ async def _handle_async_request( ) raise self._request_not_matched( - request, - self._explain_that_no_response_was_found(real_transport, request), + request, self._explain_that_no_response_was_found(real_transport, request) ) def _request_not_matched( From c9cf28272981ced837192bb8df2d70294c41cd38 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Thu, 15 Oct 2026 00:19:48 +0300 Subject: [PATCH 22/28] Consistent type hints of transports --- pytest_httpx/_httpx_mock.py | 4 ++-- pytest_httpx/_pretty_print.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 8dcdd61..0bc682e 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -229,7 +229,7 @@ def _request_not_matched( def _explain_that_callback_must_return_a_response( self, - real_transport: "httpx.BaseTransport | httpx.AsyncBaseTransport", + real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", request: "httpx.Request", ) -> str: backend = _get_backend(request) @@ -237,7 +237,7 @@ def _explain_that_callback_must_return_a_response( def _explain_that_no_response_was_found( self, - real_transport: "httpx.BaseTransport | httpx.AsyncBaseTransport", + real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", request: "httpx.Request", ) -> str: matchers = [matcher for matcher, _ in self._callbacks] diff --git a/pytest_httpx/_pretty_print.py b/pytest_httpx/_pretty_print.py index 98a6e46..fa83ce4 100644 --- a/pytest_httpx/_pretty_print.py +++ b/pytest_httpx/_pretty_print.py @@ -12,7 +12,7 @@ class RequestDescription: def __init__( self, backend: HTTPXBackend, - real_transport: "httpx.BaseTransport | httpx.AsyncBaseTransport", + real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", request: "httpx.Request", matchers: list[_RequestMatcher], ): From e35d301279df62a701f536803eb877f0f55b3d5a Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Thu, 15 Oct 2026 00:27:57 +0300 Subject: [PATCH 23/28] Move get_backend/set_backend helpers to the compat module --- pytest_httpx/_httpx_compat.py | 16 +++++++++++++- pytest_httpx/_httpx_mock.py | 38 +++++++++----------------------- pytest_httpx/_pretty_print.py | 5 ++--- pytest_httpx/_request_matcher.py | 4 ++-- 4 files changed, 30 insertions(+), 33 deletions(-) diff --git a/pytest_httpx/_httpx_compat.py b/pytest_httpx/_httpx_compat.py index 9ddb0c5..7730b91 100644 --- a/pytest_httpx/_httpx_compat.py +++ b/pytest_httpx/_httpx_compat.py @@ -45,7 +45,21 @@ def get_backends() -> list[HTTPXBackend]: return backends +_BACKEND_ATTRIBUTE = "__pytest_httpx_backend" + + +def set_backend(request: "httpx.Request", backend: HTTPXBackend) -> None: + # Store the intercepting backend on the request so that backend-agnostic + # callbacks (e.g. the one built by `add_response`) can build a response + # with the matching library. + setattr(request, _BACKEND_ATTRIBUTE, backend) + + +def get_backend(request: "httpx.Request") -> HTTPXBackend: + return getattr(request, _BACKEND_ATTRIBUTE) + + if TYPE_CHECKING: import httpcore # noqa: F401 - import httpx # noqa: F401 + import httpx import httpx2 # noqa: F401 diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index 0bc682e..a8639d5 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, TypeAlias from pytest_httpx import _httpx_internals -from pytest_httpx._httpx_compat import HTTPXBackend +from pytest_httpx._httpx_compat import HTTPXBackend, get_backend, set_backend from pytest_httpx._options import _HTTPXMockOptions from pytest_httpx._pretty_print import RequestDescription from pytest_httpx._request_matcher import _RequestMatcher @@ -82,7 +82,7 @@ def add_response( def response_callback(request: "httpx.Request") -> "httpx.Response": # Build the response with the backend that intercepted the request # so that its type matches the client. - backend = _get_backend(request) + backend = get_backend(request) return backend.httpx.Response( status_code=status_code, extensions={"http_version": http_version.encode("ascii")}, @@ -145,7 +145,7 @@ def add_exception(self, exception: BaseException, **matchers: Any) -> None: """ def exception_callback(request: "httpx.Request") -> None: - backend = _get_backend(request) + backend = get_backend(request) if isinstance(exception, backend.httpx.RequestError): exception.request = request @@ -161,7 +161,7 @@ def _handle_request( ) -> "httpx.Response": # Save the backend that intercepted this request so that callbacks # use the matching library. - _set_backend(request, backend) + set_backend(request, backend) # Store the content in request for future matching request.read() @@ -193,7 +193,7 @@ async def _handle_async_request( ) -> "httpx.Response": # Save the backend that intercepted this request so that callbacks # use the matching library. - _set_backend(request, backend) + set_backend(request, backend) # Store the content in request for future matching await request.aread() @@ -223,7 +223,7 @@ async def _handle_async_request( def _request_not_matched( self, request: "httpx.Request", message: str ) -> "httpx.TimeoutException": - backend = _get_backend(request) + backend = get_backend(request) self._requests_not_matched.append(request) return backend.httpx.TimeoutException(message, request=request) @@ -232,8 +232,8 @@ def _explain_that_callback_must_return_a_response( real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", request: "httpx.Request", ) -> str: - backend = _get_backend(request) - return f"Callback registered for {RequestDescription(backend, real_transport, request, [])} MUST return {backend.name}.Response" + backend = get_backend(request) + return f"Callback registered for {RequestDescription(real_transport, request, [])} MUST return {backend.name}.Response" def _explain_that_no_response_was_found( self, @@ -241,9 +241,8 @@ def _explain_that_no_response_was_found( request: "httpx.Request", ) -> str: matchers = [matcher for matcher, _ in self._callbacks] - backend = _get_backend(request) - message = f"No response can be found for {RequestDescription(backend, real_transport, request, matchers)}" + message = f"No response can be found for {RequestDescription(real_transport, request, matchers)}" already_matched = [] unmatched = [] @@ -270,11 +269,10 @@ def _get_callback( real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", request: "httpx.Request", ) -> Callback | None: - backend = _get_backend(request) callbacks = [ (matcher, callback) for matcher, callback in self._callbacks - if matcher.match(backend, real_transport, request) + if matcher.match(real_transport, request) ] # No callback match this request @@ -317,7 +315,7 @@ def get_requests(self, **matchers: Any) -> list["httpx.Request"]: return [ request for real_transport, request in self._requests - if matcher.match(_get_backend(request), real_transport, request) + if matcher.match(real_transport, request) ] def get_request(self, **matchers: Any) -> "httpx.Request | None": @@ -379,20 +377,6 @@ def _assert_options(self) -> None: ) -_BACKEND_ATTRIBUTE = "__pytest_httpx_backend" - - -def _set_backend(request: "httpx.Request", backend: HTTPXBackend) -> None: - # Store the intercepting backend on the request so that backend-agnostic - # callbacks (e.g. the one built by `add_response`) can build a response - # with the matching library. - setattr(request, _BACKEND_ATTRIBUTE, backend) - - -def _get_backend(request: "httpx.Request") -> HTTPXBackend: - return getattr(request, _BACKEND_ATTRIBUTE) - - def _unread(response: "httpx.Response") -> "httpx.Response": # Allow to read the response on client side response.is_stream_consumed = False diff --git a/pytest_httpx/_pretty_print.py b/pytest_httpx/_pretty_print.py index fa83ce4..c1b0c6e 100644 --- a/pytest_httpx/_pretty_print.py +++ b/pytest_httpx/_pretty_print.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from pytest_httpx._httpx_compat import HTTPXBackend +from pytest_httpx._httpx_compat import get_backend from pytest_httpx._httpx_internals import _proxy_url from pytest_httpx._request_matcher import _RequestMatcher @@ -11,12 +11,11 @@ class RequestDescription: def __init__( self, - backend: HTTPXBackend, real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", request: "httpx.Request", matchers: list[_RequestMatcher], ): - self.backend = backend + self.backend = get_backend(request) self.real_transport = real_transport self.request = request diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index ccac9fb..36ad71d 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -5,7 +5,7 @@ from unittest.mock import ANY from pytest_httpx import get_backends -from pytest_httpx._httpx_compat import HTTPXBackend +from pytest_httpx._httpx_compat import HTTPXBackend, get_backend from pytest_httpx._httpx_internals import PrimitiveData, _proxy_url from pytest_httpx._options import _HTTPXMockOptions @@ -165,10 +165,10 @@ def _is_matching_params_more_than_one_way(self) -> bool: def match( self, - backend: HTTPXBackend, real_transport: "httpx.HTTPTransport | httpx.AsyncHTTPTransport", request: "httpx.Request", ) -> bool: + backend = get_backend(request) return ( self._url_match(backend, request) and self._method_match(request) From 9c1ea190b46d94eaaf38b5f0230bf746aa16da81 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Thu, 15 Oct 2026 00:34:47 +0300 Subject: [PATCH 24/28] Improve comment --- pytest_httpx/_request_matcher.py | 2 +- tests/test_backends.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index 36ad71d..f9138af 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -153,7 +153,7 @@ def _is_matching_params_more_than_one_way(self) -> bool: return False [any_backend, *_] = get_backends() - # For now it doesn't matter which backend to use, both + # It doesn't matter which backend to use, both # httpx and httpx2 has the same URL params parsing logic. url_has_params = bool(any_backend.httpx.URL(str(self.url)).params) diff --git a/tests/test_backends.py b/tests/test_backends.py index 9c4037c..4364b7a 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -64,7 +64,6 @@ def test_response_type_matches_backend(httpx_mock: HTTPXMock) -> None: with httpx2.Client() as client: httpx2_response = client.get("https://test_url") - # Each response must be an instance of its own backend's Response type. assert isinstance(httpx_response, httpx.Response) assert not isinstance(httpx_response, httpx2.Response) From fb3d87efd4391be50ae4950ac12910c5f665ef25 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Thu, 15 Oct 2026 00:40:34 +0300 Subject: [PATCH 25/28] Revert unnecessary change in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d485388..95ef70a 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ should stay intact when using httpx2. ## Add responses -You can register responses for both sync and async HTTPX requests. +You can register responses for both sync and async ([`HTTPX`](https://www.python-httpx.org)) requests. ```python import pytest From 99c0b1f63ce963e1875c8bdba006d246cb3be530 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Thu, 15 Oct 2026 01:43:00 +0300 Subject: [PATCH 26/28] More pragma: no cover on httpx imports --- pytest_httpx/_httpx_compat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_httpx/_httpx_compat.py b/pytest_httpx/_httpx_compat.py index 7730b91..bb0038f 100644 --- a/pytest_httpx/_httpx_compat.py +++ b/pytest_httpx/_httpx_compat.py @@ -19,7 +19,7 @@ def get_backends() -> list[HTTPXBackend]: try: import httpcore2 as httpcore2_ # pragma: no cover - import httpx2 as httpx2_ + import httpx2 as httpx2_ # pragma: no cover except ModuleNotFoundError: # pragma: no cover pass else: @@ -27,7 +27,7 @@ def get_backends() -> list[HTTPXBackend]: try: import httpcore as httpcore_ # pragma: no cover - import httpx as httpx_ + import httpx as httpx_ # pragma: no cover except ModuleNotFoundError: # pragma: no cover pass else: From 7e9d2dea5b6e50664deaf87bbc617d2192f5b382 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Mon, 19 Oct 2026 19:14:13 +0300 Subject: [PATCH 27/28] Update the public API to use both httpx and httpx2 type hints --- pytest_httpx/_httpx_internals.py | 3 ++- pytest_httpx/_httpx_mock.py | 10 +++++----- pytest_httpx/_options.py | 6 ++++-- pytest_httpx/_request_matcher.py | 6 +++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py index d72b58b..c0c953a 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -5,11 +5,12 @@ from pytest_httpx._httpx_compat import HTTPXBackend, get_backends if TYPE_CHECKING: - from pytest_httpx._httpx_compat import httpcore, httpx + from pytest_httpx._httpx_compat import httpcore, httpx, httpx2 # Those types are internally defined within httpx._types HeaderTypes: TypeAlias = Union[ "httpx.Headers", + "httpx2.Headers", dict[str, str], dict[bytes, bytes], Sequence[tuple[str, str]], diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index a8639d5..a14e026 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -10,12 +10,12 @@ from pytest_httpx._request_matcher import _RequestMatcher if TYPE_CHECKING: - from pytest_httpx._httpx_compat import httpx + from pytest_httpx._httpx_compat import httpx, httpx2 Callback: TypeAlias = Callable[ - ["httpx.Request"], - "httpx.Response | None | Awaitable[httpx.Response | None]", + ["httpx.Request | httpx2.Request"], + "httpx.Response | httpx2.Response | None | Awaitable[httpx.Response | httpx2.Response | None]", ] @@ -294,7 +294,7 @@ def _get_callback( # All callbacks have already been matched and last registered cannot be reused return None - def get_requests(self, **matchers: Any) -> list["httpx.Request"]: + def get_requests(self, **matchers: Any) -> list["httpx.Request | httpx2.Request"]: """ Return all requests sent that match (empty list if no requests were matched). @@ -318,7 +318,7 @@ def get_requests(self, **matchers: Any) -> list["httpx.Request"]: if matcher.match(real_transport, request) ] - def get_request(self, **matchers: Any) -> "httpx.Request | None": + def get_request(self, **matchers: Any) -> "httpx.Request | httpx2.Request | None": """ Return the single request that match (or None). diff --git a/pytest_httpx/_options.py b/pytest_httpx/_options.py index 5c9131b..b623cba 100644 --- a/pytest_httpx/_options.py +++ b/pytest_httpx/_options.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from pytest_httpx._httpx_compat import httpx + from pytest_httpx._httpx_compat import httpx, httpx2 class _HTTPXMockOptions: @@ -12,7 +12,9 @@ def __init__( assert_all_responses_were_requested: bool = True, assert_all_requests_were_expected: bool = True, can_send_already_matched_responses: bool = False, - should_mock: Callable[["httpx.Request"], bool] = lambda request: True, + should_mock: Callable[ + ["httpx.Request | httpx2.Request"], bool + ] = lambda request: True, ) -> None: self.assert_all_responses_were_requested = assert_all_responses_were_requested self.assert_all_requests_were_expected = assert_all_requests_were_expected diff --git a/pytest_httpx/_request_matcher.py b/pytest_httpx/_request_matcher.py index f9138af..762702a 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -10,7 +10,7 @@ from pytest_httpx._options import _HTTPXMockOptions if TYPE_CHECKING: - from pytest_httpx._httpx_compat import httpx + from pytest_httpx._httpx_compat import httpx, httpx2 def convert_back_mock_any(original: dict, new: dict) -> None: @@ -74,9 +74,9 @@ class _RequestMatcher: def __init__( self, options: _HTTPXMockOptions, - url: "str | re.Pattern[str] | httpx.URL | None" = None, + url: "str | re.Pattern[str] | httpx.URL | httpx2.URL | None" = None, method: str | None = None, - proxy_url: "str | re.Pattern[str] | httpx.URL | None" = None, + proxy_url: "str | re.Pattern[str] | httpx.URL | httpx2.URL | None" = None, match_headers: dict[str, Any] | None = None, match_content: bytes | None = None, match_json: Any | None = None, From 6b7579f738f96eb67491ff8c9f3ca0196c7d6b31 Mon Sep 17 00:00:00 2001 From: Roman Vlasenko Date: Mon, 19 Oct 2026 19:27:13 +0300 Subject: [PATCH 28/28] Fix Callable type hint --- pytest_httpx/_httpx_mock.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py index a14e026..58c58c1 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -13,10 +13,16 @@ from pytest_httpx._httpx_compat import httpx, httpx2 -Callback: TypeAlias = Callable[ - ["httpx.Request | httpx2.Request"], - "httpx.Response | httpx2.Response | None | Awaitable[httpx.Response | httpx2.Response | None]", -] +Callback: TypeAlias = ( + Callable[ + ["httpx.Request"], + "httpx.Response | None | Awaitable[httpx.Response | None]", + ] + | Callable[ + ["httpx2.Request"], + "httpx2.Response | None | Awaitable[httpx2.Response | None]", + ] +) class HTTPXMock: