Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ lint:
flake8 $(PACKAGE) tests
pylint --rcfile=pyproject.toml $(PACKAGE) tests
mypy $(PACKAGE) tests

audit:
pip-audit -l

.PHONY: testall
Expand Down
23 changes: 19 additions & 4 deletions httprest/http/requests_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""HTTP client which uses `requests` under the hood."""

from types import ModuleType
from typing import Optional, Union

import requests
Expand All @@ -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,
Expand All @@ -37,7 +52,7 @@ def _request(
)
try:
response: requests.Response = getattr(
self._session, method.lower()
self._requester, method.lower()
)(
url,
data=data,
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/http/test_requests_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading