From 413ba18d20465850f9d1b5323d8d876ef50d3a65 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Tue, 19 May 2026 12:34:21 +0100 Subject: [PATCH 1/6] Deduplicate httpx timeout construction --- src/vws/transports.py | 59 ++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/src/vws/transports.py b/src/vws/transports.py index 80aa989f..38d07af3 100644 --- a/src/vws/transports.py +++ b/src/vws/transports.py @@ -10,6 +10,29 @@ from vws.response import Response +def _httpx_timeout( + request_timeout: float | tuple[float, float], +) -> httpx.Timeout: + """Convert a VWS request timeout to an ``httpx`` timeout.""" + match request_timeout: + case (connect_timeout, read_timeout): + return httpx.Timeout( + connect=connect_timeout, + read=read_timeout, + write=None, + pool=None, + ) + case float() | int() as timeout: + return httpx.Timeout( + connect=timeout, + read=timeout, + write=None, + pool=None, + ) + case _: + raise AssertionError + + @runtime_checkable class Transport(Protocol): """Protocol for HTTP transports used by VWS clients. @@ -149,28 +172,12 @@ def __call__( Returns: A Response populated from the httpx response. """ - if isinstance(request_timeout, tuple): - connect_timeout, read_timeout = request_timeout - httpx_timeout = httpx.Timeout( - connect=connect_timeout, - read=read_timeout, - write=None, - pool=None, - ) - else: - httpx_timeout = httpx.Timeout( - connect=request_timeout, - read=request_timeout, - write=None, - pool=None, - ) - httpx_response = self._client.request( method=method, url=url, headers=headers, content=data, - timeout=httpx_timeout, + timeout=_httpx_timeout(request_timeout=request_timeout), follow_redirects=True, ) @@ -272,28 +279,12 @@ async def __call__( Returns: A Response populated from the httpx response. """ - if isinstance(request_timeout, tuple): - connect_timeout, read_timeout = request_timeout - httpx_timeout = httpx.Timeout( - connect=connect_timeout, - read=read_timeout, - write=None, - pool=None, - ) - else: - httpx_timeout = httpx.Timeout( - connect=request_timeout, - read=request_timeout, - write=None, - pool=None, - ) - httpx_response = await self._client.request( method=method, url=url, headers=headers, content=data, - timeout=httpx_timeout, + timeout=_httpx_timeout(request_timeout=request_timeout), follow_redirects=True, ) From 1fb9ed134c558d3646b3533b22a362aaf587ee80 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Tue, 19 May 2026 12:42:09 +0100 Subject: [PATCH 2/6] Fix timeout helper coverage --- src/vws/transports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vws/transports.py b/src/vws/transports.py index 38d07af3..991c76be 100644 --- a/src/vws/transports.py +++ b/src/vws/transports.py @@ -29,7 +29,7 @@ def _httpx_timeout( write=None, pool=None, ) - case _: + case _: # pragma: no cover raise AssertionError From b4355ac6523ca381d153fb4f304fbe561bb129b3 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Tue, 19 May 2026 12:52:02 +0100 Subject: [PATCH 3/6] Inline httpx timeout match handling --- src/vws/transports.py | 63 ++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/vws/transports.py b/src/vws/transports.py index 991c76be..2adaa323 100644 --- a/src/vws/transports.py +++ b/src/vws/transports.py @@ -10,29 +10,6 @@ from vws.response import Response -def _httpx_timeout( - request_timeout: float | tuple[float, float], -) -> httpx.Timeout: - """Convert a VWS request timeout to an ``httpx`` timeout.""" - match request_timeout: - case (connect_timeout, read_timeout): - return httpx.Timeout( - connect=connect_timeout, - read=read_timeout, - write=None, - pool=None, - ) - case float() | int() as timeout: - return httpx.Timeout( - connect=timeout, - read=timeout, - write=None, - pool=None, - ) - case _: # pragma: no cover - raise AssertionError - - @runtime_checkable class Transport(Protocol): """Protocol for HTTP transports used by VWS clients. @@ -172,12 +149,30 @@ def __call__( Returns: A Response populated from the httpx response. """ + match request_timeout: + case (connect_timeout, read_timeout): + httpx_timeout = httpx.Timeout( + connect=connect_timeout, + read=read_timeout, + write=None, + pool=None, + ) + case float() | int() as timeout: + httpx_timeout = httpx.Timeout( + connect=timeout, + read=timeout, + write=None, + pool=None, + ) + case _: # pragma: no cover + raise AssertionError + httpx_response = self._client.request( method=method, url=url, headers=headers, content=data, - timeout=_httpx_timeout(request_timeout=request_timeout), + timeout=httpx_timeout, follow_redirects=True, ) @@ -279,12 +274,30 @@ async def __call__( Returns: A Response populated from the httpx response. """ + match request_timeout: + case (connect_timeout, read_timeout): + httpx_timeout = httpx.Timeout( + connect=connect_timeout, + read=read_timeout, + write=None, + pool=None, + ) + case float() | int() as timeout: + httpx_timeout = httpx.Timeout( + connect=timeout, + read=timeout, + write=None, + pool=None, + ) + case _: # pragma: no cover + raise AssertionError + httpx_response = await self._client.request( method=method, url=url, headers=headers, content=data, - timeout=_httpx_timeout(request_timeout=request_timeout), + timeout=httpx_timeout, follow_redirects=True, ) From 2d3c110f093a337b546409bae4a0c25a6fa0e288 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Tue, 19 May 2026 12:58:05 +0100 Subject: [PATCH 4/6] Use assert_never for timeout match fallback --- pyproject.toml | 1 + src/vws/transports.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d8cbe66f..898d266e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -333,6 +333,7 @@ log_cli = true [tool.coverage] run.branch = true report.exclude_also = [ + "case _ as unreachable:\n\\s*assert_never\\(", "if TYPE_CHECKING:", ] report.show_missing = true diff --git a/src/vws/transports.py b/src/vws/transports.py index 2adaa323..5c193cc1 100644 --- a/src/vws/transports.py +++ b/src/vws/transports.py @@ -1,7 +1,7 @@ """HTTP transport implementations for VWS clients.""" from collections.abc import Awaitable -from typing import Protocol, Self, runtime_checkable +from typing import Protocol, Self, assert_never, runtime_checkable import httpx import requests @@ -164,8 +164,8 @@ def __call__( write=None, pool=None, ) - case _: # pragma: no cover - raise AssertionError + case _ as unreachable: + assert_never(unreachable) # pyrefly: ignore[bad-argument-type] # ty: ignore[type-assertion-failure] httpx_response = self._client.request( method=method, @@ -289,8 +289,8 @@ async def __call__( write=None, pool=None, ) - case _: # pragma: no cover - raise AssertionError + case _ as unreachable: + assert_never(unreachable) # pyrefly: ignore[bad-argument-type] # ty: ignore[type-assertion-failure] httpx_response = await self._client.request( method=method, From 8cf510f9c521f1a0cb87544579f921d119df5d8f Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Tue, 19 May 2026 13:00:58 +0100 Subject: [PATCH 5/6] Add type checker names to spelling dictionary --- spelling_private_dict.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index 73d358a0..04814bde 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -79,6 +79,7 @@ plugins png pragma py +pyrefly pyright pytest readme @@ -97,6 +98,7 @@ todo traceback travis txt +ty unmocked untyped url From 7e3c00f4847c087de5f74f37862408a2fb92f659 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Tue, 19 May 2026 13:19:42 +0100 Subject: [PATCH 6/6] Avoid timeout match suppressions --- pyproject.toml | 1 - spelling_private_dict.txt | 2 -- src/vws/transports.py | 16 ++++++-------- tests/test_transports.py | 45 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 898d266e..d8cbe66f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -333,7 +333,6 @@ log_cli = true [tool.coverage] run.branch = true report.exclude_also = [ - "case _ as unreachable:\n\\s*assert_never\\(", "if TYPE_CHECKING:", ] report.show_missing = true diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index 04814bde..73d358a0 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -79,7 +79,6 @@ plugins png pragma py -pyrefly pyright pytest readme @@ -98,7 +97,6 @@ todo traceback travis txt -ty unmocked untyped url diff --git a/src/vws/transports.py b/src/vws/transports.py index 5c193cc1..74fadb9f 100644 --- a/src/vws/transports.py +++ b/src/vws/transports.py @@ -1,7 +1,7 @@ """HTTP transport implementations for VWS clients.""" from collections.abc import Awaitable -from typing import Protocol, Self, assert_never, runtime_checkable +from typing import Protocol, Self, runtime_checkable import httpx import requests @@ -150,22 +150,21 @@ def __call__( A Response populated from the httpx response. """ match request_timeout: - case (connect_timeout, read_timeout): + case tuple() as timeout: + connect_timeout, read_timeout = timeout httpx_timeout = httpx.Timeout( connect=connect_timeout, read=read_timeout, write=None, pool=None, ) - case float() | int() as timeout: + case timeout: httpx_timeout = httpx.Timeout( connect=timeout, read=timeout, write=None, pool=None, ) - case _ as unreachable: - assert_never(unreachable) # pyrefly: ignore[bad-argument-type] # ty: ignore[type-assertion-failure] httpx_response = self._client.request( method=method, @@ -275,22 +274,21 @@ async def __call__( A Response populated from the httpx response. """ match request_timeout: - case (connect_timeout, read_timeout): + case tuple() as timeout: + connect_timeout, read_timeout = timeout httpx_timeout = httpx.Timeout( connect=connect_timeout, read=read_timeout, write=None, pool=None, ) - case float() | int() as timeout: + case timeout: httpx_timeout = httpx.Timeout( connect=timeout, read=timeout, write=None, pool=None, ) - case _ as unreachable: - assert_never(unreachable) # pyrefly: ignore[bad-argument-type] # ty: ignore[type-assertion-failure] httpx_response = await self._client.request( method=method, diff --git a/tests/test_transports.py b/tests/test_transports.py index 4130859d..752b0ef2 100644 --- a/tests/test_transports.py +++ b/tests/test_transports.py @@ -61,6 +61,28 @@ def test_tuple_timeout() -> None: assert isinstance(response, Response) assert response.status_code == HTTPStatus.OK + @staticmethod + @respx.mock + def test_int_timeout() -> None: + """``HTTPXTransport`` works with an int timeout.""" + route = respx.post(url="https://example.com/test").mock( + return_value=httpx.Response( + status_code=HTTPStatus.OK, + text="OK", + ), + ) + transport = HTTPXTransport() + response = transport( + method="POST", + url="https://example.com/test", + headers={"Content-Type": "text/plain"}, + data=b"hello", + request_timeout=30, + ) + assert route.called + assert isinstance(response, Response) + assert response.status_code == HTTPStatus.OK + @staticmethod @respx.mock def test_context_manager() -> None: @@ -137,6 +159,29 @@ async def test_tuple_timeout() -> None: assert isinstance(response, Response) assert response.status_code == HTTPStatus.OK + @staticmethod + @pytest.mark.asyncio + @respx.mock + async def test_int_timeout() -> None: + """``AsyncHTTPXTransport`` works with an int timeout.""" + route = respx.post(url="https://example.com/test").mock( + return_value=httpx.Response( + status_code=HTTPStatus.OK, + text="OK", + ), + ) + transport = AsyncHTTPXTransport() + response = await transport( + method="POST", + url="https://example.com/test", + headers={"Content-Type": "text/plain"}, + data=b"hello", + request_timeout=30, + ) + assert route.called + assert isinstance(response, Response) + assert response.status_code == HTTPStatus.OK + @staticmethod @pytest.mark.asyncio @respx.mock