Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Support mocking `httpx2` (and `httpcore2`). `httpx` is used when installed, otherwise
`httpx2`; set `RESPX_HTTPX_PACKAGE=httpx2` to force it. Install with `respx[httpx2]`.

## [0.23.1] - 2026-04-08

### Fixed
Expand Down
39 changes: 39 additions & 0 deletions respx/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Select the HTTPX flavour (`httpx` or its fork `httpx2`) that RESPX mocks.

`httpx` is preferred when installed. `httpx2` (and its `httpcore2` dependency)
is used when `httpx` is missing, or when explicitly requested through the
`RESPX_HTTPX_PACKAGE=httpx2` environment variable.
"""

import importlib
import os
from types import ModuleType
from typing import Tuple

_PACKAGES = {"httpx": "httpcore", "httpx2": "httpcore2"}


def _select() -> Tuple[str, str]:
requested = os.environ.get("RESPX_HTTPX_PACKAGE")
if requested:
if requested not in _PACKAGES:
raise ImportError(
f"RESPX_HTTPX_PACKAGE must be one of {sorted(_PACKAGES)}, "
f"got {requested!r}"
)
return requested, _PACKAGES[requested]

for name, core in _PACKAGES.items():
try:
importlib.import_module(name)
except ImportError:
continue
return name, core

raise ImportError("RESPX requires either `httpx` or `httpx2` to be installed")


httpx_name, httpcore_name = _select()
httpx: ModuleType = importlib.import_module(httpx_name)
httpcore: ModuleType = importlib.import_module(httpcore_name)
2 changes: 1 addition & 1 deletion respx/handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Callable

import httpx
from respx._compat import httpx


class TransportHandler:
Expand Down
21 changes: 10 additions & 11 deletions respx/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from typing import TYPE_CHECKING, ClassVar, Dict, List, Type
from unittest import mock

import httpcore
import httpx
from respx._compat import httpcore, httpcore_name, httpx, httpx_name

from respx.patterns import parse_url

Expand Down Expand Up @@ -142,10 +141,10 @@ def mock(cls, spec):


class HTTPXMocker(Mocker):
name = "httpx"
name = "httpx" # registry key, regardless of the selected flavour
targets = [
"httpx._client.Client",
"httpx._client.AsyncClient",
f"{httpx_name}._client.Client",
f"{httpx_name}._client.AsyncClient",
]
target_methods = ["_transport_for_url"]

Expand Down Expand Up @@ -262,12 +261,12 @@ async def from_async_httpx_response(cls, httpx_response, target, **kwargs):
class HTTPCoreMocker(AbstractRequestMocker):
name = "httpcore"
targets = [
"httpcore._sync.connection.HTTPConnection",
"httpcore._sync.connection_pool.ConnectionPool",
"httpcore._sync.http_proxy.HTTPProxy",
"httpcore._async.connection.AsyncHTTPConnection",
"httpcore._async.connection_pool.AsyncConnectionPool",
"httpcore._async.http_proxy.AsyncHTTPProxy",
f"{httpcore_name}._sync.connection.HTTPConnection",
f"{httpcore_name}._sync.connection_pool.ConnectionPool",
f"{httpcore_name}._sync.http_proxy.HTTPProxy",
f"{httpcore_name}._async.connection.AsyncHTTPConnection",
f"{httpcore_name}._async.connection_pool.AsyncConnectionPool",
f"{httpcore_name}._async.http_proxy.AsyncHTTPProxy",
]
target_methods = ["handle_request", "handle_async_request"]

Expand Down
2 changes: 1 addition & 1 deletion respx/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from unittest import mock
from warnings import warn

import httpx
from respx._compat import httpx

from respx.utils import SetCookie

Expand Down
2 changes: 1 addition & 1 deletion respx/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from unittest.mock import ANY

import httpx
from respx._compat import httpx

from respx.utils import MultiItems, decode_data

Expand Down
2 changes: 1 addition & 1 deletion respx/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
overload,
)

import httpx
from respx._compat import httpx

from .mocks import Mocker
from .models import (
Expand Down
6 changes: 4 additions & 2 deletions respx/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
)
from warnings import warn

import httpx
from httpx import AsyncBaseTransport, BaseTransport
from respx._compat import httpx

from .models import PassThrough

AsyncBaseTransport = httpx.AsyncBaseTransport
BaseTransport = httpx.BaseTransport

if TYPE_CHECKING:
from .router import Router # pragma: nocover

Expand Down
2 changes: 1 addition & 1 deletion respx/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
Union,
)

import httpx
from respx._compat import httpx

URL = Tuple[
bytes, # scheme
Expand Down
2 changes: 1 addition & 1 deletion respx/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
)
from urllib.parse import parse_qsl

import httpx
from respx._compat import httpx


class MultiItems(defaultdict):
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@
zip_safe=False,
python_requires=">=3.8",
install_requires=["httpx>=0.25.0"],
extras_require={"httpx2": ["httpx2>=2.0.0"]},
)
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import httpx
from respx._compat import httpx
import pytest

import respx
Expand Down
4 changes: 2 additions & 2 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import socket
from unittest import mock

import httpcore
import httpx
from respx._compat import httpcore
from respx._compat import httpx
import pytest

import respx
Expand Down
41 changes: 41 additions & 0 deletions tests/test_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import importlib
import sys

import pytest

import respx._compat as compat


@pytest.fixture(autouse=True)
def restore_compat():
yield
importlib.reload(compat)


def test_invalid_package_env(monkeypatch):
monkeypatch.setenv("RESPX_HTTPX_PACKAGE", "requests")
with pytest.raises(ImportError, match="RESPX_HTTPX_PACKAGE"):
importlib.reload(compat)


@pytest.mark.parametrize("package", ["httpx", "httpx2"])
def test_package_env(monkeypatch, package):
monkeypatch.setenv("RESPX_HTTPX_PACKAGE", package)
importlib.reload(compat)
assert compat.httpx.__name__ == package


def test_fallback_to_httpx2(monkeypatch):
monkeypatch.delenv("RESPX_HTTPX_PACKAGE", raising=False)
monkeypatch.setitem(sys.modules, "httpx", None)
importlib.reload(compat)
assert compat.httpx_name == "httpx2"
assert compat.httpcore_name == "httpcore2"


def test_none_installed(monkeypatch):
monkeypatch.delenv("RESPX_HTTPX_PACKAGE", raising=False)
monkeypatch.setitem(sys.modules, "httpx", None)
monkeypatch.setitem(sys.modules, "httpx2", None)
with pytest.raises(ImportError, match="httpx2"):
importlib.reload(compat)
14 changes: 7 additions & 7 deletions tests/test_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from contextlib import ExitStack as does_not_raise
from unittest import mock

import httpcore
import httpx
from respx._compat import httpcore
from respx._compat import httpx
import pytest

import respx
Expand Down Expand Up @@ -452,25 +452,25 @@ async def test_assert_all_mocked(client, assert_all_mocked, raises):
def test_add_remove_targets():
from respx.mocks import HTTPCoreMocker

target = "httpcore._sync.connection.HTTPConnection"
target = f"{httpcore.__name__}._sync.connection.HTTPConnection"
assert HTTPCoreMocker.targets.count(target) == 1
HTTPCoreMocker.add_targets(target)
assert HTTPCoreMocker.targets.count(target) == 1

pre_add_count = len(HTTPCoreMocker.targets)
try:
HTTPCoreMocker.add_targets(
"httpx._transports.asgi.ASGITransport",
"httpx._transports.wsgi.WSGITransport",
f"{httpx.__name__}._transports.asgi.ASGITransport",
f"{httpx.__name__}._transports.wsgi.WSGITransport",
)
assert len(HTTPCoreMocker.targets) == pre_add_count + 2

HTTPCoreMocker.remove_targets("foobar")
assert len(HTTPCoreMocker.targets) == pre_add_count + 2
finally:
HTTPCoreMocker.remove_targets(
"httpx._transports.asgi.ASGITransport",
"httpx._transports.wsgi.WSGITransport",
f"{httpx.__name__}._transports.asgi.ASGITransport",
f"{httpx.__name__}._transports.wsgi.WSGITransport",
)
assert len(HTTPCoreMocker.targets) == pre_add_count

Expand Down
2 changes: 1 addition & 1 deletion tests/test_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
from unittest.mock import ANY

import httpx
from respx._compat import httpx
import pytest

from respx.patterns import (
Expand Down
2 changes: 1 addition & 1 deletion tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
def test_respx_mock_fixture(testdir):
testdir.makepyfile(
"""
import httpx
from respx._compat import httpx
import pytest

@pytest.fixture
Expand Down
4 changes: 2 additions & 2 deletions tests/test_router.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import warnings

import httpcore
import httpx
from respx._compat import httpcore
from respx._compat import httpx
import pytest

from respx import Route, Router
Expand Down
2 changes: 1 addition & 1 deletion tests/test_stats.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re

import httpx
from respx._compat import httpx
import pytest

import respx
Expand Down
2 changes: 1 addition & 1 deletion tests/test_transports.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import warnings

import httpx
from respx._compat import httpx
import pytest

from respx.models import PassThrough
Expand Down