Tighten client closed-state behaviour (#1346)

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
This commit is contained in:
Tom Christie 2020-10-06 13:38:05 +01:00 committed by GitHub
parent a3eb0f99dc
commit 2a2bbe58a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 61 deletions

View 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 "

View 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()
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_not_closed_in_with_block():
async with httpx.AsyncClient() as client:
async def test_client_closed_state_using_with_block():
async with httpx.AsyncClient(transport=MockTransport(hello_world)) as client:
assert not client.is_closed
@pytest.mark.usefixtures("async_environment")
async def test_that_async_client_is_closed_after_with_block():
async with httpx.AsyncClient() as client:
pass
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

View 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
def test_that_client_is_not_closed_in_with_block():
with httpx.Client() as client:
assert not client.is_closed
def test_that_client_is_closed_after_with_block():
with httpx.Client() as client:
pass
client.close()
assert client.is_closed
with pytest.raises(RuntimeError):
client.get("http://example.com")
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")