Tighten client closed-state behaviour (#1346)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
This commit is contained in:
parent
a3eb0f99dc
commit
2a2bbe58a6
@ -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 "
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user