diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f2ee3f..6d13e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `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. ## [0.36.2] - 2026-04-09 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index dd1aa14..780d3ed 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 6ed9be5..34bd6c1 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_mock import HTTPXMock from pytest_httpx._httpx_internals import IteratorStream from pytest_httpx._options import _HTTPXMockOptions diff --git a/pytest_httpx/_compat.py b/pytest_httpx/_compat.py new file mode 100644 index 0000000..dacafab --- /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 httpx2 as httpx + import httpcore2 as httpcore +else: + try: + import httpx2 as httpx + import httpcore2 as httpcore + except ModuleNotFoundError: + try: + import httpx # noqa: F401 + import httpcore # 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 cc70c1c..2b0c2af 100644 --- a/pytest_httpx/_httpx_internals.py +++ b/pytest_httpx/_httpx_internals.py @@ -2,11 +2,7 @@ from typing import Union, Optional from collections.abc import Sequence, Iterable, AsyncIterator, Iterator -import httpcore -import httpx - -# TODO Get rid of this internal import -from httpx._content import IteratorByteStream, AsyncIteratorByteStream +from pytest_httpx._compat import httpx, httpcore # Those types are internally defined within httpx._types HeaderTypes = Union[ @@ -19,7 +15,11 @@ PrimitiveData = Optional[Union[str, int, float, bool]] -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 07d570d..2ef315f 100644 --- a/pytest_httpx/_httpx_mock.py +++ b/pytest_httpx/_httpx_mock.py @@ -3,7 +3,7 @@ from typing import Union, Optional, Callable, Any from collections.abc import Awaitable -import httpx +from pytest_httpx._compat import httpx from pytest_httpx import _httpx_internals from pytest_httpx._options import _HTTPXMockOptions diff --git a/pytest_httpx/_options.py b/pytest_httpx/_options.py index d5969ab..e31ab82 100644 --- a/pytest_httpx/_options.py +++ b/pytest_httpx/_options.py @@ -1,6 +1,6 @@ from typing 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 58a880e..089535a 100644 --- a/pytest_httpx/_pretty_print.py +++ b/pytest_httpx/_pretty_print.py @@ -1,6 +1,6 @@ from typing import Union -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 a1c1a0e..4f8e147 100644 --- a/pytest_httpx/_request_matcher.py +++ b/pytest_httpx/_request_matcher.py @@ -4,8 +4,7 @@ from re import Pattern from unittest.mock import ANY -import httpx -from httpx import QueryParams +from pytest_httpx._compat import httpx from pytest_httpx._httpx_internals import _proxy_url, PrimitiveData from pytest_httpx._options import _HTTPXMockOptions @@ -39,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) @@ -51,7 +50,7 @@ def _url_match( return (received_params == expected_params) and (url == received_url) -def to_params_dict(params: QueryParams) -> dict[str, Union[str | list[str]]]: +def to_params_dict(params: httpx.QueryParams) -> dict[str, Union[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: