Skip to content
Open
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
38 changes: 25 additions & 13 deletions src/requests/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,30 @@ def proxy_headers(self, proxy):

return headers

def _prepare_timeout(self, timeout):
"""Convert timeout to TimeoutSauce object.

This method should not be called from user code, and is only exposed
for use when subclassing the
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.

:param timeout: None, float, tuple, or TimeoutSauce object
:return: TimeoutSauce object
:raises ValueError: If timeout tuple has wrong format
"""
if isinstance(timeout, tuple):
try:
connect, read = timeout
return TimeoutSauce(connect=connect, read=read)
except ValueError as exc:
raise ValueError(
f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, "
f"or a single float to set both timeouts to the same value."
) from exc
if isinstance(timeout, TimeoutSauce):
return timeout
return TimeoutSauce(connect=timeout, read=timeout)

def send(
self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
):
Expand Down Expand Up @@ -626,19 +650,7 @@ def send(

chunked = not (request.body is None or "Content-Length" in request.headers)

if isinstance(timeout, tuple):
try:
connect, read = timeout
timeout = TimeoutSauce(connect=connect, read=read)
except ValueError:
raise ValueError(
f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, "
f"or a single float to set both timeouts to the same value."
)
elif isinstance(timeout, TimeoutSauce):
pass
else:
timeout = TimeoutSauce(connect=timeout, read=timeout)
timeout = self._prepare_timeout(timeout)

try:
resp = conn.urlopen(
Expand Down
88 changes: 88 additions & 0 deletions tests/test_adapters.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import pytest
from urllib3.util import Timeout as TimeoutSauce

import requests.adapters


Expand All @@ -6,3 +9,88 @@ def test_request_url_trims_leading_path_separators():
a = requests.adapters.HTTPAdapter()
p = requests.Request(method="GET", url="http://127.0.0.1:10000//v:h").prepare()
assert "/v:h" == a.request_url(p, {})


class TestPrepareTimeout:
"""Tests for timeout processing in HTTPAdapter."""

def test_prepare_timeout_with_valid_tuple(self):
"""Test that valid timeout tuples are converted to TimeoutSauce."""
adapter = requests.adapters.HTTPAdapter()
result = adapter._prepare_timeout((5.0, 10.0))
assert isinstance(result, TimeoutSauce)
assert result.connect_timeout == 5.0
assert result.read_timeout == 10.0

def test_prepare_timeout_with_none_values_in_tuple(self):
"""Test that tuples with None values are handled correctly."""
adapter = requests.adapters.HTTPAdapter()
result = adapter._prepare_timeout((5.0, None))
assert isinstance(result, TimeoutSauce)
assert result.connect_timeout == 5.0
assert result.read_timeout is None

def test_prepare_timeout_with_float(self):
"""Test that float timeouts are converted to TimeoutSauce."""
adapter = requests.adapters.HTTPAdapter()
result = adapter._prepare_timeout(10.0)
assert isinstance(result, TimeoutSauce)
assert result.connect_timeout == 10.0
assert result.read_timeout == 10.0

def test_prepare_timeout_with_none(self):
"""Test that None timeout is converted to TimeoutSauce."""
adapter = requests.adapters.HTTPAdapter()
result = adapter._prepare_timeout(None)
assert isinstance(result, TimeoutSauce)
assert result.connect_timeout is None
assert result.read_timeout is None

def test_prepare_timeout_with_timeout_sauce(self):
"""Test that TimeoutSauce objects are returned unchanged."""
adapter = requests.adapters.HTTPAdapter()
timeout_sauce = TimeoutSauce(connect=3.0, read=5.0)
result = adapter._prepare_timeout(timeout_sauce)
assert result is timeout_sauce

def test_prepare_timeout_with_too_many_values(self):
"""Test that tuples with too many values raise ValueError with chaining."""
adapter = requests.adapters.HTTPAdapter()
with pytest.raises(ValueError) as exc_info:
adapter._prepare_timeout((1, 2, 3))

# Check that the error message is correct
assert "Invalid timeout" in str(exc_info.value)
assert "(connect, read)" in str(exc_info.value)

# Check that exception chaining is preserved
assert exc_info.value.__cause__ is not None
assert isinstance(exc_info.value.__cause__, ValueError)
assert "too many values to unpack" in str(exc_info.value.__cause__)

def test_prepare_timeout_with_too_few_values(self):
"""Test that tuples with too few values raise ValueError with chaining."""
adapter = requests.adapters.HTTPAdapter()
with pytest.raises(ValueError) as exc_info:
adapter._prepare_timeout((1,))

# Check that the error message is correct
assert "Invalid timeout" in str(exc_info.value)
assert "(connect, read)" in str(exc_info.value)

# Check that exception chaining is preserved
assert exc_info.value.__cause__ is not None
assert isinstance(exc_info.value.__cause__, ValueError)
assert "not enough values to unpack" in str(exc_info.value.__cause__)

def test_prepare_timeout_with_empty_tuple(self):
"""Test that empty tuples raise ValueError with chaining."""
adapter = requests.adapters.HTTPAdapter()
with pytest.raises(ValueError) as exc_info:
adapter._prepare_timeout(())

# Check that the error message is correct
assert "Invalid timeout" in str(exc_info.value)

# Check that exception chaining is preserved
assert exc_info.value.__cause__ is not None