From 8640d397f9771f00a24e7d2bbefb869e9aa6e05e Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Fri, 17 Jul 2026 08:01:46 +0100 Subject: [PATCH 1/4] Retain explicitly provided falsy custom transports. Only None should select the default transport across all sync and async clients. Closes #3092. Co-authored-by: Cursor --- newsfragments/3092.change.rst | 1 + src/vws/async_query.py | 4 +- src/vws/async_vumark_service.py | 4 +- src/vws/async_vws.py | 4 +- src/vws/query.py | 4 +- src/vws/vumark_service.py | 4 +- src/vws/vws.py | 4 +- tests/test_transports.py | 201 ++++++++++++++++++++++++++++++++ 8 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 newsfragments/3092.change.rst diff --git a/newsfragments/3092.change.rst b/newsfragments/3092.change.rst new file mode 100644 index 000000000..a15105df8 --- /dev/null +++ b/newsfragments/3092.change.rst @@ -0,0 +1 @@ +Retain explicitly provided falsy custom transports instead of replacing them with defaults. diff --git a/src/vws/async_query.py b/src/vws/async_query.py index bebd3089d..88536144f 100644 --- a/src/vws/async_query.py +++ b/src/vws/async_query.py @@ -60,7 +60,9 @@ def __init__( self._client_secret_key = client_secret_key self._base_vwq_url = base_vwq_url self._request_timeout_seconds = request_timeout_seconds - self._transport = transport or AsyncHTTPXTransport() + self._transport = ( + transport if transport is not None else AsyncHTTPXTransport() + ) async def aclose(self) -> None: """Close the underlying transport if it supports closing.""" diff --git a/src/vws/async_vumark_service.py b/src/vws/async_vumark_service.py index 51c2ef349..24191d7c6 100644 --- a/src/vws/async_vumark_service.py +++ b/src/vws/async_vumark_service.py @@ -46,7 +46,9 @@ def __init__( self._server_secret_key = server_secret_key self._base_vws_url = base_vws_url self._request_timeout_seconds = request_timeout_seconds - self._transport = transport or AsyncHTTPXTransport() + self._transport = ( + transport if transport is not None else AsyncHTTPXTransport() + ) async def aclose(self) -> None: """Close the underlying transport if it supports closing.""" diff --git a/src/vws/async_vws.py b/src/vws/async_vws.py index 92675ac18..5ec523565 100644 --- a/src/vws/async_vws.py +++ b/src/vws/async_vws.py @@ -57,7 +57,9 @@ def __init__( self._server_secret_key = server_secret_key self._base_vws_url = base_vws_url self._request_timeout_seconds = request_timeout_seconds - self._transport = transport or AsyncHTTPXTransport() + self._transport = ( + transport if transport is not None else AsyncHTTPXTransport() + ) async def aclose(self) -> None: """Close the underlying transport if it supports closing.""" diff --git a/src/vws/query.py b/src/vws/query.py index 44840d6f0..3d9e19d7f 100644 --- a/src/vws/query.py +++ b/src/vws/query.py @@ -56,7 +56,9 @@ def __init__( self._client_secret_key = client_secret_key self._base_vwq_url = base_vwq_url self._request_timeout_seconds = request_timeout_seconds - self._transport = transport or RequestsTransport() + self._transport = ( + transport if transport is not None else RequestsTransport() + ) def query( self, diff --git a/src/vws/vumark_service.py b/src/vws/vumark_service.py index f4b04152f..a11eb5fc5 100644 --- a/src/vws/vumark_service.py +++ b/src/vws/vumark_service.py @@ -43,7 +43,9 @@ def __init__( self._server_secret_key = server_secret_key self._base_vws_url = base_vws_url self._request_timeout_seconds = request_timeout_seconds - self._transport = transport or RequestsTransport() + self._transport = ( + transport if transport is not None else RequestsTransport() + ) def generate_vumark_instance( self, diff --git a/src/vws/vws.py b/src/vws/vws.py index 7eda102fe..b5122ae5f 100644 --- a/src/vws/vws.py +++ b/src/vws/vws.py @@ -56,7 +56,9 @@ def __init__( self._server_secret_key = server_secret_key self._base_vws_url = base_vws_url self._request_timeout_seconds = request_timeout_seconds - self._transport = transport or RequestsTransport() + self._transport = ( + transport if transport is not None else RequestsTransport() + ) def make_request( self, diff --git a/tests/test_transports.py b/tests/test_transports.py index 752b0ef23..f8865e57d 100644 --- a/tests/test_transports.py +++ b/tests/test_transports.py @@ -1,13 +1,24 @@ """Tests for HTTP transport implementations.""" +import io +import uuid from http import HTTPStatus import httpx import pytest import respx +from vws import ( + VWS, + AsyncCloudRecoService, + AsyncVuMarkService, + AsyncVWS, + CloudRecoService, + VuMarkService, +) from vws.response import Response from vws.transports import AsyncHTTPXTransport, HTTPXTransport +from vws.vumark_accept import VuMarkAccept class TestHTTPXTransport: @@ -206,3 +217,193 @@ async def test_context_manager() -> None: assert route.called assert isinstance(response, Response) assert response.status_code == HTTPStatus.OK + + +class _FalsyTransport: + """A sync transport that is falsy but protocol-conforming.""" + + def __bool__(self) -> bool: + """Return ``False`` so truthiness checks would skip this + transport. + """ + return False + + def close(self) -> None: + """Close the transport.""" + + def __call__( + self, + *, + method: str, + url: str, + headers: dict[str, str], + data: bytes, + request_timeout: float | tuple[float, float], + ) -> Response: + """Return a successful API response for the requested URL.""" + del method, headers, request_timeout + if url.endswith("/query"): + body = '{"result_code":"Success","results":[]}' + return Response( + text=body, + url=url, + status_code=HTTPStatus.OK, + headers={}, + request_body=data, + tell_position=0, + content=body.encode(), + ) + if "/instances" in url: + content = b"vumark-bytes" + return Response( + text="", + url=url, + status_code=HTTPStatus.OK, + headers={}, + request_body=data, + tell_position=0, + content=content, + ) + body = '{"result_code":"Success","results":[]}' + return Response( + text=body, + url=url, + status_code=HTTPStatus.OK, + headers={}, + request_body=data, + tell_position=0, + content=body.encode(), + ) + + +class _FalsyAsyncTransport: + """An async transport that is falsy but protocol-conforming.""" + + def __bool__(self) -> bool: + """Return ``False`` so truthiness checks would skip this + transport. + """ + return False + + async def aclose(self) -> None: + """Close the transport.""" + + async def __call__( + self, + *, + method: str, + url: str, + headers: dict[str, str], + data: bytes, + request_timeout: float | tuple[float, float], + ) -> Response: + """Return a successful API response for the requested URL.""" + del method, headers, request_timeout + if url.endswith("/query"): + body = '{"result_code":"Success","results":[]}' + return Response( + text=body, + url=url, + status_code=HTTPStatus.OK, + headers={}, + request_body=data, + tell_position=0, + content=body.encode(), + ) + if "/instances" in url: + content = b"vumark-bytes" + return Response( + text="", + url=url, + status_code=HTTPStatus.OK, + headers={}, + request_body=data, + tell_position=0, + content=content, + ) + body = '{"result_code":"Success","results":[]}' + return Response( + text=body, + url=url, + status_code=HTTPStatus.OK, + headers={}, + request_body=data, + tell_position=0, + content=body.encode(), + ) + + +def test_falsy_sync_transport_is_retained( + high_quality_image: io.BytesIO, +) -> None: + """Falsy custom sync transports are not replaced by the default.""" + access_key = uuid.uuid4().hex + secret_key = uuid.uuid4().hex + transport = _FalsyTransport() + + assert ( + VWS( + server_access_key=access_key, + server_secret_key=secret_key, + transport=transport, + ).list_targets() + == [] + ) + assert ( + CloudRecoService( + client_access_key=access_key, + client_secret_key=secret_key, + transport=transport, + ).query(image=high_quality_image) + == [] + ) + assert ( + VuMarkService( + server_access_key=access_key, + server_secret_key=secret_key, + transport=transport, + ).generate_vumark_instance( + target_id="target", + instance_id="instance", + accept=VuMarkAccept.PNG, + ) + == b"vumark-bytes" + ) + + +@pytest.mark.asyncio +async def test_falsy_async_transport_is_retained( + high_quality_image: io.BytesIO, +) -> None: + """Falsy custom async transports are not replaced by the default.""" + access_key = uuid.uuid4().hex + secret_key = uuid.uuid4().hex + transport = _FalsyAsyncTransport() + + async with AsyncVWS( + server_access_key=access_key, + server_secret_key=secret_key, + transport=transport, + ) as vws_client: + assert await vws_client.list_targets() == [] + + async with AsyncCloudRecoService( + client_access_key=access_key, + client_secret_key=secret_key, + transport=transport, + ) as cloud_reco_client: + assert await cloud_reco_client.query(image=high_quality_image) == [] + + async with AsyncVuMarkService( + server_access_key=access_key, + server_secret_key=secret_key, + transport=transport, + ) as vumark_client: + assert ( + await vumark_client.generate_vumark_instance( + target_id="target", + instance_id="instance", + accept=VuMarkAccept.PNG, + ) + == b"vumark-bytes" + ) From 211f333b83edb390a29a2d85029eae59518ef6bc Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Fri, 17 Jul 2026 08:06:06 +0100 Subject: [PATCH 2/4] Fix spelling and pylint issues in falsy transport tests. Co-authored-by: Cursor --- spelling_private_dict.txt | 2 ++ tests/test_transports.py | 50 ++++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index 73d358a00..382e8ccd3 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -2,6 +2,7 @@ AuthenticationFailure BadImage ConnectionErrorPossiblyImageTooLarge DateRangeError +Falsy ImageTooLarge InactiveProject JSONDecodeError @@ -45,6 +46,7 @@ dev dict docstring enum +falsy filename foo formdata diff --git a/tests/test_transports.py b/tests/test_transports.py index f8865e57d..fa3c7aef8 100644 --- a/tests/test_transports.py +++ b/tests/test_transports.py @@ -341,34 +341,30 @@ def test_falsy_sync_transport_is_retained( secret_key = uuid.uuid4().hex transport = _FalsyTransport() - assert ( - VWS( - server_access_key=access_key, - server_secret_key=secret_key, - transport=transport, - ).list_targets() - == [] - ) - assert ( - CloudRecoService( - client_access_key=access_key, - client_secret_key=secret_key, - transport=transport, - ).query(image=high_quality_image) - == [] - ) - assert ( - VuMarkService( - server_access_key=access_key, - server_secret_key=secret_key, - transport=transport, - ).generate_vumark_instance( - target_id="target", - instance_id="instance", - accept=VuMarkAccept.PNG, - ) - == b"vumark-bytes" + targets = VWS( + server_access_key=access_key, + server_secret_key=secret_key, + transport=transport, + ).list_targets() + assert targets == [] + + query_results = CloudRecoService( + client_access_key=access_key, + client_secret_key=secret_key, + transport=transport, + ).query(image=high_quality_image) + assert query_results == [] + + vumark_bytes = VuMarkService( + server_access_key=access_key, + server_secret_key=secret_key, + transport=transport, + ).generate_vumark_instance( + target_id="target", + instance_id="instance", + accept=VuMarkAccept.PNG, ) + assert vumark_bytes == b"vumark-bytes" @pytest.mark.asyncio From c1edafb7edc3c225b9be1dd247c89ebba9ac3436 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Fri, 17 Jul 2026 08:08:42 +0100 Subject: [PATCH 3/4] Satisfy pylint empty-list comparison check in transport tests. Co-authored-by: Cursor --- tests/test_transports.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_transports.py b/tests/test_transports.py index fa3c7aef8..78c3ff0cf 100644 --- a/tests/test_transports.py +++ b/tests/test_transports.py @@ -346,14 +346,14 @@ def test_falsy_sync_transport_is_retained( server_secret_key=secret_key, transport=transport, ).list_targets() - assert targets == [] + assert not targets query_results = CloudRecoService( client_access_key=access_key, client_secret_key=secret_key, transport=transport, ).query(image=high_quality_image) - assert query_results == [] + assert not query_results vumark_bytes = VuMarkService( server_access_key=access_key, @@ -381,14 +381,14 @@ async def test_falsy_async_transport_is_retained( server_secret_key=secret_key, transport=transport, ) as vws_client: - assert await vws_client.list_targets() == [] + assert not await vws_client.list_targets() async with AsyncCloudRecoService( client_access_key=access_key, client_secret_key=secret_key, transport=transport, ) as cloud_reco_client: - assert await cloud_reco_client.query(image=high_quality_image) == [] + assert not await cloud_reco_client.query(image=high_quality_image) async with AsyncVuMarkService( server_access_key=access_key, From 9a9bf49c58faf9fd2ec7afb4b6b6daf931be8da6 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Fri, 17 Jul 2026 08:14:43 +0100 Subject: [PATCH 4/4] Cover falsy transport __bool__ methods in retention tests. Co-authored-by: Cursor --- tests/test_transports.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_transports.py b/tests/test_transports.py index 78c3ff0cf..f208339d4 100644 --- a/tests/test_transports.py +++ b/tests/test_transports.py @@ -340,6 +340,7 @@ def test_falsy_sync_transport_is_retained( access_key = uuid.uuid4().hex secret_key = uuid.uuid4().hex transport = _FalsyTransport() + assert not transport targets = VWS( server_access_key=access_key, @@ -375,6 +376,7 @@ async def test_falsy_async_transport_is_retained( access_key = uuid.uuid4().hex secret_key = uuid.uuid4().hex transport = _FalsyAsyncTransport() + assert not transport async with AsyncVWS( server_access_key=access_key,