diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc2f92..3ea4360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ 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 + * `RequestsHTTPClient`: optional `requester` parameter ## [0.4.3] - 2026-03-14 diff --git a/Makefile b/Makefile index dfa4548..9e112bf 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,8 @@ lint: flake8 $(PACKAGE) tests pylint --rcfile=pyproject.toml $(PACKAGE) tests mypy $(PACKAGE) tests + +audit: pip-audit -l .PHONY: testall diff --git a/httprest/http/requests_client.py b/httprest/http/requests_client.py index 6824183..db663eb 100644 --- a/httprest/http/requests_client.py +++ b/httprest/http/requests_client.py @@ -1,5 +1,6 @@ """HTTP client which uses `requests` under the hood.""" +from types import ModuleType from typing import Optional, Union import requests @@ -12,11 +13,25 @@ class RequestsHTTPClient(HTTPClient): - """`requests` HTTP client.""" + """`requests` HTTP client. - def __init__(self, timeout: Optional[Timeout] = None) -> None: + By default a reusable ``requests.Session`` is used (connection reuse). + Pass ``requester`` to override this: + + * a custom ``requests.Session`` (e.g. with retries/adapters configured), or + * the ``requests`` module itself, to make independent module-level calls + with no connection reuse. + """ + + def __init__( + self, + timeout: Optional[Timeout] = None, + requester: Optional[Union[requests.Session, ModuleType]] = None, + ) -> None: super().__init__(timeout) - self._session = requests.Session() + self._requester = ( + requester if requester is not None else requests.Session() + ) def _request( self, @@ -37,7 +52,7 @@ def _request( ) try: response: requests.Response = getattr( - self._session, method.lower() + self._requester, method.lower() )( url, data=data, diff --git a/tests/unit/http/test_requests_client.py b/tests/unit/http/test_requests_client.py index 16ee597..e1ff293 100644 --- a/tests/unit/http/test_requests_client.py +++ b/tests/unit/http/test_requests_client.py @@ -81,3 +81,34 @@ def test_query_params(): "get", "https://example.com", params={"p": "hello/world"} ) assert response.status_code == 200 + + +@responses.activate +def test_requester_module(): + """Test using the `requests` module directly (no session).""" + responses.add(responses.GET, "https://example.com", json={"k": "v"}) + client = RequestsHTTPClient(requester=requests) + response = client.request("get", "https://example.com") + assert response.status_code == 200 + assert response.json == {"k": "v"} + + +@responses.activate +def test_injected_session_is_used(): + """An injected session must be the one actually performing the request. + + The session carries a custom header that a default, internally-created + session would not have. The request is only matched (and succeeds) if it + goes through this exact session, so matching proves it is used -- no need + to inspect the client's internals. + """ + responses.get( + "https://example.com", + json={"k": "v"}, + match=[matchers.header_matcher({"X-Custom": "v"})], + ) + session = requests.Session() + session.headers["X-Custom"] = "v" + client = RequestsHTTPClient(requester=session) + response = client.request("get", "https://example.com") + assert response.status_code == 200