From 1e2de3da55e1fad98cafca2bddb69aaa17e1f39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 22 Dec 2024 16:51:52 +0200 Subject: [PATCH] Wrap backend-specific exceptions in SerializationError/DeserializationError --- docs/api.rst | 6 ++++++ docs/versionhistory.rst | 3 +++ src/asphalt/serialization/__init__.py | 14 +++++++------- src/asphalt/serialization/_api.py | 14 +++++++++++--- src/asphalt/serialization/_exceptions.py | 6 ++++++ src/asphalt/serialization/serializers/cbor.py | 11 +++++++++-- src/asphalt/serialization/serializers/json.py | 13 ++++++++++--- .../serialization/serializers/msgpack.py | 11 +++++++++-- .../serialization/serializers/pickle.py | 11 +++++++++-- src/asphalt/serialization/serializers/yaml.py | 15 +++++++++++---- tests/test_serializers.py | 18 +++++++++++++++++- 11 files changed, 98 insertions(+), 24 deletions(-) create mode 100644 src/asphalt/serialization/_exceptions.py diff --git a/docs/api.rst b/docs/api.rst index dda1953..5a67e92 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -15,6 +15,12 @@ Serializer API .. autoclass:: CustomizableSerializer .. autoclass:: CustomTypeCodec +Exceptions +---------- + +.. autoexception:: SerializationError +.. autoexception:: DeserializationError + Marshalling callbacks --------------------- diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index fcdda0e..680ec1f 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -7,6 +7,9 @@ This library adheres to `Semantic Versioning 2.0 `_. - Dropped support for Python 3.7 and 3.8 - **BACKWARD INCOMPATIBLE** Bumped minimum Asphalt version to 5.0 +- **BACKWARD INCOMPATIBLE** The ``Serializer.serialize()`` and + ``Serializer.deserialize()`` methods now raise ``SerializationError`` and + ``DeserializationError`` regardless of back-end when something goes wrong **6.0.0** (2022-06-04) diff --git a/src/asphalt/serialization/__init__.py b/src/asphalt/serialization/__init__.py index df5d386..2269983 100644 --- a/src/asphalt/serialization/__init__.py +++ b/src/asphalt/serialization/__init__.py @@ -1,16 +1,16 @@ -from typing import Any - from ._api import CustomizableSerializer as CustomizableSerializer from ._api import CustomTypeCodec as CustomTypeCodec from ._api import Serializer as Serializer from ._component import SerializationComponent as SerializationComponent +from ._exceptions import DeserializationError as DeserializationError +from ._exceptions import SerializationError as SerializationError from ._marshalling import default_marshaller as default_marshaller from ._marshalling import default_unmarshaller as default_unmarshaller from ._object_codec import DefaultCustomTypeCodec as DefaultCustomTypeCodec # Re-export imports, so they look like they live directly in this package -key: str -value: Any -for key, value in list(locals().items()): - if getattr(value, "__module__", "").startswith(f"{__name__}."): - value.__module__ = __name__ +for __value in list(locals().values()): + if getattr(__value, "__module__", "").startswith(f"{__name__}."): + __value.__module__ = __name__ + +del __value diff --git a/src/asphalt/serialization/_api.py b/src/asphalt/serialization/_api.py index 5fb82cf..a9a2a3d 100644 --- a/src/asphalt/serialization/_api.py +++ b/src/asphalt/serialization/_api.py @@ -38,15 +38,23 @@ class Serializer(metaclass=ABCMeta): serialization support for custom types. """ - __slots__ = () + __slots__ = ("__weakref__",) @abstractmethod def serialize(self, obj: Any) -> bytes: - """Serialize a Python object into bytes.""" + """ + Serialize a Python object into bytes. + + :raises SerializationError: if serialization fails + """ @abstractmethod def deserialize(self, payload: bytes) -> Any: - """Deserialize bytes into a Python object.""" + """ + Deserialize bytes into a Python object. + + :raises DesrializationError: if deserialization fails + """ @property @abstractmethod diff --git a/src/asphalt/serialization/_exceptions.py b/src/asphalt/serialization/_exceptions.py new file mode 100644 index 0000000..0c84bb2 --- /dev/null +++ b/src/asphalt/serialization/_exceptions.py @@ -0,0 +1,6 @@ +class SerializationError(Exception): + """Raised when serialization fails.""" + + +class DeserializationError(Exception): + """Raised when deserialization fails.""" diff --git a/src/asphalt/serialization/serializers/cbor.py b/src/asphalt/serialization/serializers/cbor.py index 43e101e..169b3e3 100644 --- a/src/asphalt/serialization/serializers/cbor.py +++ b/src/asphalt/serialization/serializers/cbor.py @@ -5,6 +5,7 @@ import cbor2 from asphalt.core import qualified_name, resolve_reference +from .. import DeserializationError, SerializationError from .._api import CustomizableSerializer from .._object_codec import DefaultCustomTypeCodec @@ -129,10 +130,16 @@ def __init__( self.decoder_options: dict[str, Any] = decoder_options or {} def serialize(self, obj: Any) -> bytes: - return cbor2.dumps(obj, **self.encoder_options) + try: + return cbor2.dumps(obj, **self.encoder_options) + except cbor2.CBOREncodeError as exc: + raise SerializationError(str(exc)) from exc def deserialize(self, payload: bytes) -> Any: - return cbor2.loads(payload, **self.decoder_options) + try: + return cbor2.loads(payload, **self.decoder_options) + except cbor2.CBORDecodeError as exc: + raise DeserializationError(str(exc)) from exc @property def mimetype(self) -> str: diff --git a/src/asphalt/serialization/serializers/json.py b/src/asphalt/serialization/serializers/json.py index 8493918..98f3c4a 100644 --- a/src/asphalt/serialization/serializers/json.py +++ b/src/asphalt/serialization/serializers/json.py @@ -6,6 +6,7 @@ from asphalt.core import resolve_reference +from .. import DeserializationError, SerializationError from .._api import CustomizableSerializer from .._object_codec import DefaultCustomTypeCodec @@ -81,11 +82,17 @@ def __init__( self._decoder = JSONDecoder(**self.decoder_options) def serialize(self, obj: Any) -> bytes: - return self._encoder.encode(obj).encode(self.encoding) + try: + return self._encoder.encode(obj).encode(self.encoding) + except Exception as exc: + raise SerializationError(str(exc)) from exc def deserialize(self, payload: bytes) -> Any: - text_payload = payload.decode(self.encoding) - return self._decoder.decode(text_payload) + try: + text_payload = payload.decode(self.encoding) + return self._decoder.decode(text_payload) + except Exception as exc: + raise DeserializationError(str(exc)) from exc @property def mimetype(self) -> str: diff --git a/src/asphalt/serialization/serializers/msgpack.py b/src/asphalt/serialization/serializers/msgpack.py index f73c2af..e8e6538 100644 --- a/src/asphalt/serialization/serializers/msgpack.py +++ b/src/asphalt/serialization/serializers/msgpack.py @@ -5,6 +5,7 @@ from asphalt.core import resolve_reference from msgpack import ExtType, packb, unpackb +from .. import DeserializationError, SerializationError from .._api import CustomizableSerializer from .._object_codec import DefaultCustomTypeCodec @@ -103,10 +104,16 @@ def __init__( self.unpacker_options.setdefault("raw", False) def serialize(self, obj: Any) -> bytes: - return packb(obj, **self.packer_options) # type: ignore[no-any-return] + try: + return packb(obj, **self.packer_options) # type: ignore[no-any-return] + except Exception as exc: + raise SerializationError(str(exc)) from exc def deserialize(self, payload: bytes) -> Any: - return unpackb(payload, **self.unpacker_options) + try: + return unpackb(payload, **self.unpacker_options) + except Exception as exc: + raise DeserializationError(str(exc)) from exc @property def mimetype(self) -> str: diff --git a/src/asphalt/serialization/serializers/pickle.py b/src/asphalt/serialization/serializers/pickle.py index e7c9e16..2f97662 100644 --- a/src/asphalt/serialization/serializers/pickle.py +++ b/src/asphalt/serialization/serializers/pickle.py @@ -4,6 +4,7 @@ from typing import Any from .._api import Serializer +from .._exceptions import DeserializationError, SerializationError class PickleSerializer(Serializer): @@ -26,10 +27,16 @@ def __init__(self, protocol: int = pickle.HIGHEST_PROTOCOL): self.protocol: int = protocol def serialize(self, obj: Any) -> bytes: - return pickle.dumps(obj, protocol=self.protocol) + try: + return pickle.dumps(obj, protocol=self.protocol) + except Exception as exc: + raise SerializationError(str(exc)) from exc def deserialize(self, payload: bytes) -> Any: - return pickle.loads(payload) + try: + return pickle.loads(payload) + except Exception as exc: + raise DeserializationError(str(exc)) from exc @property def mimetype(self) -> str: diff --git a/src/asphalt/serialization/serializers/yaml.py b/src/asphalt/serialization/serializers/yaml.py index 972e069..3c0483b 100644 --- a/src/asphalt/serialization/serializers/yaml.py +++ b/src/asphalt/serialization/serializers/yaml.py @@ -5,6 +5,7 @@ from ruamel.yaml import YAML +from .. import DeserializationError, SerializationError from .._api import Serializer @@ -35,12 +36,18 @@ def __init__(self, safe: bool = True): self._yaml = YAML(typ="safe" if safe else "unsafe") def serialize(self, obj: Any) -> bytes: - buffer = StringIO() - self._yaml.dump(obj, buffer) - return buffer.getvalue().encode("utf-8") + try: + buffer = StringIO() + self._yaml.dump(obj, buffer) + return buffer.getvalue().encode("utf-8") + except Exception as exc: + raise SerializationError(str(exc)) from exc def deserialize(self, payload: bytes) -> Any: - return self._yaml.load(payload) + try: + return self._yaml.load(payload) + except Exception as exc: + raise DeserializationError(str(exc)) from exc @property def mimetype(self) -> str: diff --git a/tests/test_serializers.py b/tests/test_serializers.py index f4c1c9a..4000b3d 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -2,8 +2,10 @@ import re import sys +from binascii import unhexlify from datetime import datetime, timezone from functools import partial +from socket import socket from types import SimpleNamespace from typing import Any, cast @@ -12,7 +14,11 @@ from cbor2 import CBORTag from msgpack import ExtType -from asphalt.serialization import CustomizableSerializer +from asphalt.serialization import ( + CustomizableSerializer, + DeserializationError, + SerializationError, +) from asphalt.serialization.serializers.cbor import CBORSerializer, CBORTypeCodec from asphalt.serialization.serializers.json import JSONSerializer from asphalt.serialization.serializers.msgpack import ( @@ -115,6 +121,16 @@ def test_basic_types_roundtrip(serializer: CustomizableSerializer, input: Any) - assert deserialized == input +def test_serialization_error(serializer: CustomizableSerializer) -> None: + with pytest.raises(SerializationError), socket() as sock: + serializer.serialize(sock) + + +def test_deserialization_error(serializer: CustomizableSerializer) -> None: + with pytest.raises(DeserializationError): + serializer.deserialize(unhexlify("c11b9b9b9b0000000000")) + + @pytest.mark.parametrize("serializer_type", ["cbor", "pickle", "yaml"]) def test_circular_reference(serializer: CustomizableSerializer) -> None: a: dict[str, Any] = {"foo": 1}