From 20a8b88e5784533beda66124729fa1fa141f726d Mon Sep 17 00:00:00 2001 From: tboy1337 Date: Tue, 21 Oct 2025 14:33:43 +0100 Subject: [PATCH] Add _prepare_timeout method to HTTPAdapter for timeout handling This commit introduces the _prepare_timeout method in the HTTPAdapter class, which standardizes the conversion of various timeout inputs (None, float, tuple, or TimeoutSauce) into a TimeoutSauce object. The method raises appropriate ValueErrors for invalid input formats. Additionally, tests have been added to ensure the correct functionality of this method, covering various scenarios including valid tuples, floats, and error handling for incorrect input formats. --- src/requests/adapters.py | 38 +++++++++++------ tests/test_adapters.py | 88 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 13 deletions(-) diff --git a/src/requests/adapters.py b/src/requests/adapters.py index 670c92767c..c4d3725335 100644 --- a/src/requests/adapters.py +++ b/src/requests/adapters.py @@ -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 `. + + :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 ): @@ -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( diff --git a/tests/test_adapters.py b/tests/test_adapters.py index 6c55d5a130..3419afe394 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -1,3 +1,6 @@ +import pytest +from urllib3.util import Timeout as TimeoutSauce + import requests.adapters @@ -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