diff --git a/docs/source/api/serializers/composite.rst b/docs/source/api/serializers/composite.rst new file mode 100644 index 00000000..cb3fe0f1 --- /dev/null +++ b/docs/source/api/serializers/composite.rst @@ -0,0 +1,24 @@ +********************* +Composite Serializers +********************* + +.. automodule:: easynetwork.serializers.composite + +.. autoclass:: StapledPacketSerializer + :members: + :inherited-members: + +.. autoclass:: StapledIncrementalPacketSerializer + :members: + :inherited-members: + +.. autoclass:: StapledBufferedIncrementalPacketSerializer + :members: + :inherited-members: + +----- + +.. seealso:: + + :doc:`/howto/advanced/serializer_composition` + Describes where and when a :term:`composite serializer` is used. diff --git a/docs/source/api/serializers/index.rst b/docs/source/api/serializers/index.rst index c617c092..64db9ee6 100644 --- a/docs/source/api/serializers/index.rst +++ b/docs/source/api/serializers/index.rst @@ -11,6 +11,7 @@ Serializers :caption: Built-In Serializers abc + composite json pickle line diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index ff978784..9c98c30c 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -34,6 +34,11 @@ Glossary .. seealso:: :class:`.StapledPacketConverter` class. + composite serializer + A :term:`serializer wrapper` that processes different data formats in input and output. + + .. seealso:: :class:`.StapledPacketSerializer` class. + converter An interface responsible for bridging the gap between the Python objects manipulated by the application/software and the :term:`data transfer objects ` handled by the :term:`serializer`. @@ -97,7 +102,7 @@ Glossary if supported by the underlying transport layer. serializer wrapper - A :term:`serializer` that transforms data coming from another :term:`serializer`. + A :term:`serializer` that (potentially) transforms data coming from another :term:`serializer`. Example: diff --git a/docs/source/howto/advanced/index.rst b/docs/source/howto/advanced/index.rst index 73a9ed55..3d8e0d6a 100644 --- a/docs/source/howto/advanced/index.rst +++ b/docs/source/howto/advanced/index.rst @@ -8,4 +8,5 @@ Advanced Guide serializers buffered_serializers serializer_combinations + serializer_composition standalone_servers diff --git a/docs/source/howto/advanced/serializer_combinations.rst b/docs/source/howto/advanced/serializer_combinations.rst index d92043c7..a2959290 100644 --- a/docs/source/howto/advanced/serializer_combinations.rst +++ b/docs/source/howto/advanced/serializer_combinations.rst @@ -1,5 +1,5 @@ ******************************** -How-to — Serializer combinations +How-to — Serializer Combinations ******************************** .. contents:: Table of Contents diff --git a/docs/source/howto/advanced/serializer_composition.rst b/docs/source/howto/advanced/serializer_composition.rst new file mode 100644 index 00000000..d737f920 --- /dev/null +++ b/docs/source/howto/advanced/serializer_composition.rst @@ -0,0 +1,70 @@ +******************************* +How-to — Serializer Composition +******************************* + +.. contents:: Table of Contents + :local: + +------ + +The Basics +========== + +A :term:`composite serializer` fulfills the same need as a :term:`composite converter`, +which is to handle two disjoint formats between sent and received packet data. + +This is typically done using the :class:`.StapledPacketSerializer`: + +>>> import pickle +>>> from easynetwork.serializers import * +>>> from easynetwork.serializers.composite import * +>>> s = StapledPacketSerializer(sent_packet_serializer=PickleSerializer(), received_packet_serializer=JSONSerializer()) +>>> s.deserialize(b'{"data": 42}') +{'data': 42} +>>> data = s.serialize({"data": 42}) +>>> pickle.loads(data) +{'data': 42} + +:class:`.StapledPacketSerializer` will return the correct implementation according to +the base class of ``sent_packet_serializer`` and ``received_packet_serializer``: + +>>> from easynetwork.serializers.abc import * +>>> +>>> StapledPacketSerializer(sent_packet_serializer=PickleSerializer(), received_packet_serializer=JSONSerializer()) +StapledPacketSerializer(...) +>>> isinstance(_, (AbstractIncrementalPacketSerializer, BufferedIncrementalPacketSerializer)) +False +>>> +>>> StapledPacketSerializer(sent_packet_serializer=StringLineSerializer(), received_packet_serializer=JSONSerializer()) +StapledIncrementalPacketSerializer(...) +>>> isinstance(_, AbstractIncrementalPacketSerializer) +True +>>> +>>> StapledPacketSerializer(sent_packet_serializer=JSONSerializer(), received_packet_serializer=StringLineSerializer()) +StapledBufferedIncrementalPacketSerializer(...) +>>> isinstance(_, BufferedIncrementalPacketSerializer) +True + + +Use Case: Different Structure Between A Request And A Response +============================================================== + +>>> from typing import NamedTuple +>>> from easynetwork.serializers import NamedTupleStructSerializer +>>> from easynetwork.serializers.composite import StapledPacketSerializer +>>> class Request(NamedTuple): +... type: int +... data: bytes +... +>>> class Response(NamedTuple): +... rc: int +... message: str +... +>>> s = StapledPacketSerializer( +... sent_packet_serializer=NamedTupleStructSerializer(Request, {"type": "B", "data": "1024s"}, encoding=None), +... received_packet_serializer=NamedTupleStructSerializer(Response, {"rc": "h", "message": "10s"}, encoding="utf8"), +... ) +>>> s.serialize(Request(type=42, data=b"some data to send")) +b'*some data to send\x00\x00\x00\x00\x00...' +>>> s.deserialize(b"\x00\xc8OK\x00\x00\x00\x00\x00\x00\x00\x00") +Response(rc=200, message='OK') diff --git a/pyproject.toml b/pyproject.toml index c3c291ed..63c74a34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -228,6 +228,7 @@ ignore_missing_imports = true [tool.pytest.ini_options] asyncio_mode = "strict" # Avoid some unwanted behaviour addopts = "--dist=worksteal --strict-markers -p 'no:anyio' -p 'no:benchmark'" # hatch CLI dependencies installs anyio +doctest_optionflags = "ELLIPSIS" minversion = "7.1.2" testpaths = ["tests"] norecursedirs = ["scripts"] diff --git a/src/easynetwork/serializers/composite.py b/src/easynetwork/serializers/composite.py new file mode 100644 index 00000000..41fe2d8a --- /dev/null +++ b/src/easynetwork/serializers/composite.py @@ -0,0 +1,268 @@ +# Copyright 2021-2024, Francis Clairicia-Rose-Claire-Josephine +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +"""Packet serializer composite module.""" + +from __future__ import annotations + +__all__ = [ + "StapledBufferedIncrementalPacketSerializer", + "StapledIncrementalPacketSerializer", + "StapledPacketSerializer", +] + +from collections.abc import Generator +from typing import TYPE_CHECKING, Any, Generic, Self, final, overload + +from .._typevars import _T_Buffer, _T_ReceivedDTOPacket, _T_SentDTOPacket +from ..lowlevel._final import runtime_final_class +from .abc import AbstractIncrementalPacketSerializer, AbstractPacketSerializer, BufferedIncrementalPacketSerializer + +if TYPE_CHECKING: + from _typeshed import ReadableBuffer + + +@final +class StapledPacketSerializer(AbstractPacketSerializer[_T_SentDTOPacket, _T_ReceivedDTOPacket]): + """ + A :term:`composite serializer` that merges two serializers. + """ + + __slots__ = ("__sent_packet_serializer", "__received_packet_serializer") + + __sent_packet_serializer: AbstractPacketSerializer[_T_SentDTOPacket, Any] + __received_packet_serializer: AbstractPacketSerializer[Any, _T_ReceivedDTOPacket] + + @overload + def __new__( + cls, + sent_packet_serializer: AbstractIncrementalPacketSerializer[_T_SentDTOPacket, Any], + received_packet_serializer: BufferedIncrementalPacketSerializer[Any, _T_ReceivedDTOPacket, _T_Buffer], + ) -> StapledBufferedIncrementalPacketSerializer[_T_SentDTOPacket, _T_ReceivedDTOPacket, _T_Buffer]: ... + + @overload + def __new__( + cls, + sent_packet_serializer: AbstractIncrementalPacketSerializer[_T_SentDTOPacket, Any], + received_packet_serializer: AbstractIncrementalPacketSerializer[Any, _T_ReceivedDTOPacket], + ) -> StapledIncrementalPacketSerializer[_T_SentDTOPacket, _T_ReceivedDTOPacket]: ... + + @overload + def __new__( + cls, + sent_packet_serializer: AbstractPacketSerializer[_T_SentDTOPacket, Any], + received_packet_serializer: AbstractPacketSerializer[Any, _T_ReceivedDTOPacket], + ) -> Self: ... + + def __new__( + cls, + sent_packet_serializer: AbstractPacketSerializer[_T_SentDTOPacket, Any], + received_packet_serializer: AbstractPacketSerializer[Any, _T_ReceivedDTOPacket], + ) -> Self: + self: StapledPacketSerializer[_T_SentDTOPacket, _T_ReceivedDTOPacket] + match (sent_packet_serializer, received_packet_serializer): + case ( + AbstractIncrementalPacketSerializer(), + BufferedIncrementalPacketSerializer(), + ) if cls is StapledPacketSerializer or cls is StapledIncrementalPacketSerializer: + self = super().__new__(StapledBufferedIncrementalPacketSerializer) + case ( + AbstractIncrementalPacketSerializer(), + AbstractIncrementalPacketSerializer(), + ) if cls is StapledPacketSerializer: + self = super().__new__(StapledIncrementalPacketSerializer) + case _: + self = super().__new__(cls) + + self.__sent_packet_serializer = sent_packet_serializer + self.__received_packet_serializer = received_packet_serializer + return self + + @property + def sent_packet_serializer(self) -> AbstractPacketSerializer[_T_SentDTOPacket, Any]: + """Sent packet serializer.""" + return self.__sent_packet_serializer + + @property + def received_packet_serializer(self) -> AbstractPacketSerializer[Any, _T_ReceivedDTOPacket]: + """Received packet serializer.""" + return self.__received_packet_serializer + + def __repr__(self) -> str: + sent_packet_serializer = self.sent_packet_serializer + received_packet_serializer = self.received_packet_serializer + return f"{self.__class__.__name__}({sent_packet_serializer=!r}, {received_packet_serializer=!r})" + + def serialize(self, packet: _T_SentDTOPacket) -> bytes: + """ + Calls ``self.sent_packet_serializer.serialize(packet)``. + + Parameters: + packet: The Python object to serialize. + + Returns: + a byte sequence. + """ + return self.sent_packet_serializer.serialize(packet) + + def deserialize(self, data: bytes) -> _T_ReceivedDTOPacket: + """ + Calls ``self.received_packet_serializer.deserialize(data)``. + + Parameters: + data: The byte sequence to deserialize. + + Raises: + DeserializeError: An unrelated deserialization error occurred. + + Returns: + the deserialized Python object. + """ + return self.received_packet_serializer.deserialize(data) + + +@final +class StapledIncrementalPacketSerializer( # type: ignore[misc] + StapledPacketSerializer[_T_SentDTOPacket, _T_ReceivedDTOPacket], # pyright: ignore + AbstractIncrementalPacketSerializer[_T_SentDTOPacket, _T_ReceivedDTOPacket], + Generic[_T_SentDTOPacket, _T_ReceivedDTOPacket], +): + """ + A :term:`composite serializer` that merges two incremental serializers. + """ + + __slots__ = () + + if TYPE_CHECKING: + + @overload + def __new__( + cls, + sent_packet_serializer: AbstractIncrementalPacketSerializer[_T_SentDTOPacket, Any], + received_packet_serializer: BufferedIncrementalPacketSerializer[Any, _T_ReceivedDTOPacket, _T_Buffer], + ) -> StapledBufferedIncrementalPacketSerializer[_T_SentDTOPacket, _T_ReceivedDTOPacket, _T_Buffer]: ... + + @overload + def __new__( + cls, + sent_packet_serializer: AbstractIncrementalPacketSerializer[_T_SentDTOPacket, Any], + received_packet_serializer: AbstractIncrementalPacketSerializer[Any, _T_ReceivedDTOPacket], + ) -> Self: ... + + def __new__( + cls, + sent_packet_serializer: AbstractIncrementalPacketSerializer[_T_SentDTOPacket, Any], + received_packet_serializer: AbstractIncrementalPacketSerializer[Any, _T_ReceivedDTOPacket], + ) -> Self: ... + + @property + def sent_packet_serializer(self) -> AbstractIncrementalPacketSerializer[_T_SentDTOPacket, Any]: ... + + @property + def received_packet_serializer(self) -> AbstractIncrementalPacketSerializer[Any, _T_ReceivedDTOPacket]: ... + + def incremental_serialize(self, packet: _T_SentDTOPacket) -> Generator[bytes, None, None]: + """ + Calls ``self.sent_packet_serializer.incremental_serialize(packet)``. + + Parameters: + packet: The Python object to serialize. + + Yields: + all the parts of the :term:`packet`. + """ + return self.sent_packet_serializer.incremental_serialize(packet) + + def incremental_deserialize(self) -> Generator[None, bytes, tuple[_T_ReceivedDTOPacket, bytes]]: + """ + Calls ``self.received_packet_serializer.incremental_deserialize()``. + + Raises: + IncrementalDeserializeError: An unrelated deserialization error occurred. + + Yields: + :data:`None` until the whole :term:`packet` has been deserialized. + + Returns: + a tuple with the deserialized Python object and the unused trailing data. + """ + return self.received_packet_serializer.incremental_deserialize() + + +@final +class StapledBufferedIncrementalPacketSerializer( # type: ignore[misc] + StapledIncrementalPacketSerializer[_T_SentDTOPacket, _T_ReceivedDTOPacket], # pyright: ignore + BufferedIncrementalPacketSerializer[_T_SentDTOPacket, _T_ReceivedDTOPacket, _T_Buffer], + Generic[_T_SentDTOPacket, _T_ReceivedDTOPacket, _T_Buffer], +): + """ + A :term:`composite serializer` that merges two incremental serializers with manual control of the receive buffer. + """ + + __slots__ = () + + if TYPE_CHECKING: + + def __new__( + cls, + sent_packet_serializer: AbstractIncrementalPacketSerializer[_T_SentDTOPacket, Any], + received_packet_serializer: BufferedIncrementalPacketSerializer[Any, _T_ReceivedDTOPacket, _T_Buffer], + ) -> Self: ... + + @property + def sent_packet_serializer(self) -> AbstractIncrementalPacketSerializer[_T_SentDTOPacket, Any]: ... + + @property + def received_packet_serializer(self) -> BufferedIncrementalPacketSerializer[Any, _T_ReceivedDTOPacket, _T_Buffer]: ... + + def create_deserializer_buffer(self, sizehint: int) -> _T_Buffer: + """ + Calls ``self.received_packet_serializer.create_deserializer_buffer(sizehint)``. + + Parameters: + sizehint: the recommended size (in bytes) for the returned buffer. + It is acceptable to return smaller or larger buffers than what `sizehint` suggests. + + Returns: + an object implementing the :ref:`buffer protocol `. It is an error to return a buffer with a zero size. + """ + return self.received_packet_serializer.create_deserializer_buffer(sizehint) + + def buffered_incremental_deserialize( + self, + buffer: _T_Buffer, + ) -> Generator[int | None, int, tuple[_T_ReceivedDTOPacket, ReadableBuffer]]: + """ + Calls ``self.received_packet_serializer.buffered_incremental_deserialize(buffer)``. + + Parameters: + buffer: The buffer allocated by :meth:`create_deserializer_buffer`. + + Raises: + IncrementalDeserializeError: An unrelated deserialization error occurred. + + Yields: + until the whole :term:`packet` has been deserialized. + + Returns: + a tuple with the deserialized Python object and the unused trailing data. + + The remainder can be a :class:`memoryview` pointing to `buffer` or an external :term:`bytes-like object`. + """ + return self.received_packet_serializer.buffered_incremental_deserialize(buffer) + + +runtime_final_class(StapledPacketSerializer) +runtime_final_class(StapledIncrementalPacketSerializer) +runtime_final_class(StapledBufferedIncrementalPacketSerializer) diff --git a/tests/unit_test/test_serializers/test_composite.py b/tests/unit_test/test_serializers/test_composite.py new file mode 100644 index 00000000..21c92899 --- /dev/null +++ b/tests/unit_test/test_serializers/test_composite.py @@ -0,0 +1,310 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, assert_type + +from easynetwork.serializers.composite import ( + StapledBufferedIncrementalPacketSerializer, + StapledIncrementalPacketSerializer, + StapledPacketSerializer, +) + +import pytest + +if TYPE_CHECKING: + from unittest.mock import MagicMock + + from pytest_mock import MockerFixture + + +class TestStapledPacketSerializer: + def test____init_subclass____cannot_be_subclassed(self) -> None: + # Arrange + + # Act & Assert + with pytest.raises(TypeError, match=r"^StapledPacketSerializer cannot be subclassed$"): + + class Subclass(StapledPacketSerializer[Any, Any]): # type: ignore[misc] + ... + + def test____dunder_new____instance_and_type_hint(self) -> None: + # Arrange + from easynetwork.serializers import JSONSerializer, PickleSerializer, StringLineSerializer + + ### PickleSerializer is a one-shot serializer + ### JSONSerializer is an incremental serializer + ### StringLineSerializer is a buffered incremental serializer + # + # Act + stapled_serializer = StapledPacketSerializer( + PickleSerializer(), + PickleSerializer(), + ) + stapled_incremental_serializer = StapledPacketSerializer( + JSONSerializer(), + JSONSerializer(), + ) + stapled_buffered_incremental_serializer = StapledPacketSerializer( + StringLineSerializer(), + StringLineSerializer(), + ) + + # Assert + # + ## These statements are only understandable by mypy + ## assert_type() is a no-op at runtime + assert_type(stapled_serializer, StapledPacketSerializer[Any, Any]) + assert_type(stapled_incremental_serializer, StapledIncrementalPacketSerializer[Any, Any]) + assert_type(stapled_buffered_incremental_serializer, StapledBufferedIncrementalPacketSerializer[str, str, bytearray]) + + assert type(stapled_serializer) is StapledPacketSerializer + assert type(stapled_incremental_serializer) is StapledIncrementalPacketSerializer + assert type(stapled_buffered_incremental_serializer) is StapledBufferedIncrementalPacketSerializer + + def test____serialize____use_sent_packet_serializer( + self, + mock_serializer_factory: Callable[[], MagicMock], + mocker: MockerFixture, + ) -> None: + # Arrange + mock_sent_packet_serializer = mock_serializer_factory() + mock_received_packet_serializer = mock_serializer_factory() + + mock_sent_packet_serializer.configure_mock(**{"serialize.return_value": mocker.sentinel.serialized_packet_data}) + stapled_serializer: StapledPacketSerializer[Any, Any] = StapledPacketSerializer( + sent_packet_serializer=mock_sent_packet_serializer, + received_packet_serializer=mock_received_packet_serializer, + ) + assert stapled_serializer.sent_packet_serializer is mock_sent_packet_serializer + + # Act + data = stapled_serializer.serialize(mocker.sentinel.packet) + + # Assert + mock_sent_packet_serializer.serialize.assert_called_once_with(mocker.sentinel.packet) + mock_received_packet_serializer.serialize.assert_not_called() + assert data is mocker.sentinel.serialized_packet_data + + def test____deserialize____use_received_packet_serializer( + self, + mock_serializer_factory: Callable[[], MagicMock], + mocker: MockerFixture, + ) -> None: + # Arrange + mock_sent_packet_serializer = mock_serializer_factory() + mock_received_packet_serializer = mock_serializer_factory() + + mock_received_packet_serializer.configure_mock(**{"deserialize.return_value": mocker.sentinel.packet}) + stapled_serializer: StapledPacketSerializer[Any, Any] = StapledPacketSerializer( + sent_packet_serializer=mock_sent_packet_serializer, + received_packet_serializer=mock_received_packet_serializer, + ) + assert stapled_serializer.received_packet_serializer is mock_received_packet_serializer + + # Act + packet = stapled_serializer.deserialize(mocker.sentinel.serialized_packet_data) + + # Assert + mock_received_packet_serializer.deserialize.assert_called_once_with(mocker.sentinel.serialized_packet_data) + mock_sent_packet_serializer.deserialize.assert_not_called() + assert packet is mocker.sentinel.packet + + +class TestStapledIncrementalPacketSerializer: + def test____init_subclass____cannot_be_subclassed(self) -> None: + # Arrange + + # Act & Assert + with pytest.raises(TypeError, match=r"^StapledIncrementalPacketSerializer cannot be subclassed$"): + + class Subclass(StapledIncrementalPacketSerializer[Any, Any]): # type: ignore[misc] + ... + + @pytest.mark.parametrize( + "method", + [ + "serialize", + "deserialize", + ], + ) + def test____base_class____implements_default_methods(self, method: str) -> None: + # Arrange + + # Act & Assert + assert getattr(StapledIncrementalPacketSerializer, method) is getattr(StapledPacketSerializer, method) + + def test____dunder_new____instance_and_type_hint(self) -> None: + # Arrange + from easynetwork.serializers import JSONSerializer, StringLineSerializer + + ### JSONSerializer is an incremental serializer + ### StringLineSerializer is a buffered incremental serializer + # + # Act + stapled_incremental_serializer = StapledIncrementalPacketSerializer( + JSONSerializer(), + JSONSerializer(), + ) + stapled_buffered_incremental_serializer = StapledIncrementalPacketSerializer( + StringLineSerializer(), + StringLineSerializer(), + ) + + # Assert + # + ## These statements are only understandable by mypy + ## assert_type() is a no-op at runtime + assert_type(stapled_incremental_serializer, StapledIncrementalPacketSerializer[Any, Any]) + assert_type(stapled_buffered_incremental_serializer, StapledBufferedIncrementalPacketSerializer[str, str, bytearray]) + + assert type(stapled_incremental_serializer) is StapledIncrementalPacketSerializer + assert type(stapled_buffered_incremental_serializer) is StapledBufferedIncrementalPacketSerializer + + def test____incremental_serialize____use_sent_packet_serializer( + self, + mock_incremental_serializer_factory: Callable[[], MagicMock], + mocker: MockerFixture, + ) -> None: + # Arrange + mock_sent_packet_serializer = mock_incremental_serializer_factory() + mock_received_packet_serializer = mock_incremental_serializer_factory() + + mock_sent_packet_serializer.configure_mock( + **{"incremental_serialize.return_value": mocker.sentinel.serialized_packet_generator} + ) + stapled_serializer: StapledIncrementalPacketSerializer[Any, Any] = StapledIncrementalPacketSerializer( + sent_packet_serializer=mock_sent_packet_serializer, + received_packet_serializer=mock_received_packet_serializer, + ) + assert stapled_serializer.sent_packet_serializer is mock_sent_packet_serializer + + # Act + generator = stapled_serializer.incremental_serialize(mocker.sentinel.packet) + + # Assert + mock_sent_packet_serializer.incremental_serialize.assert_called_once_with(mocker.sentinel.packet) + mock_received_packet_serializer.incremental_serialize.assert_not_called() + assert generator is mocker.sentinel.serialized_packet_generator + + def test____incremental_deserialize____use_received_packet_serializer( + self, + mock_incremental_serializer_factory: Callable[[], MagicMock], + mocker: MockerFixture, + ) -> None: + # Arrange + mock_sent_packet_serializer = mock_incremental_serializer_factory() + mock_received_packet_serializer = mock_incremental_serializer_factory() + + mock_received_packet_serializer.configure_mock( + **{"incremental_deserialize.return_value": mocker.sentinel.received_data_generator} + ) + stapled_serializer: StapledIncrementalPacketSerializer[Any, Any] = StapledIncrementalPacketSerializer( + sent_packet_serializer=mock_sent_packet_serializer, + received_packet_serializer=mock_received_packet_serializer, + ) + assert stapled_serializer.received_packet_serializer is mock_received_packet_serializer + + # Act + generator = stapled_serializer.incremental_deserialize() + + # Assert + mock_received_packet_serializer.incremental_deserialize.assert_called_once_with() + mock_sent_packet_serializer.incremental_deserialize.assert_not_called() + assert generator is mocker.sentinel.received_data_generator + + +class TestStapledBufferedIncrementalPacketSerializer: + def test____init_subclass____cannot_be_subclassed(self) -> None: + # Arrange + + # Act & Assert + with pytest.raises(TypeError, match=r"^StapledBufferedIncrementalPacketSerializer cannot be subclassed$"): + + class Subclass(StapledBufferedIncrementalPacketSerializer[Any, Any, Any]): # type: ignore[misc] + ... + + @pytest.mark.parametrize( + "method", + [ + "serialize", + "deserialize", + "incremental_serialize", + "incremental_deserialize", + ], + ) + def test____base_class____implements_default_methods(self, method: str) -> None: + # Arrange + + # Act & Assert + assert getattr(StapledBufferedIncrementalPacketSerializer, method) is getattr(StapledIncrementalPacketSerializer, method) + + def test____dunder_new____instance_and_type_hint(self) -> None: + # Arrange + from easynetwork.serializers import StringLineSerializer + + ### StringLineSerializer is a buffered incremental serializer + # + # Act + stapled_buffered_incremental_serializer = StapledBufferedIncrementalPacketSerializer( + StringLineSerializer(), + StringLineSerializer(), + ) + + # Assert + # + ## These statements are only understandable by mypy + ## assert_type() is a no-op at runtime + assert_type(stapled_buffered_incremental_serializer, StapledBufferedIncrementalPacketSerializer[str, str, bytearray]) + + assert type(stapled_buffered_incremental_serializer) is StapledBufferedIncrementalPacketSerializer + + def test____create_deserializer_buffer____use_received_packet_serializer( + self, + mock_buffered_incremental_serializer_factory: Callable[[], MagicMock], + mock_incremental_serializer_factory: Callable[[], MagicMock], + ) -> None: + # Arrange + mock_received_packet_serializer = mock_buffered_incremental_serializer_factory() + + stapled_serializer: StapledBufferedIncrementalPacketSerializer[Any, Any, memoryview] = ( + StapledBufferedIncrementalPacketSerializer( + sent_packet_serializer=mock_incremental_serializer_factory(), + received_packet_serializer=mock_received_packet_serializer, + ) + ) + assert stapled_serializer.received_packet_serializer is mock_received_packet_serializer + + # Act + buffer = stapled_serializer.create_deserializer_buffer(1024) + + # Assert + mock_received_packet_serializer.create_deserializer_buffer.assert_called_once_with(1024) + assert len(buffer) == 1024 + + def test____buffered_incremental_deserialize____use_received_packet_serializer( + self, + mock_buffered_incremental_serializer_factory: Callable[[], MagicMock], + mock_incremental_serializer_factory: Callable[[], MagicMock], + mocker: MockerFixture, + ) -> None: + # Arrange + mock_received_packet_serializer = mock_buffered_incremental_serializer_factory() + + mock_received_packet_serializer.configure_mock( + **{"buffered_incremental_deserialize.return_value": mocker.sentinel.received_data_generator} + ) + stapled_serializer: StapledBufferedIncrementalPacketSerializer[Any, Any, memoryview] = ( + StapledBufferedIncrementalPacketSerializer( + sent_packet_serializer=mock_incremental_serializer_factory(), + received_packet_serializer=mock_received_packet_serializer, + ) + ) + assert stapled_serializer.received_packet_serializer is mock_received_packet_serializer + buffer = memoryview(bytearray(1024)) + + # Act + generator = stapled_serializer.buffered_incremental_deserialize(buffer) + + # Assert + mock_received_packet_serializer.buffered_incremental_deserialize.assert_called_once_with(buffer) + assert generator is mocker.sentinel.received_data_generator