Skip to content

Commit

Permalink
Wrap backend-specific exceptions in SerializationError/Deserializatio…
Browse files Browse the repository at this point in the history
…nError
  • Loading branch information
agronholm committed Dec 22, 2024
1 parent dd3fcc3 commit 1e2de3d
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 24 deletions.
6 changes: 6 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ Serializer API
.. autoclass:: CustomizableSerializer
.. autoclass:: CustomTypeCodec

Exceptions
----------

.. autoexception:: SerializationError
.. autoexception:: DeserializationError

Marshalling callbacks
---------------------

Expand Down
3 changes: 3 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ This library adheres to `Semantic Versioning 2.0 <https://semver.org/>`_.

- 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)

Expand Down
14 changes: 7 additions & 7 deletions src/asphalt/serialization/__init__.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 11 additions & 3 deletions src/asphalt/serialization/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/asphalt/serialization/_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class SerializationError(Exception):
"""Raised when serialization fails."""


class DeserializationError(Exception):
"""Raised when deserialization fails."""
11 changes: 9 additions & 2 deletions src/asphalt/serialization/serializers/cbor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
13 changes: 10 additions & 3 deletions src/asphalt/serialization/serializers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from asphalt.core import resolve_reference

from .. import DeserializationError, SerializationError
from .._api import CustomizableSerializer
from .._object_codec import DefaultCustomTypeCodec

Expand Down Expand Up @@ -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:
Expand Down
11 changes: 9 additions & 2 deletions src/asphalt/serialization/serializers/msgpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
11 changes: 9 additions & 2 deletions src/asphalt/serialization/serializers/pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any

from .._api import Serializer
from .._exceptions import DeserializationError, SerializationError


class PickleSerializer(Serializer):
Expand All @@ -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:
Expand Down
15 changes: 11 additions & 4 deletions src/asphalt/serialization/serializers/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from ruamel.yaml import YAML

from .. import DeserializationError, SerializationError
from .._api import Serializer


Expand Down Expand Up @@ -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:
Expand Down
18 changes: 17 additions & 1 deletion tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 (
Expand Down Expand Up @@ -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}
Expand Down

0 comments on commit 1e2de3d

Please sign in to comment.