diff --git a/py_hamt/store_httpx.py b/py_hamt/store_httpx.py index 89b6c9b..a4bb45d 100644 --- a/py_hamt/store_httpx.py +++ b/py_hamt/store_httpx.py @@ -232,9 +232,21 @@ def __init__( # helper: get or create the client bound to the current running loop # # --------------------------------------------------------------------- # def _loop_client(self) -> httpx.AsyncClient: - """Get or create a client for the current event loop.""" + """Get or create a client for the current event loop. + + If the instance was previously closed but owns its clients, a fresh + client mapping is lazily created on demand. Users that supplied their + own ``httpx.AsyncClient`` still receive an error when the instance has + been closed, as we cannot safely recreate their client. + """ if self._closed: - raise RuntimeError("KuboCAS is closed; create a new instance") + if not self._owns_client: + raise RuntimeError("KuboCAS is closed; create a new instance") + # We previously closed all internally-owned clients. Reset the + # state so that new clients can be created lazily. + self._closed = False + self._client_per_loop = {} + loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() try: return self._client_per_loop[loop] @@ -245,7 +257,7 @@ def _loop_client(self) -> httpx.AsyncClient: headers=self._default_headers, auth=self._default_auth, limits=httpx.Limits(max_connections=64, max_keepalive_connections=32), - # Uncomment when they finally support Robost HTTP/2 GOAWAY responses + # Uncomment when they finally support Robust HTTP/2 GOAWAY responses # http2=True, ) self._client_per_loop[loop] = client diff --git a/tests/test_kubocas_session.py b/tests/test_kubocas_session.py index 7e01938..bcc1c48 100644 --- a/tests/test_kubocas_session.py +++ b/tests/test_kubocas_session.py @@ -3,6 +3,7 @@ import unittest from threading import Event, Thread +import httpx import pytest from py_hamt import KuboCAS @@ -189,18 +190,34 @@ def fake_run(coro): @pytest.mark.asyncio -async def test_loop_client_raises_after_close(): - """ - Verify that calling _loop_client() on a closed KuboCAS instance - raises a RuntimeError. - """ - # Arrange: Create a KuboCAS instance +async def test_loop_client_reopens_after_close(): + """Calling _loop_client() after aclose() recreates a fresh client.""" cas = KuboCAS() - # Act: Close the instance. This should set the internal _closed flag. + first = cas._loop_client() await cas.aclose() - # Assert: Check that calling _loop_client again raises the expected error. + # Should no longer raise; instead a new client is created. + reopened = cas._loop_client() + assert isinstance(reopened, httpx.AsyncClient) + assert reopened is not first + assert cas._closed is False + + await cas.aclose() + + +@pytest.mark.asyncio +async def test_loop_client_rejects_reuse_of_external_client(global_client_session): + """Calling _loop_client() after aclose() raises when client is user-supplied.""" + cas = KuboCAS( + client=global_client_session, + rpc_base_url="http://127.0.0.1:5001", + gateway_base_url="http://127.0.0.1:8080", + ) + assert cas._loop_client() is global_client_session + + await cas.aclose() + cas._closed = True # simulate closed instance with external client with pytest.raises(RuntimeError, match="KuboCAS is closed; create a new instance"): cas._loop_client()