]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Tighten client closed-state behaviour (#1346)
authorTom Christie <tom@tomchristie.com>
Tue, 6 Oct 2020 12:38:05 +0000 (13:38 +0100)
committerGitHub <noreply@github.com>
Tue, 6 Oct 2020 12:38:05 +0000 (13:38 +0100)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
httpx/_client.py
tests/client/test_async_client.py
tests/client/test_client.py

index 74cee4f1dcd606d173a55f912431d2bcf36da781..46d121ee59f0655d7c4072ca33cde4a38d697726 100644 (file)
@@ -1,4 +1,5 @@
 import datetime
+import enum
 import functools
 import typing
 import warnings
@@ -71,6 +72,12 @@ ACCEPT_ENCODING = ", ".join(
 )
 
 
+class ClientState(enum.Enum):
+    UNOPENED = 1
+    OPENED = 2
+    CLOSED = 3
+
+
 class BaseClient:
     def __init__(
         self,
@@ -101,14 +108,14 @@ class BaseClient:
         }
         self._trust_env = trust_env
         self._netrc = NetRCInfo()
-        self._is_closed = True
+        self._state = ClientState.UNOPENED
 
     @property
     def is_closed(self) -> bool:
         """
         Check if the client being closed
         """
-        return self._is_closed
+        return self._state == ClientState.CLOSED
 
     @property
     def trust_env(self) -> bool:
@@ -750,8 +757,10 @@ class Client(BaseClient):
 
         [0]: /advanced/#request-instances
         """
-        self._is_closed = False
+        if self._state == ClientState.CLOSED:
+            raise RuntimeError("Cannot send a request, as the client has been closed.")
 
+        self._state = ClientState.OPENED
         timeout = self.timeout if isinstance(timeout, UnsetType) else Timeout(timeout)
 
         auth = self._build_request_auth(request, auth)
@@ -1104,8 +1113,8 @@ class Client(BaseClient):
         """
         Close transport and proxies.
         """
-        if not self.is_closed:
-            self._is_closed = True
+        if self._state != ClientState.CLOSED:
+            self._state = ClientState.CLOSED
 
             self._transport.close()
             for proxy in self._proxies.values():
@@ -1113,11 +1122,12 @@ class Client(BaseClient):
                     proxy.close()
 
     def __enter__(self: T) -> T:
+        self._state = ClientState.OPENED
+
         self._transport.__enter__()
         for proxy in self._proxies.values():
             if proxy is not None:
                 proxy.__enter__()
-        self._is_closed = False
         return self
 
     def __exit__(
@@ -1126,13 +1136,12 @@ class Client(BaseClient):
         exc_value: BaseException = None,
         traceback: TracebackType = None,
     ) -> None:
-        if not self.is_closed:
-            self._is_closed = True
+        self._state = ClientState.CLOSED
 
-            self._transport.__exit__(exc_type, exc_value, traceback)
-            for proxy in self._proxies.values():
-                if proxy is not None:
-                    proxy.__exit__(exc_type, exc_value, traceback)
+        self._transport.__exit__(exc_type, exc_value, traceback)
+        for proxy in self._proxies.values():
+            if proxy is not None:
+                proxy.__exit__(exc_type, exc_value, traceback)
 
     def __del__(self) -> None:
         self.close()
@@ -1394,8 +1403,10 @@ class AsyncClient(BaseClient):
 
         [0]: /advanced/#request-instances
         """
-        self._is_closed = False
+        if self._state == ClientState.CLOSED:
+            raise RuntimeError("Cannot send a request, as the client has been closed.")
 
+        self._state = ClientState.OPENED
         timeout = self.timeout if isinstance(timeout, UnsetType) else Timeout(timeout)
 
         auth = self._build_request_auth(request, auth)
@@ -1750,8 +1761,8 @@ class AsyncClient(BaseClient):
         """
         Close transport and proxies.
         """
-        if not self.is_closed:
-            self._is_closed = True
+        if self._state != ClientState.CLOSED:
+            self._state = ClientState.CLOSED
 
             await self._transport.aclose()
             for proxy in self._proxies.values():
@@ -1759,11 +1770,12 @@ class AsyncClient(BaseClient):
                     await proxy.aclose()
 
     async def __aenter__(self: U) -> U:
+        self._state = ClientState.OPENED
+
         await self._transport.__aenter__()
         for proxy in self._proxies.values():
             if proxy is not None:
                 await proxy.__aenter__()
-        self._is_closed = False
         return self
 
     async def __aexit__(
@@ -1772,15 +1784,15 @@ class AsyncClient(BaseClient):
         exc_value: BaseException = None,
         traceback: TracebackType = None,
     ) -> None:
-        if not self.is_closed:
-            self._is_closed = True
-            await self._transport.__aexit__(exc_type, exc_value, traceback)
-            for proxy in self._proxies.values():
-                if proxy is not None:
-                    await proxy.__aexit__(exc_type, exc_value, traceback)
+        self._state = ClientState.CLOSED
+
+        await self._transport.__aexit__(exc_type, exc_value, traceback)
+        for proxy in self._proxies.values():
+            if proxy is not None:
+                await proxy.__aexit__(exc_type, exc_value, traceback)
 
     def __del__(self) -> None:
-        if not self.is_closed:
+        if self._state == ClientState.OPENED:
             warnings.warn(
                 f"Unclosed {self!r}. "
                 "See https://www.python-httpx.org/async/#opening-and-closing-clients "
index 32f5424a18dedca38bac3e5d345ebe6556a893b7..9aa995c2f3ac4a3477e3b9a618823d3dca573b31 100644 (file)
@@ -4,6 +4,7 @@ import httpcore
 import pytest
 
 import httpx
+from tests.utils import MockTransport
 
 
 @pytest.mark.usefixtures("async_environment")
@@ -208,43 +209,39 @@ async def test_context_managed_transport():
     ]
 
 
-@pytest.mark.usefixtures("async_environment")
-async def test_that_async_client_is_closed_by_default():
-    client = httpx.AsyncClient()
-
-    assert client.is_closed
+def hello_world(request):
+    return httpx.Response(200, text="Hello, world!")
 
 
 @pytest.mark.usefixtures("async_environment")
-async def test_that_send_cause_async_client_to_be_not_closed():
-    client = httpx.AsyncClient()
+async def test_client_closed_state_using_implicit_open():
+    client = httpx.AsyncClient(transport=MockTransport(hello_world))
 
+    assert not client.is_closed
     await client.get("http://example.com")
 
     assert not client.is_closed
-
     await client.aclose()
 
-
-@pytest.mark.usefixtures("async_environment")
-async def test_that_async_client_is_not_closed_in_with_block():
-    async with httpx.AsyncClient() as client:
-        assert not client.is_closed
+    assert client.is_closed
+    with pytest.raises(RuntimeError):
+        await client.get("http://example.com")
 
 
 @pytest.mark.usefixtures("async_environment")
-async def test_that_async_client_is_closed_after_with_block():
-    async with httpx.AsyncClient() as client:
-        pass
+async def test_client_closed_state_using_with_block():
+    async with httpx.AsyncClient(transport=MockTransport(hello_world)) as client:
+        assert not client.is_closed
+        await client.get("http://example.com")
 
     assert client.is_closed
+    with pytest.raises(RuntimeError):
+        await client.get("http://example.com")
 
 
 @pytest.mark.usefixtures("async_environment")
-async def test_that_async_client_caused_warning_when_being_deleted():
-    async_client = httpx.AsyncClient()
-
-    await async_client.get("http://example.com")
-
+async def test_deleting_unclosed_async_client_causes_warning():
+    client = httpx.AsyncClient(transport=MockTransport(hello_world))
+    await client.get("http://example.com")
     with pytest.warns(UserWarning):
-        del async_client
+        del client
index 54c67a366bf44a8c44420ef8a16c4526b5ef644a..b56ac5ac22cf219fac28db67d165a69138c9ff5a 100644 (file)
@@ -4,6 +4,7 @@ import httpcore
 import pytest
 
 import httpx
+from tests.utils import MockTransport
 
 
 def test_get(server):
@@ -247,27 +248,29 @@ def test_context_managed_transport():
     ]
 
 
-def test_that_client_is_closed_by_default():
-    client = httpx.Client()
-
-    assert client.is_closed
+def hello_world(request):
+    return httpx.Response(200, text="Hello, world!")
 
 
-def test_that_send_cause_client_to_be_not_closed():
-    client = httpx.Client()
+def test_client_closed_state_using_implicit_open():
+    client = httpx.Client(transport=MockTransport(hello_world))
 
+    assert not client.is_closed
     client.get("http://example.com")
 
     assert not client.is_closed
+    client.close()
 
-
-def test_that_client_is_not_closed_in_with_block():
-    with httpx.Client() as client:
-        assert not client.is_closed
+    assert client.is_closed
+    with pytest.raises(RuntimeError):
+        client.get("http://example.com")
 
 
-def test_that_client_is_closed_after_with_block():
-    with httpx.Client() as client:
-        pass
+def test_client_closed_state_using_with_block():
+    with httpx.Client(transport=MockTransport(hello_world)) as client:
+        assert not client.is_closed
+        client.get("http://example.com")
 
     assert client.is_closed
+    with pytest.raises(RuntimeError):
+        client.get("http://example.com")