From 77fc926ed10b4043bbdfdbeb00e9f2f244a5d920 Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 13 Nov 2023 19:34:58 +0100
Subject: [PATCH 1/4] Reproduce the fact that custom transport is not used
---
tests/test_httpx_async.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index e9ad034..584d5c4 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -5,6 +5,7 @@
import httpx
import pytest
+from httpx import Request, Response
from pytest import Testdir
from unittest.mock import ANY
@@ -2015,3 +2016,26 @@ async def test_streams_are_not_cascading_resulting_in_maximum_recursion(
tasks = [client.get("https://example.com/") 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):
+ def __init__(self, prefix: str, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.prefix = prefix
+
+ async def handle_async_request(
+ self,
+ request: Request,
+ ) -> 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:
+ response = await client.post("https://test_url", content=b"This is the body")
+ assert response.read() == b""
+ assert response.headers["x-prefix"] == "test"
From 7ac04bc8fbc2edb69a20032912d528bbcd6ccdac Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 13 Nov 2023 20:12:07 +0100
Subject: [PATCH 2/4] Handle custom transport
---
CHANGELOG.md | 5 ++++
pytest_httpx/__init__.py | 44 ++++++++++++++++++++------------
pytest_httpx/_httpx_internals.py | 13 ++++------
pytest_httpx/_httpx_mock.py | 26 +++----------------
pytest_httpx/_request_matcher.py | 4 +--
tests/test_httpx_async.py | 5 ++--
tests/test_httpx_sync.py | 23 ++++++++++++++++-
7 files changed, 67 insertions(+), 53 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf57b10..b2088ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Fixed
+- Custom HTTP transport are now handled (parent call to `handle_async_request` or `handle_request`).
+
+### Changed
+- Only HTTP transport are now mocked, this should not have any impact, however if it does, please feel free to open an issue describing your use case.
## [0.26.0] - 2023-09-18
### Added
diff --git a/pytest_httpx/__init__.py b/pytest_httpx/__init__.py
index b579fa9..3fe8071 100644
--- a/pytest_httpx/__init__.py
+++ b/pytest_httpx/__init__.py
@@ -5,11 +5,7 @@
import pytest
from pytest import MonkeyPatch
-from pytest_httpx._httpx_mock import (
- HTTPXMock,
- _PytestSyncTransport,
- _PytestAsyncTransport,
-)
+from pytest_httpx._httpx_mock import HTTPXMock
from pytest_httpx._httpx_internals import IteratorStream
from pytest_httpx.version import __version__
@@ -45,22 +41,36 @@ def httpx_mock(
mock = HTTPXMock()
# Mock synchronous requests
- real_sync_transport = httpx.Client._transport_for_url
+ real_handle_request = httpx.HTTPTransport.handle_request
+
+ def mocked_handle_request(
+ transport: httpx.HTTPTransport, request: httpx.Request
+ ) -> httpx.Response:
+ if request.url.host in non_mocked_hosts:
+ return real_handle_request(transport, request)
+ return mock._handle_request(transport, request)
+
monkeypatch.setattr(
- httpx.Client,
- "_transport_for_url",
- lambda self, url: real_sync_transport(self, url)
- if url.host in non_mocked_hosts
- else _PytestSyncTransport(real_sync_transport(self, url), mock),
+ httpx.HTTPTransport,
+ "handle_request",
+ mocked_handle_request,
)
+
# Mock asynchronous requests
- real_async_transport = httpx.AsyncClient._transport_for_url
+ real_handle_async_request = httpx.AsyncHTTPTransport.handle_async_request
+
+ async def mocked_handle_async_request(
+ transport: httpx.AsyncHTTPTransport, request: httpx.Request
+ ) -> httpx.Response:
+ if request.url.host in non_mocked_hosts:
+ return await real_handle_async_request(transport, request)
+ return await mock._handle_async_request(transport, request)
+
monkeypatch.setattr(
- httpx.AsyncClient,
- "_transport_for_url",
- lambda self, url: real_async_transport(self, url)
- if url.host in non_mocked_hosts
- else _PytestAsyncTransport(real_async_transport(self, url), mock),
+ httpx.AsyncHTTPTransport,
+ "handle_async_request",
+ mocked_handle_async_request,
)
+
yield mock
mock.reset(assert_all_responses_were_requested)
diff --git a/pytest_httpx/_httpx_internals.py b/pytest_httpx/_httpx_internals.py
index e12b933..da21797 100644
--- a/pytest_httpx/_httpx_internals.py
+++ b/pytest_httpx/_httpx_internals.py
@@ -61,12 +61,9 @@ def _to_httpx_url(url: httpcore.URL, headers: list[tuple[bytes, bytes]]) -> http
def _proxy_url(
- real_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport]
+ real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport]
) -> Optional[httpx.URL]:
- if isinstance(real_transport, httpx.HTTPTransport):
- if isinstance(real_pool := real_transport._pool, httpcore.HTTPProxy):
- return _to_httpx_url(real_pool._proxy_url, real_pool._proxy_headers)
-
- if isinstance(real_transport, httpx.AsyncHTTPTransport):
- if isinstance(real_pool := real_transport._pool, httpcore.AsyncHTTPProxy):
- return _to_httpx_url(real_pool._proxy_url, real_pool._proxy_headers)
+ if isinstance(
+ real_pool := real_transport._pool, (httpcore.HTTPProxy, httpcore.AsyncHTTPProxy)
+ ):
+ return _to_httpx_url(real_pool._proxy_url, real_pool._proxy_headers)
diff --git a/pytest_httpx/_httpx_mock.py b/pytest_httpx/_httpx_mock.py
index 425f420..941b050 100644
--- a/pytest_httpx/_httpx_mock.py
+++ b/pytest_httpx/_httpx_mock.py
@@ -12,7 +12,7 @@
class HTTPXMock:
def __init__(self) -> None:
self._requests: list[
- tuple[Union[httpx.BaseTransport, httpx.AsyncBaseTransport], httpx.Request]
+ tuple[Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport], httpx.Request]
] = []
self._callbacks: list[
tuple[
@@ -123,7 +123,7 @@ def exception_callback(request: httpx.Request) -> None:
def _handle_request(
self,
- real_transport: httpx.BaseTransport,
+ real_transport: httpx.HTTPTransport,
request: httpx.Request,
) -> httpx.Response:
self._requests.append((real_transport, request))
@@ -142,7 +142,7 @@ def _handle_request(
async def _handle_async_request(
self,
- real_transport: httpx.AsyncBaseTransport,
+ real_transport: httpx.AsyncHTTPTransport,
request: httpx.Request,
) -> httpx.Response:
self._requests.append((real_transport, request))
@@ -178,7 +178,7 @@ def _explain_that_no_response_was_found(
def _get_callback(
self,
- real_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport],
+ real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport],
request: httpx.Request,
) -> Optional[
Callable[
@@ -266,24 +266,6 @@ def _reset_callbacks(self) -> list[_RequestMatcher]:
return callbacks_not_executed
-class _PytestSyncTransport(httpx.BaseTransport):
- def __init__(self, real_transport: httpx.BaseTransport, mock: HTTPXMock):
- self._real_transport = real_transport
- self._mock = mock
-
- def handle_request(self, request: httpx.Request) -> httpx.Response:
- return self._mock._handle_request(self._real_transport, request)
-
-
-class _PytestAsyncTransport(httpx.AsyncBaseTransport):
- def __init__(self, real_transport: httpx.AsyncBaseTransport, mock: HTTPXMock):
- self._real_transport = real_transport
- self._mock = mock
-
- async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
- return await self._mock._handle_async_request(self._real_transport, request)
-
-
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/_request_matcher.py b/pytest_httpx/_request_matcher.py
index e80df4b..1bb590c 100644
--- a/pytest_httpx/_request_matcher.py
+++ b/pytest_httpx/_request_matcher.py
@@ -52,7 +52,7 @@ def __init__(
def match(
self,
- real_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport],
+ real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport],
request: httpx.Request,
) -> bool:
return (
@@ -106,7 +106,7 @@ def _content_match(self, request: httpx.Request) -> bool:
return False
def _proxy_match(
- self, real_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport]
+ self, real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport]
) -> bool:
if not self.proxy_url:
return True
diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py
index 584d5c4..54eb82a 100644
--- a/tests/test_httpx_async.py
+++ b/tests/test_httpx_async.py
@@ -5,7 +5,6 @@
import httpx
import pytest
-from httpx import Request, Response
from pytest import Testdir
from unittest.mock import ANY
@@ -2027,8 +2026,8 @@ def __init__(self, prefix: str, *args, **kwargs):
async def handle_async_request(
self,
- request: Request,
- ) -> Response:
+ request: httpx.Request,
+ ) -> httpx.Response:
httpx_response = await super().handle_async_request(request)
httpx_response.headers["x-prefix"] = self.prefix
return httpx_response
diff --git a/tests/test_httpx_sync.py b/tests/test_httpx_sync.py
index 00431c5..e1e4938 100644
--- a/tests/test_httpx_sync.py
+++ b/tests/test_httpx_sync.py
@@ -1,5 +1,4 @@
import re
-from typing import Any
from unittest.mock import ANY
import httpx
@@ -1706,3 +1705,25 @@ def test_mutating_json(httpx_mock: HTTPXMock) -> None:
response = client.get("https://test_url")
assert response.json() == {"content": "request 2"}
+
+
+def test_custom_transport(httpx_mock: HTTPXMock) -> None:
+ class CustomTransport(httpx.HTTPTransport):
+ def __init__(self, prefix: str, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.prefix = prefix
+
+ def handle_request(
+ self,
+ 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:
+ response = client.post("https://test_url", content=b"This is the body")
+ assert response.read() == b""
+ assert response.headers["x-prefix"] == "test"
From d8f952d3c3ef224e9d33f8e6536b2188520ee94e Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 13 Nov 2023 20:19:14 +0100
Subject: [PATCH 3/4] Handle python 3.12
---
.github/workflows/release.yml | 2 +-
.github/workflows/test.yml | 2 +-
.pre-commit-config.yaml | 2 +-
CHANGELOG.md | 3 +++
README.md | 2 +-
setup.py | 1 +
6 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 087ee82..ab4a5df 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['3.9', '3.10', '3.11']
+ python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 81c921d..8cac5ed 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['3.9', '3.10', '3.11']
+ python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index be9aceb..3ec3b4f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/psf/black
- rev: 23.7.0
+ rev: 23.11.0
hooks:
- id: black
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2088ce..6d8c4cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+- Explicit support for python `3.12`.
+
### Fixed
- Custom HTTP transport are now handled (parent call to `handle_async_request` or `handle_request`).
diff --git a/README.md b/README.md
index 006123d..8fc751c 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/setup.py b/setup.py
index e10c5ab..5e17eea 100644
--- a/setup.py
+++ b/setup.py
@@ -31,6 +31,7 @@
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Build Tools",
"Topic :: Internet :: WWW/HTTP",
"Framework :: Pytest",
From 68a3a32e0bc2c1c97201e275d17924e9243ba51e Mon Sep 17 00:00:00 2001
From: Colin-b
Date: Mon, 13 Nov 2023 20:24:24 +0100
Subject: [PATCH 4/4] Release version 0.27.0 today
---
CHANGELOG.md | 5 ++++-
pytest_httpx/version.py | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d8c4cf..84c4b2c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+
+## [0.27.0] - 2023-11-13
### Added
- Explicit support for python `3.12`.
@@ -295,7 +297,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- First release, should be considered as unstable for now as design might change.
-[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.26.0...HEAD
+[Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.27.0...HEAD
+[0.27.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.26.0...v0.27.0
[0.26.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.25.0...v0.26.0
[0.25.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.24.0...v0.25.0
[0.24.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.23.1...v0.24.0
diff --git a/pytest_httpx/version.py b/pytest_httpx/version.py
index 2773c8d..5921900 100644
--- a/pytest_httpx/version.py
+++ b/pytest_httpx/version.py
@@ -3,4 +3,4 @@
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
-__version__ = "0.26.0"
+__version__ = "0.27.0"