From d1ac75601e1283c573d8e4b4bbc8bbb56b0c3753 Mon Sep 17 00:00:00 2001 From: Francis CLAIRICIA-ROSE-CLAIRE-JOSEPHINE Date: Sun, 29 Oct 2023 00:11:21 +0200 Subject: [PATCH] [FIX] Socket adapters that use asyncio.Transport and asyncio.DatagramTransport do not use is_closing() anymore --- src/easynetwork/serializers/cbor.py | 4 ++-- src/easynetwork_asyncio/datagram/listener.py | 8 +++++++- src/easynetwork_asyncio/datagram/socket.py | 8 +++++++- src/easynetwork_asyncio/stream/socket.py | 9 ++++++++- .../test_asyncio_backend/test_datagram.py | 14 +++++++++----- .../test_async/test_asyncio_backend/test_stream.py | 14 +++++++++----- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/easynetwork/serializers/cbor.py b/src/easynetwork/serializers/cbor.py index c5feee1d..4151317c 100644 --- a/src/easynetwork/serializers/cbor.py +++ b/src/easynetwork/serializers/cbor.py @@ -88,8 +88,8 @@ def __init__( raise ModuleNotFoundError("cbor dependencies are missing. Consider adding 'cbor' extra") from exc super().__init__(expected_load_error=(cbor2.CBORDecodeError, UnicodeError)) - self.__encoder_cls: Callable[[IO[bytes]], cbor2.CBOREncoder] # type: ignore[no-any-unimported] - self.__decoder_cls: Callable[[IO[bytes]], cbor2.CBORDecoder] # type: ignore[no-any-unimported] + self.__encoder_cls: Callable[[IO[bytes]], cbor2.CBOREncoder] # type: ignore[no-any-unimported,unused-ignore] + self.__decoder_cls: Callable[[IO[bytes]], cbor2.CBORDecoder] # type: ignore[no-any-unimported,unused-ignore] if encoder_config is None: encoder_config = CBOREncoderConfig() diff --git a/src/easynetwork_asyncio/datagram/listener.py b/src/easynetwork_asyncio/datagram/listener.py index 3d0843d9..4faf8d6d 100644 --- a/src/easynetwork_asyncio/datagram/listener.py +++ b/src/easynetwork_asyncio/datagram/listener.py @@ -42,6 +42,7 @@ class AsyncioTransportDatagramListenerSocketAdapter(transports.AsyncDatagramList __slots__ = ( "__endpoint", "__socket", + "__closing", ) def __init__(self, endpoint: DatagramEndpoint) -> None: @@ -52,11 +53,16 @@ def __init__(self, endpoint: DatagramEndpoint) -> None: assert socket is not None, "transport must be a socket transport" # nosec assert_used self.__socket: asyncio.trsock.TransportSocket = socket + # asyncio.DatagramTransport.is_closing() can suddently become true if there is something wrong with the socket + # even if transport.close() was never called. + # To bypass this side effect, we use our own flag. + self.__closing: bool = False def is_closing(self) -> bool: - return self.__endpoint.is_closing() + return self.__closing async def aclose(self) -> None: + self.__closing = True self.__endpoint.close() try: await self.__endpoint.wait_closed() diff --git a/src/easynetwork_asyncio/datagram/socket.py b/src/easynetwork_asyncio/datagram/socket.py index c8e08e3a..c2535b78 100644 --- a/src/easynetwork_asyncio/datagram/socket.py +++ b/src/easynetwork_asyncio/datagram/socket.py @@ -41,6 +41,7 @@ class AsyncioTransportDatagramSocketAdapter(transports.AsyncDatagramTransport): __slots__ = ( "__endpoint", "__socket", + "__closing", ) def __init__(self, endpoint: DatagramEndpoint) -> None: @@ -51,8 +52,13 @@ def __init__(self, endpoint: DatagramEndpoint) -> None: assert socket is not None, "transport must be a socket transport" # nosec assert_used self.__socket: asyncio.trsock.TransportSocket = socket + # asyncio.DatagramTransport.is_closing() can suddently become true if there is something wrong with the socket + # even if transport.close() was never called. + # To bypass this side effect, we use our own flag. + self.__closing: bool = False async def aclose(self) -> None: + self.__closing = True self.__endpoint.close() try: return await self.__endpoint.wait_closed() @@ -61,7 +67,7 @@ async def aclose(self) -> None: raise def is_closing(self) -> bool: - return self.__endpoint.is_closing() + return self.__closing async def recv(self) -> bytes: data, _ = await self.__endpoint.recvfrom() diff --git a/src/easynetwork_asyncio/stream/socket.py b/src/easynetwork_asyncio/stream/socket.py index cddffb12..e4afdae9 100644 --- a/src/easynetwork_asyncio/stream/socket.py +++ b/src/easynetwork_asyncio/stream/socket.py @@ -41,6 +41,7 @@ class AsyncioTransportStreamSocketAdapter(transports.AsyncStreamTransport): "__reader", "__writer", "__socket", + "__closing", ) def __init__( @@ -56,7 +57,13 @@ def __init__( assert socket is not None, "Writer transport must be a socket transport" # nosec assert_used self.__socket: asyncio.trsock.TransportSocket = socket + # asyncio.Transport.is_closing() can suddently become true if there is something wrong with the socket + # even if transport.close() was never called. + # To bypass this side effect, we use our own flag. + self.__closing: bool = False + async def aclose(self) -> None: + self.__closing = True if not self.__writer.is_closing(): try: if self.__writer.can_write_eof(): @@ -74,7 +81,7 @@ async def aclose(self) -> None: raise def is_closing(self) -> bool: - return self.__writer.is_closing() + return self.__closing async def recv(self, bufsize: int) -> bytes: if bufsize < 0: diff --git a/tests/unit_test/test_async/test_asyncio_backend/test_datagram.py b/tests/unit_test/test_async/test_asyncio_backend/test_datagram.py index 5eca5f0c..90ebf1cc 100644 --- a/tests/unit_test/test_async/test_asyncio_backend/test_datagram.py +++ b/tests/unit_test/test_async/test_asyncio_backend/test_datagram.py @@ -730,21 +730,25 @@ async def test____aclose____abort_transport_if_cancelled( mock_endpoint.wait_closed.assert_awaited_once_with() mock_endpoint.transport.abort.assert_called_once_with() - async def test____is_closing____return_endpoint_state( + @pytest.mark.parametrize("transport_closed", [False, True], ids=lambda p: f"transport_closed=={p}") + async def test____is_closing____return_internal_flag( self, + transport_closed: bool, socket: AsyncBaseTransport, mock_endpoint: MagicMock, - mocker: MockerFixture, ) -> None: # Arrange - mock_endpoint.is_closing.return_value = mocker.sentinel.is_closing + if transport_closed: + await socket.aclose() + mock_endpoint.reset_mock() + mock_endpoint.is_closing.side_effect = AssertionError # Act state = socket.is_closing() # Assert - mock_endpoint.is_closing.assert_called_once_with() - assert state is mocker.sentinel.is_closing + mock_endpoint.is_closing.assert_not_called() + assert state is transport_closed @pytest.mark.asyncio diff --git a/tests/unit_test/test_async/test_asyncio_backend/test_stream.py b/tests/unit_test/test_async/test_asyncio_backend/test_stream.py index b70f3fca..a0a63c1f 100644 --- a/tests/unit_test/test_async/test_asyncio_backend/test_stream.py +++ b/tests/unit_test/test_async/test_asyncio_backend/test_stream.py @@ -149,21 +149,25 @@ async def test____aclose____abort_transport_if_cancelled( mock_asyncio_writer.wait_closed.assert_awaited_once_with() mock_asyncio_writer.transport.abort.assert_called_once_with() - async def test____is_closing____return_writer_state( + @pytest.mark.parametrize("transport_closed", [False, True], ids=lambda p: f"transport_closed=={p}") + async def test____is_closing____return_internal_flag( self, + transport_closed: bool, socket: AsyncioTransportStreamSocketAdapter, mock_asyncio_writer: MagicMock, - mocker: MockerFixture, ) -> None: # Arrange - mock_asyncio_writer.is_closing.return_value = mocker.sentinel.is_closing + if transport_closed: + await socket.aclose() + mock_asyncio_writer.reset_mock() + mock_asyncio_writer.is_closing.side_effect = AssertionError # Act state = socket.is_closing() # Assert - mock_asyncio_writer.is_closing.assert_called_once_with() - assert state is mocker.sentinel.is_closing + mock_asyncio_writer.is_closing.assert_not_called() + assert state is transport_closed async def test____recv____read_from_reader( self,